Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
## [Development]
<!-- Do Not Erase This Section - Used for tracking unreleased changes -->

### Breaking 🔥
- **API v1 Removal**: Removed legacy VGraph/protobuf API v1 support in favor of Apache Arrow API v3.
* Removed `_etl1()`, `_etl_url()`, `_check_url()` methods from `pygraphistry.py`
* Removed API v1 dispatch path from `PlotterBase.py`
* Changed `register(api=...)` parameter type from `Literal[1, 3]` to `Literal[3]`
* Updated `client_session.py` type from `Literal["arrow", "vgraph"]` to `Literal["arrow"]`
* **Migration**: Users calling `graphistry.register(api=1)` must switch to `graphistry.register(api=3)` or omit the parameter (defaults to v3)

### Fixed
- **GFQL:** `Chain` now validates on construction (matching docs) and rejects invalid hops immediately; pass `validate=False` to defer validation when assembling advanced flows (fixes #860).
- **GFQL / eq:** `eq()` now accepts strings in addition to numeric/temporal values (use `isna()`/`notna()` for nulls); added coverage across validator, schema validation, JSON, and GFQL runtime (fixes #862).
Expand Down
50 changes: 19 additions & 31 deletions graphistry/PlotterBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from weakref import WeakValueDictionary

from graphistry.privacy import Privacy, Mode, ModeAction
from graphistry.client_session import ClientSession, AuthManagerProtocol
from graphistry.client_session import ClientSession, AuthManagerProtocol, DatasetInfo

from .constants import SRC, DST, NODE
from .plugins.igraph import to_igraph, from_igraph, compute_igraph, layout_igraph
Expand Down Expand Up @@ -2137,36 +2137,24 @@ def plot(
self._check_mandatory_bindings(not isinstance(n, type(None)))

logger.debug("2. @PloatterBase plot: self._pygraphistry.org_name: {}".format(self.session.org_name))
dataset: Union[ArrowUploader, Dict[str, Any], None] = None
uploader = None # Initialize to avoid UnboundLocalError when api_version != 3
if self.session.api_version == 1:
dataset = self._plot_dispatch(g, n, name, description, 'json', self._style, memoize)
if skip_upload:
return dataset
info = self._pygraphistry._etl1(dataset)
elif self.session.api_version == 3:
logger.debug("3. @PloatterBase plot: self._pygraphistry.org_name: {}".format(self.session.org_name))
self._pygraphistry.refresh()
logger.debug("4. @PloatterBase plot: self._pygraphistry.org_name: {}".format(self.session.org_name))

uploader = dataset = self._plot_dispatch_arrow(g, n, name, description, self._style, memoize)
assert uploader is not None
if skip_upload:
return uploader
uploader.token = self.session.api_token # type: ignore[assignment]
uploader.post(as_files=as_files, memoize=memoize, validate=validate, erase_files_on_fail=erase_files_on_fail)
uploader.maybe_post_share_link(self)
info = {
'name': uploader.dataset_id,
'type': 'arrow',
'viztoken': str(uuid.uuid4())
}
else:
raise ValueError(
f"Unsupported API version: {self.session.api_version}. "
f"Supported versions are 1 and 3. "
f"Please check your graphistry configuration or contact support."
)
uploader = None

logger.debug("3. @PloatterBase plot: self._pygraphistry.org_name: {}".format(self.session.org_name))
self._pygraphistry.refresh()
logger.debug("4. @PloatterBase plot: self._pygraphistry.org_name: {}".format(self.session.org_name))

uploader = self._plot_dispatch_arrow(g, n, name, description, self._style, memoize)
assert uploader is not None
if skip_upload:
return uploader
uploader.token = self.session.api_token # type: ignore[assignment]
uploader.post(as_files=as_files, memoize=memoize, validate=validate, erase_files_on_fail=erase_files_on_fail)
uploader.maybe_post_share_link(self)
info: DatasetInfo = {
'name': uploader.dataset_id,
'type': 'arrow',
'viztoken': str(uuid.uuid4())
}

viz_url = self._pygraphistry._viz_url(info, self._url_params)
cfg_client_protocol_hostname = self.session.client_protocol_hostname
Expand Down
13 changes: 5 additions & 8 deletions graphistry/client_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@



ApiVersion = Literal[1, 3]
ApiVersion = Literal[3]

ENV_GRAPHISTRY_API_KEY = "GRAPHISTRY_API_KEY"

Expand Down Expand Up @@ -55,9 +55,9 @@ def __init__(self) -> None:

env_api_version = get_from_env("GRAPHISTRY_API_VERSION", int)
if env_api_version is None:
env_api_version = 1
elif env_api_version not in [1, 3]:
raise ValueError("Expected API version to be 1, 3, instead got (likely from API_VERSION): %s" % env_api_version)
env_api_version = 3
elif env_api_version != 3:
raise ValueError("Expected API version to be 3 (Arrow format). Legacy API versions 1 and 2 are no longer supported. Got: %s" % env_api_version)
Copy link
Contributor

@lmeyerov lmeyerov Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Api equal 3. Supports arrow, parquet, CSV, json and orc via file api. Basically anything that cudf takes. I do think we do happen to do arrow uploads for DFs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, done!

self.api_version: ApiVersion = cast(ApiVersion, env_api_version)

self.dataset_prefix: str = get_from_env("GRAPHISTRY_DATASET_PREFIX", str, "PyGraphistry/")
Expand Down Expand Up @@ -125,16 +125,13 @@ def as_proxy(self) -> MutableMapping[str, Any]:
class DatasetInfo(TypedDict):
name: str
viztoken: str
type: Literal["arrow", "vgraph"]
type: Literal["arrow"]



class AuthManagerProtocol(Protocol):
session: ClientSession

def _etl1(self, dataset: Any) -> DatasetInfo:
...

def refresh(self, token: Optional[str] = None, fail_silent: bool = False) -> Optional[str]:
...

Expand Down
169 changes: 15 additions & 154 deletions graphistry/pygraphistry.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,26 +104,16 @@ def _is_authenticated(self, value: bool) -> None:
self.session._is_authenticated = value

def authenticate(self) -> None:
"""Authenticate via already provided configuration (api=1,2).
"""Authenticate via already provided configuration.
This is called once automatically per session when uploading and rendering a visualization.
In api=3, if token_refresh_ms > 0 (defaults to 10min), this starts an automatic refresh loop.
In that case, note that a manual .login() is still required every 24hr by default.
If token_refresh_ms > 0 (defaults to 10min), this starts an automatic refresh loop.
Note that a manual .login() is still required every 24hr by default.
"""

if self.api_version() == 3:
if not (self.api_token() is None):
self.refresh()
else:
key = self.api_key()
# Mocks may set to True, so bypass in that case
if (key is None) and (self.session._is_authenticated is False):
util.error(
"In api=1 mode, API key not set explicitly in `register()` or available at "
+ ENV_GRAPHISTRY_API_KEY
)
if not self.session._is_authenticated:
self._check_key_and_version()
self.session._is_authenticated = True
if not (self.api_token() is None):
self.refresh()
elif not self.session._is_authenticated:
self.session._is_authenticated = True

def __reset_token_creds_in_memory(self) -> None:
"""Reset the token and creds in memory, used when switching hosts, switching register method"""
Expand Down Expand Up @@ -578,7 +568,7 @@ def register(
personal_key_secret: Optional[str] = None,
server: Optional[str] = None,
protocol: Optional[str] = None,
api: Optional[Literal[1, 3]] = None,
api: Optional[Literal[3]] = None,
certificate_validation: Optional[bool] = None,
bolt: Optional[Union[Dict, Any]] = None,
store_token_creds_in_memory: Optional[bool] = None,
Expand All @@ -594,15 +584,15 @@ def register(

Changing the key effects all derived Plotter instances.

Provide one of key (deprecated api=1), username/password (api=3) or temporary token (api=3).
Provide username/password or temporary token for authentication.

:param key: API key (deprecated 1.0 API)
:param key: API key (deprecated, ignored)
:type key: Optional[str]
:param username: Account username (2.0 API).
:param username: Account username.
:type username: Optional[str]
:param password: Account password (2.0 API).
:param password: Account password.
:type password: Optional[str]
:param token: Valid Account JWT token (2.0). Provide token, or username/password, but not both.
:param token: Valid Account JWT token. Provide token, or username/password, but not both.
:type token: Optional[str]
:param personal_key_id: Personal Key id for service account.
:type personal_key_id: Optional[str]
Expand All @@ -612,8 +602,8 @@ def register(
:type server: Optional[str]
:param protocol: Protocol to use for server uploaders, defaults to "https".
:type protocol: Optional[str]
:param api: API version to use, defaults to 1 (deprecated slow json 1.0 API), prefer 3 (2.0 API with Arrow+JWT)
:type api: Optional[Literal[1, 3]]
:param api: API version (only 3 is supported, uses Arrow+JWT)
:type api: Optional[Literal[3]]
:param certificate_validation: Override default-on check for valid TLS certificate by setting to True.
:type certificate_validation: Optional[bool]
:param bolt: Neo4j bolt information. Optional driver or named constructor arguments for instantiating a new one.
Expand Down Expand Up @@ -2290,16 +2280,6 @@ def settings(self, height=None, url_params={}, render=None):

return self._plotter().settings(height, url_params, render)

def _etl_url(self):
hostname = self.session.hostname
protocol = self.session.protocol
return "%s://%s/etl" % (protocol, hostname)

def _check_url(self):
hostname = self.session.hostname
protocol = self.session.protocol
return "%s://%s/api/check" % (protocol, hostname)

def _viz_url(self, info: DatasetInfo, url_params: Dict[str, Any]) -> str:
splash_time = int(calendar.timegm(time.gmtime())) + 15
extra = "&".join([k + "=" + str(v) for k, v in list(url_params.items())])
Expand All @@ -2321,125 +2301,6 @@ def _switch_org_url(self, org_name):
return "{}://{}/api/v2/o/{}/switch/".format(protocol, hostname, org_name)


def _coerce_str(self, v):
try:
return str(v)
except UnicodeDecodeError:
print("UnicodeDecodeError")
print("=", v, "=")
x = v.decode("utf-8")
print("x", x)
return x

def _get_data_file(self, dataset, mode):
out_file = io.BytesIO()
if mode == "json":
json_dataset = None
try:
json_dataset = json.dumps(
dataset, ensure_ascii=False, cls=NumpyJSONEncoder
)
except TypeError:
warnings.warn("JSON: Switching from NumpyJSONEncoder to str()")
json_dataset = json.dumps(dataset, default=self._coerce_str)

with gzip.GzipFile(fileobj=out_file, mode="w", compresslevel=9) as f:
if sys.version_info < (3, 0) and isinstance(json_dataset, bytes):
f.write(json_dataset)
else:
f.write(json_dataset.encode("utf8"))
else:
raise ValueError("Unknown mode:", mode)

kb_size = len(out_file.getvalue()) // 1024
if kb_size >= 5 * 1024:
print("Uploading %d kB. This may take a while..." % kb_size)
sys.stdout.flush()

return out_file

def _etl1(self, dataset: Any) -> DatasetInfo:
self.authenticate()

headers = {"Content-Encoding": "gzip", "Content-Type": "application/json"}
params = {
"usertag": self.session._tag,
"agent": "pygraphistry",
"apiversion": "1",
"agentversion": sys.modules["graphistry"].__version__,
"key": self.session.api_key,
}

out_file = self._get_data_file(dataset, "json")
response = requests.post(
self._etl_url(),
out_file.getvalue(),
headers=headers,
params=params,
verify=self.session.certificate_validation,
)
log_requests_error(response)
response.raise_for_status()

try:
jres = response.json()
except Exception:
raise ValueError("Unexpected server response", response)

if jres["success"] is not True:
raise ValueError("Server reported error:", jres["msg"])
else:
return {
"name": jres["dataset"],
"viztoken": jres["viztoken"],
"type": "vgraph",
}


def _check_key_and_version(self):
params = {"text": self.session.api_key}
try:
response = requests.get(
self._check_url(),
params=params,
timeout=(3, 3),
verify=self.session.certificate_validation,
)
log_requests_error(response)
response.raise_for_status()
jres = response.json()

cver = sys.modules["graphistry"].__version__
if (
"pygraphistry" in jres
and "minVersion" in jres["pygraphistry"] # noqa: W503
and "latestVersion" in jres["pygraphistry"] # noqa: W503
):
mver = jres["pygraphistry"]["minVersion"]
lver = jres["pygraphistry"]["latestVersion"]

from packaging.version import parse
try:
if parse(mver) > parse(cver):
util.warn(
"Your version of PyGraphistry is no longer supported (installed=%s latest=%s). Please upgrade!"
% (cver, lver)
)
elif parse(lver) > parse(cver):
print(
"A new version of PyGraphistry is available (installed=%s latest=%s)."
% (cver, lver)
)
except:
raise ValueError(f'Unexpected version value format when comparing {mver}, {cver}, and {lver}')
if jres["success"] is not True:
util.warn(jres["error"])
except Exception:
util.warn(
"Could not contact %s. Are you connected to the Internet?"
% self.session.hostname
)

def layout_settings(self,
play: Optional[int] = None,
locked_x: Optional[bool] = None,
Expand Down
Loading
Loading