Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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 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. Legacy API versions 1 and 2 are no longer supported. Got: %s" % env_api_version)
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
183 changes: 19 additions & 164 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 @@ -535,15 +525,15 @@ def protocol(self, value: Optional[str] = None) -> str:
return value

def api_version(self, value: Optional[ApiVersion] = None) -> ApiVersion:
"""Set or get the API version: 1 for 1.0 (deprecated), 3 for 2.0.
Setting api=2 (protobuf) fully deprecated from the PyGraphistry client.
"""Set or get the API version. Only api=3 is supported.
Legacy API versions 1 and 2 are no longer supported.
Also set via environment variable GRAPHISTRY_API_VERSION."""

if value is None:
value = self.session.api_version

if value not in [1, 3]:
raise ValueError("Expected API version to be 1, 3, instead got: %s" % value)
if value != 3:
raise ValueError("Expected API version to be 3. Legacy API versions 1 and 2 are no longer supported. Got: %s" % value)

# setter
self.session.api_version = value
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 @@ -689,12 +679,6 @@ def register(
import graphistry
graphistry.register(api=3, protocol='http', server='nginx', client_protocol_hostname='https://my.site.com', token='abc')

**Example: Standard (1.0)**
::

import graphistry
graphistry.register(api=1, key="my api key")

"""
global _is_client_mode_warned
if self is PyGraphistry and _client_mode_enabled and not _is_client_mode_warned:
Expand Down Expand Up @@ -2290,16 +2274,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 +2295,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
2 changes: 1 addition & 1 deletion graphistry/tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class NoAuthTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Register once per test class to set up the session
graphistry.register(api=1)
graphistry.register(api=3, verify_token=False)
# HACK: Set _is_authenticated after register() to bypass auth
# This is reset by register(), so we set it after
graphistry.pygraphistry.PyGraphistry._is_authenticated = True
Expand Down
2 changes: 1 addition & 1 deletion graphistry/tests/test_client_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def setup_method(self):
Reset global state at the start of every test. Calling register()
without credentials clears the in-memory session cleanly.
"""
graphistry.register(api=1)
graphistry.register(api=3, verify_token=False)

# --------------------------------------------------------------------- #
# Basic config proxy #
Expand Down
Loading
Loading