Skip to content

Commit 13ae562

Browse files
committed
Revise runInstalledQuery backward compatibility
1 parent 3d7646d commit 13ae562

24 files changed

Lines changed: 1120 additions & 341 deletions

CHANGELOG.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,116 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.0.1] - 2026-03-23
9+
10+
### Breaking Changes
11+
12+
- **`showSecrets()` has been removed.** Replace all calls with `getSecrets()`.
13+
- **`usePost` default changed from `False` to `None`** in `runInstalledQuery()`. The transport is now auto-selected based on `params` type (dict → POST, string → GET). Code that relied on dict params always going through POST is unaffected; code that passed a raw query string and expected POST will now use GET instead.
14+
- **`usePost=True` with a string `params` now raises `TigerGraphException`** instead of silently sending a malformed request body. Convert the string to a dict or drop `usePost=True` instead.
15+
16+
### New Features
17+
18+
- **Unified vertex parameter syntax for `runInstalledQuery()`.** The same tuple notation now works correctly for both GET and POST — no need to format params differently depending on `usePost`:
19+
- `(id,)` — typed vertex `VERTEX<T>`
20+
- `(id, "type")` — untyped vertex `VERTEX`
21+
- `[(id,), ...]` — typed vertex set `SET<VERTEX<T>>`
22+
- `[(id, "type"), ...]` — untyped vertex set `SET<VERTEX>`
23+
- **MAP query parameter support.** Pass a Python `dict` directly for a `MAP` parameter; it is converted to TigerGraph's wire format automatically. A dict with an `"id"` key is treated as a pre-formatted vertex object and passed through unchanged.
24+
25+
### Compatibility Notes
26+
27+
- **Old-style plain IDs for typed vertex params still work**, but at a cost. If you pass `{"p": 1}` instead of `{"p": (1,)}` for a `VERTEX<T>` parameter, `runInstalledQuery()` catches the server-side rejection and retries transparently via GET, logging a warning. Each such call incurs one extra HTTP round-trip. Migrate to `(id,)` tuples to eliminate the overhead.
28+
- **`(id, "")` (empty type string) now raises immediately** on the client instead of forwarding to TigerGraph. Update any code relying on the server-side error to handle the client-side `TigerGraphException` instead.
29+
- **MCP tools have moved to [`pytigergraph-mcp`](https://github.com/tigergraph/pytigergraph-mcp).** Do not import from `pyTigerGraph.mcp` directly; install and use the dedicated `pytigergraph-mcp` package instead.
30+
31+
---
32+
33+
## [2.0.0] - 2025-03-04
34+
35+
### Added
36+
37+
- **MCP (Model Context Protocol) tools.** pyTigerGraph now ships with built-in MCP tool definitions, enabling integration with MCP-compatible AI frameworks.
38+
39+
---
40+
41+
## [1.9.1] - 2024-11-04
42+
43+
### Changed
44+
45+
- API enhancements.
46+
47+
---
48+
49+
## [1.9.0] - 2025-06-30
50+
51+
### Changed
52+
53+
- Multiple API enhancements.
54+
55+
---
56+
57+
## [1.8.4] - 2025-01-20
58+
59+
### Fixed
60+
61+
- Fixed URL construction when `gsPort` and `restppPort` are set to the same value.
62+
63+
---
64+
65+
## [1.8.3] - 2024-12-04
66+
67+
### Fixed
68+
69+
- Fixed `httpx` timeout during async function calls, most notably when installing a query via `.gsql()`.
70+
71+
---
72+
73+
## [1.8.1] - 2024-11-19
74+
75+
### Fixed
76+
77+
- Fixed import error of `TigerGraphException` in the GDS submodule.
78+
79+
---
80+
81+
## [1.8.0] - 2024-11-04
82+
83+
### Added
84+
85+
- **`AsyncTigerGraphConnection`** — full async communication with TigerGraph using the new `AsyncTigerGraphConnection` class.
86+
- **`delVerticesByType()`** — delete all vertices of a given type in one call.
87+
- **`limit` parameter for `getEdgesByType()`** — cap the number of edges returned. Note: the limit is applied client-side after retrieval.
88+
- **Upsert atomicity configuration** — new parameters to control atomicity behaviour of upsert operations.
89+
- **`runLoadingJobWithDataFrame()`** — run a GSQL loading job directly from a Pandas DataFrame.
90+
- **`runLoadingJobWithData()`** — run a GSQL loading job from a raw data string.
91+
92+
---
93+
94+
## [1.7.4] - 2024-10-16
95+
96+
### Fixed
97+
98+
- Fixed error when generating a token via `getToken()` with a secret key.
99+
100+
---
101+
102+
## [1.7.3] - 2024-10-14
103+
104+
### Fixed
105+
106+
- Fixed error when generating a token via `getToken()` on TigerGraph Cloud v3.x instances.
107+
108+
---
109+
110+
## [1.7.2] - 2024-10-01
111+
112+
### Added
113+
114+
- **`delVerticesByType()`** — delete all vertices of a specified type. Supports `permanent` (prevent re-insertion of the same IDs) and `ack` (`"all"` or `"none"`) parameters.
115+
116+
---
117+
8118
## [1.1] - 2022-09-06
9119

10120
Release of pyTigerGraph version 1.1.

pyTigerGraph/__init__.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,8 @@
1111

1212
__license__ = "Apache 2"
1313

14-
# Optional MCP support
15-
try:
16-
from pyTigerGraph.mcp import serve, MCPServer, get_connection, ConnectionManager
17-
__all__ = [
18-
"TigerGraphConnection",
19-
"AsyncTigerGraphConnection",
20-
"TigerGraphException",
21-
"serve",
22-
"MCPServer",
23-
"get_connection",
24-
"ConnectionManager",
25-
]
26-
except ImportError:
27-
# MCP dependencies not installed
28-
__all__ = [
29-
"TigerGraphConnection",
30-
"AsyncTigerGraphConnection",
31-
"TigerGraphException",
32-
]
14+
__all__ = [
15+
"TigerGraphConnection",
16+
"AsyncTigerGraphConnection",
17+
"TigerGraphException",
18+
]

pyTigerGraph/common/query.py

Lines changed: 131 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,34 +53,46 @@ def _parse_query_parameters(params: dict) -> str:
5353
require special handling.
5454
5555
See xref:tigergraph-server:API:built-in-endpoints.adoc#_query_parameter_passing[Query parameter passing]
56-
57-
TODO Accept this format for SET<VERTEX>:
58-
"key": [([p_id1, p_id2, ...], "vtype"), ...]
59-
I.e. multiple primary IDs of the same vertex type
6056
"""
6157
logger.debug("entry: _parseQueryParameters")
6258
logger.debug("params: " + str(params))
6359

6460
parts = []
6561
for k, v in params.items():
6662
if isinstance(v, tuple):
67-
if len(v) == 2 and isinstance(v[1], str):
63+
if len(v) == 1:
64+
# VERTEX<T> (typed): (id,) → k=id
65+
parts.append(k + "=" + _safe_char(v[0]))
66+
elif len(v) == 2 and isinstance(v[1], str):
67+
if not v[1]:
68+
raise TigerGraphException(
69+
f"Invalid vertex parameter '{k}': vertex type string must not be empty. "
70+
"Use (id,) for VERTEX<T> or (id, 'type') for untyped VERTEX.")
71+
# VERTEX (untyped): (id, "type") → k=id&k.type=type
6872
parts.append(k + "=" + str(v[0]))
6973
parts.append(k + ".type=" + _safe_char(v[1]))
7074
else:
7175
raise TigerGraphException(
72-
"Invalid parameter value: (vertex_primary_id, vertex_type)"
73-
" was expected.")
76+
f"Invalid vertex parameter '{k}': expected (id,) for VERTEX<T> "
77+
"or (id, 'type') for untyped VERTEX.")
7478
elif isinstance(v, list):
7579
for i, vv in enumerate(v):
7680
if isinstance(vv, tuple):
77-
if len(vv) == 2 and isinstance(vv[1], str):
81+
if len(vv) == 1:
82+
# SET<VERTEX<T>>: (id,) → k=id (repeated, no index)
83+
parts.append(k + "=" + _safe_char(vv[0]))
84+
elif len(vv) == 2 and isinstance(vv[1], str):
85+
if not vv[1]:
86+
raise TigerGraphException(
87+
f"Invalid vertex parameter '{k}[{i}]': vertex type string must not be empty. "
88+
"Use (id,) for VERTEX<T> or (id, 'type') for untyped VERTEX.")
89+
# SET<VERTEX>: (id, "type") → k[i]=id&k[i].type=type
7890
parts.append(k + "[" + str(i) + "]=" + _safe_char(vv[0]))
7991
parts.append(k + "[" + str(i) + "].type=" + vv[1])
8092
else:
8193
raise TigerGraphException(
82-
"Invalid parameter value: (vertex_primary_id, vertex_type)"
83-
" was expected.")
94+
f"Invalid vertex parameter '{k}[{i}]': expected (id,) for VERTEX<T> "
95+
"or (id, 'type') for untyped VERTEX.")
8496
else:
8597
parts.append(k + "=" + _safe_char(vv))
8698
elif isinstance(v, datetime):
@@ -95,6 +107,115 @@ def _parse_query_parameters(params: dict) -> str:
95107

96108
return ret
97109

110+
def _encode_str_for_post(value: str) -> str:
111+
"""Encode ``%`` as ``%25`` in string values destined for TigerGraph's POST
112+
/query JSON body.
113+
114+
TigerGraph's RESTPP POST parser URL-decodes string values inside JSON
115+
payloads. A bare ``%`` followed by hex-like characters is mis-interpreted
116+
as a percent-encoded sequence, causing a ``REST-30000`` parse error.
117+
Encoding ``%`` → ``%25`` prevents this; the server decodes it back to
118+
``%`` transparently.
119+
"""
120+
return value.replace("%", "%25")
121+
122+
123+
def _prep_query_parameters_json(params: dict) -> dict:
124+
"""Converts a parameter dictionary into the JSON format expected by TigerGraph's
125+
POST /query endpoint.
126+
127+
Handles the same Python conventions as ``_parse_query_parameters`` (used for
128+
GET mode), but produces a JSON-serialisable dict instead of a query string.
129+
130+
Conversion rules (per TigerGraph REST API docs):
131+
- ``(id,)`` 1-tuple → ``{"id": id}``
132+
Use for VERTEX<T> (typed vertex) parameters.
133+
- ``(id, "type")`` 2-tuple → ``{"id": id, "type": "type"}``
134+
Use for VERTEX (untyped vertex) parameters.
135+
- ``list`` of 1-tuples → ``[{"id": id}, ...]``
136+
Use for SET<VERTEX<T>> (typed vertex set) parameters.
137+
- ``list`` of 2-tuples → ``[{"id": id, "type": "type"}, ...]``
138+
Use for SET<VERTEX> (untyped vertex set) parameters.
139+
- ``datetime`` → ``"YYYY-MM-DD HH:MM:SS"``
140+
- ``dict`` with ``"id"`` → passed through unchanged
141+
(caller already supplied correct vertex JSON object)
142+
- ``dict`` without ``"id"``→ ``{"keylist": [...], "valuelist": [...]}``
143+
(Python dict → TigerGraph MAP wire format)
144+
- ``list`` of dicts → passed through unchanged
145+
- ``str`` → ``%`` encoded as ``%25``
146+
(TigerGraph URL-decodes strings inside JSON POST bodies)
147+
- primitive / other → left as-is
148+
149+
Note: plain ``str`` / ``int`` values are correct for STRING/INT/FLOAT/…
150+
parameters only. Always use tuples for vertex parameters — the API cannot
151+
distinguish a STRING value from a VERTEX<T> ID without schema introspection.
152+
153+
See https://docs.tigergraph.com/tigergraph-server/4.2/api/built-in-endpoints#_run_an_installed_query_post
154+
"""
155+
if not params or not isinstance(params, dict):
156+
return params
157+
158+
converted = {}
159+
for k, v in params.items():
160+
if isinstance(v, tuple):
161+
if len(v) == 1:
162+
# VERTEX<T> (typed): (id,) → {"id": id}
163+
converted[k] = {"id": v[0]}
164+
elif len(v) == 2 and isinstance(v[1], str):
165+
if not v[1]:
166+
raise TigerGraphException(
167+
f"Invalid vertex parameter '{k}': vertex type string must not be empty. "
168+
"Use (id,) for VERTEX<T> or (id, 'type') for untyped VERTEX.")
169+
# VERTEX (untyped): (id, "type") → {"id": id, "type": "type"}
170+
converted[k] = {"id": v[0], "type": v[1]}
171+
else:
172+
raise TigerGraphException(
173+
f"Invalid vertex parameter '{k}': expected (id,) for VERTEX<T> "
174+
"or (id, 'type') for untyped VERTEX.")
175+
elif isinstance(v, dict):
176+
if "id" in v:
177+
# Pre-formatted vertex object — pass through unchanged.
178+
converted[k] = v
179+
else:
180+
# Python dict → TigerGraph MAP wire format.
181+
converted[k] = {
182+
"keylist": list(v.keys()),
183+
"valuelist": list(v.values()),
184+
}
185+
elif isinstance(v, list):
186+
new_list = []
187+
for vv in v:
188+
if isinstance(vv, tuple):
189+
if len(vv) == 1:
190+
# SET<VERTEX<T>>: (id,) → {"id": id}
191+
new_list.append({"id": vv[0]})
192+
elif len(vv) == 2 and isinstance(vv[1], str):
193+
if not vv[1]:
194+
raise TigerGraphException(
195+
f"Invalid vertex parameter '{k}': vertex type string must not be empty. "
196+
"Use (id,) for VERTEX<T> or (id, 'type') for untyped VERTEX.")
197+
# SET<VERTEX>: (id, "type") → {"id": id, "type": "type"}
198+
new_list.append({"id": vv[0], "type": vv[1]})
199+
else:
200+
raise TigerGraphException(
201+
f"Invalid vertex parameter '{k}': expected (id,) for VERTEX<T> "
202+
"or (id, 'type') for untyped VERTEX.")
203+
elif isinstance(vv, datetime):
204+
new_list.append(vv.strftime("%Y-%m-%d %H:%M:%S"))
205+
elif isinstance(vv, str):
206+
new_list.append(_encode_str_for_post(vv))
207+
else:
208+
new_list.append(vv)
209+
converted[k] = new_list
210+
elif isinstance(v, datetime):
211+
converted[k] = v.strftime("%Y-%m-%d %H:%M:%S")
212+
elif isinstance(v, str):
213+
converted[k] = _encode_str_for_post(v)
214+
else:
215+
converted[k] = v
216+
return converted
217+
218+
98219
def _prep_run_installed_query(timeout, sizeLimit, runAsync, replica, threadLimit, memoryLimit):
99220
"""header builder for runInstalledQuery()"""
100221
headers = {}

pyTigerGraph/pyTigerGraphAuth.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""
66

77
import logging
8-
import warnings
98
import requests
109

1110
from typing import Union, Tuple, Dict
@@ -136,16 +135,6 @@ def getSecrets(self, userName: str = "", alias: str = "", createNew: bool = Fals
136135

137136
return ret
138137

139-
def showSecrets(self) -> Dict[str, str]:
140-
"""DEPRECATED
141-
142-
Use `getSecrets()` instead.
143-
"""
144-
warnings.warn("The `showSecrets()` function is deprecated; use `getSecrets()` instead.",
145-
DeprecationWarning)
146-
147-
return self.getSecrets()
148-
149138
def createSecret(self, alias: str = "", withAlias: bool = False, value: str = "", userName: str = "" ) -> Union[str, Dict[str, str]]:
150139
"""Creates a secret for generating authentication tokens.
151140
@@ -283,8 +272,7 @@ def dropSecret(self, alias: Union[str, list], ignoreErrors: bool = True, userNam
283272
single_secret = secrets_list[0]
284273
try:
285274
res = self._delete(self.gsUrl+"/gsql/v1/secrets/"+single_secret,
286-
authMode="pwd", resKey="message",
287-
headers={'Content-Type': 'application/json'})
275+
authMode="pwd", resKey=None)
288276
if logger.level == logging.DEBUG:
289277
logger.debug("return: " + str(res))
290278
logger.debug("exit: dropSecret")
@@ -295,7 +283,7 @@ def dropSecret(self, alias: Union[str, list], ignoreErrors: bool = True, userNam
295283
if logger.level == logging.DEBUG:
296284
logger.debug("return: error ignored")
297285
logger.debug("exit: dropSecret")
298-
return {"error": False, "message": "Error ignored as requested"}
286+
return {"error": False, "message": f"Failed to drop secrets: [{single_secret}]"}
299287

300288
# Multiple secrets/aliases or with userName - use payload
301289
url = self.gsUrl+"/gsql/v1/secrets"
@@ -305,8 +293,8 @@ def dropSecret(self, alias: Union[str, list], ignoreErrors: bool = True, userNam
305293

306294
data = {"secrets": secrets_list}
307295
try:
308-
res = self._delete(url, data=data, params=params, authMode="pwd", resKey="message",
309-
headers={'Content-Type': 'application/json'}, jsonData=True)
296+
res = self._delete(url, data=data, params=params, authMode="pwd", resKey=None,
297+
jsonData=True)
310298
if logger.level == logging.DEBUG:
311299
logger.debug("return: " + str(res))
312300
logger.debug("exit: dropSecret")
@@ -317,7 +305,7 @@ def dropSecret(self, alias: Union[str, list], ignoreErrors: bool = True, userNam
317305
if logger.level == logging.DEBUG:
318306
logger.debug("return: error ignored")
319307
logger.debug("exit: dropSecret")
320-
return {"error": False, "message": "Error ignored as requested"}
308+
return {"error": False, "message": f"Failed to drop secrets: {secrets_list}"}
321309
else:
322310
# For older versions, use GSQL command
323311
if isinstance(alias, str):
@@ -574,7 +562,7 @@ def checkJwtToken(self, token: str = None) -> dict:
574562
data = {"token": token}
575563
res = self._post(self.gsUrl+"/gsql/v1/tokens/check",
576564
data=data, authMode="pwd", resKey=None,
577-
headers={'Content-Type': 'application/json'})
565+
headers={'Content-Type': 'application/json'}, jsonData=True)
578566

579567
if logger.level == logging.DEBUG:
580568
logger.debug("return: " + str(res))

0 commit comments

Comments
 (0)