Skip to content

Commit 565697d

Browse files
Merge pull request #286 from tigergraph/release_2.0.3
GML-2077 Release 2.0.3
2 parents 63d828e + 1a776f8 commit 565697d

14 files changed

Lines changed: 322 additions & 86 deletions

‎CHANGELOG.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ 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.3] - 2026-04-09
9+
10+
### New Features
11+
12+
- **`dropGraph()` cascade parameter** — `dropGraph(graphName, cascade=True)` automatically removes associated queries and loading jobs. Uses the REST API on TigerGraph >= 4.0 and GSQL `DROP GRAPH ... CASCADE` on older versions.
13+
14+
### Fixed
15+
16+
- **Host URL port extraction** — when the host URL contains a port (e.g. `http://127.0.0.1:14240`), it is now correctly extracted and used instead of producing malformed double-port URLs. Conflicts with explicitly provided `restppPort`/`gsPort` are detected and reported.
17+
- **Host URL validation** — malformed or empty hostnames and out-of-range ports now raise `TigerGraphException` instead of crashing with an uncaught error.
18+
- **HTTP timeout during large file uploads** — the connect timeout no longer stays on the socket during POST body transmission; both connect and read now use a single timeout value derived from `GSQL-TIMEOUT`.
19+
- **Header merge order** — per-request headers (e.g. `timeout` passed to `runLoadingJobWithFile()`) now correctly override `responseConfigHeader` set by `customizeHeader()`.
20+
- **Loading job default timeout** — changed from 16000ms to 0 (use server-wide timeout) for `runLoadingJobWithFile()`, `runLoadingJobWithData()`, `runLoadingJobWithDataFrame()`, and `uploadFile()`.
21+
- **PEP 639 license compliance** — removed the deprecated `License ::` classifier that conflicts with the `license = "Apache-2.0"` expression, fixing build failures with newer setuptools.
22+
23+
---
24+
825
## [2.0.2] - 2026-04-07
926

1027
### New Features
@@ -35,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3552
- **`dropAllDataSources()`** now correctly uses `self.graphname` fallback for the 4.x REST API path.
3653
- **`getVectorIndexStatus()`** no longer produces a malformed URL when called without a graph name; now supports global scope (returns status for all graphs).
3754
- **`previewSampleData()`** now raises `TigerGraphException` when no graph name is available, instead of sending an empty graph name to the server.
55+
- **Host URL port extraction** — when the host URL contains a port (e.g. `http://127.0.0.1:14240`), it is now correctly extracted and used instead of producing malformed double-port URLs. Conflicts with explicitly provided `restppPort`/`gsPort` are detected and reported.
3856
- **Docstring fixes** — corrected `timeout` parameter descriptions across vertex and edge query methods.
3957

4058
---

‎build.sh‎

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -76,32 +76,6 @@ if $DO_UPLOAD; then
7676

7777
echo "---- Uploading to PyPI ----"
7878
python3 -m twine upload dist/*
79-
80-
# Update conda recipe meta.yaml with the new version and sha256
81-
PKG_VERSION=$(grep "^version" pyproject.toml | awk -F'"' '{print $2}')
82-
TARBALL_URL="https://pypi.org/packages/source/p/$PYPI_PACKAGE/$PYPI_PACKAGE-$PKG_VERSION.tar.gz"
83-
84-
echo "---- Updating conda recipe to $PKG_VERSION ----"
85-
# Wait briefly for PyPI to make the tarball available
86-
for i in $(seq 1 30); do
87-
HTTP_CODE=$(curl -sL -o /dev/null -w "%{http_code}" "$TARBALL_URL")
88-
if [[ "$HTTP_CODE" == "200" ]]; then
89-
break
90-
fi
91-
echo " Waiting for PyPI tarball to become available... ($i/30)"
92-
sleep 5
93-
done
94-
if [[ "$HTTP_CODE" != "200" ]]; then
95-
echo "Warning: could not fetch tarball from PyPI. Update meta.yaml manually." >&2
96-
else
97-
NEW_SHA=$(curl -sL "$TARBALL_URL" | sha256sum | awk '{print $1}')
98-
sed -i.bak "s|^ version:.*| version: \"$PKG_VERSION\"|" "$RECIPE_DIR/meta.yaml"
99-
sed -i.bak "s|^ url:.*| url: $TARBALL_URL|" "$RECIPE_DIR/meta.yaml"
100-
sed -i.bak "s|^ sha256:.*| sha256: $NEW_SHA|" "$RECIPE_DIR/meta.yaml"
101-
sed -i.bak "s|^ # sha256:.*| sha256: $NEW_SHA|" "$RECIPE_DIR/meta.yaml"
102-
rm -f "$RECIPE_DIR/meta.yaml.bak"
103-
echo " ✓ Updated meta.yaml: version=$PKG_VERSION sha256=$NEW_SHA"
104-
fi
10579
fi
10680

10781
# ── Conda ───────────────────────────────────────────────────────────────────
@@ -113,7 +87,7 @@ if $DO_CONDA_BUILD; then
11387
fi
11488

11589
# Verify the required version is already published on PyPI before proceeding.
116-
RECIPE_VERSION=$(grep "^ version:" "$RECIPE_DIR/meta.yaml" | awk '{print $2}' | tr -d '"')
90+
RECIPE_VERSION=$(grep '{%\s*set\s*version' "$RECIPE_DIR/meta.yaml" | sed 's/.*"\(.*\)".*/\1/')
11791
echo "---- Checking PyPI for $PYPI_PACKAGE==$RECIPE_VERSION ----"
11892
PYPI_VERSIONS=$(curl -sf "https://pypi.org/pypi/$PYPI_PACKAGE/json" | python3 -c "import sys,json; print('\n'.join(json.load(sys.stdin)['releases'].keys()))" 2>/dev/null || true)
11993
if ! echo "$PYPI_VERSIONS" | grep -qx "$RECIPE_VERSION"; then
@@ -126,21 +100,14 @@ if $DO_CONDA_BUILD; then
126100
echo " ✓ Found $PYPI_PACKAGE==$RECIPE_VERSION on PyPI"
127101
fi
128102

129-
# Compute sha256 of the tarball declared in the recipe and verify it matches.
130-
TARBALL_URL=$(grep "url:" "$RECIPE_DIR/meta.yaml" | awk '{print $2}')
131-
echo "---- Computing sha256 for $TARBALL_URL ----"
103+
# Fetch sha256 from PyPI and update the recipe.
104+
TARBALL_URL=$(grep "url:" "$RECIPE_DIR/meta.yaml" | sed 's/.*url:[[:space:]]*//' | sed "s/{{[^}]*}}/$RECIPE_VERSION/g")
105+
echo "---- Fetching sha256 for $TARBALL_URL ----"
132106
COMPUTED_SHA=$(curl -sL "$TARBALL_URL" | sha256sum | awk '{print $1}')
133-
RECIPE_SHA=$(grep "sha256:" "$RECIPE_DIR/meta.yaml" | awk '{print $2}' || true)
134-
if [[ -n "$RECIPE_SHA" && "$COMPUTED_SHA" != "$RECIPE_SHA" ]]; then
135-
echo "Error: sha256 mismatch!" >&2
136-
echo " recipe : $RECIPE_SHA" >&2
137-
echo " actual : $COMPUTED_SHA" >&2
138-
exit 1
139-
fi
140-
if [[ -z "$RECIPE_SHA" ]]; then
141-
echo "Warning: no sha256 in recipe. For conda-forge submission add:"
142-
echo " sha256: $COMPUTED_SHA"
143-
fi
107+
sed -i.bak "s|^ sha256:.*| sha256: $COMPUTED_SHA|" "$RECIPE_DIR/meta.yaml"
108+
sed -i.bak "s|^ # sha256:.*| sha256: $COMPUTED_SHA|" "$RECIPE_DIR/meta.yaml"
109+
rm -f "$RECIPE_DIR/meta.yaml.bak"
110+
echo " Updated meta.yaml sha256: $COMPUTED_SHA"
144111

145112
echo "---- Building conda package ----"
146113
conda build -c conda-forge "$RECIPE_DIR"

‎pyTigerGraph/common/base.py‎

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ def excepthook(type, value, traceback):
4747
class PyTigerGraphCore(object):
4848
def __init__(self, host: str = "http://127.0.0.1", graphname: str = "",
4949
gsqlSecret: str = "", username: str = "tigergraph", password: str = "tigergraph",
50-
tgCloud: bool = False, restppPort: Union[int, str] = "9000",
51-
gsPort: Union[int, str] = "14240", gsqlVersion: str = "", version: str = "",
50+
tgCloud: bool = False, restppPort: Union[int, str] = None,
51+
gsPort: Union[int, str] = None, gsqlVersion: str = "", version: str = "",
5252
apiToken: str = "", useCert: bool = None, certPath: str = None, debug: bool = None,
5353
sslPort: Union[int, str] = "443", gcp: bool = False, jwtToken: str = ""):
5454
"""Initiate a connection object.
@@ -105,8 +105,50 @@ def __init__(self, host: str = "http://127.0.0.1", graphname: str = "",
105105
if inputHost.scheme not in ["http", "https"]:
106106
raise TigerGraphException("Invalid URL scheme. Supported schemes are http and https.",
107107
"E-0003")
108-
self.netloc = inputHost.netloc
109-
self.host = "{0}://{1}".format(inputHost.scheme, self.netloc)
108+
# Extract port from URL if present (e.g. http://192.168.11.11:14240)
109+
# Use hostname (without port) to avoid double-port URLs later.
110+
hostOnly = inputHost.hostname
111+
if not hostOnly:
112+
raise TigerGraphException(
113+
"Invalid or empty hostname in host URL: " + host)
114+
try:
115+
hostPort = inputHost.port # int or None
116+
except ValueError:
117+
raise TigerGraphException(
118+
"Invalid port in host URL: " + host)
119+
if hostPort is not None:
120+
_hp = str(hostPort)
121+
_restpp_explicit = restppPort is not None
122+
_gs_explicit = gsPort is not None
123+
if _restpp_explicit and _gs_explicit:
124+
# Both ports explicitly provided — URL port must match one.
125+
if str(restppPort) != _hp and str(gsPort) != _hp:
126+
raise TigerGraphException(
127+
"Port {0} in host URL conflicts with restppPort={1} and gsPort={2}. "
128+
"The URL port must match restppPort or gsPort when both are specified."
129+
.format(hostPort, restppPort, gsPort), "E-0003")
130+
elif _restpp_explicit and str(restppPort) != _hp:
131+
raise TigerGraphException(
132+
"Port {0} in host URL conflicts with restppPort={1}. "
133+
"Specify the port in the URL or via restppPort, not both."
134+
.format(hostPort, restppPort), "E-0003")
135+
elif _gs_explicit and str(gsPort) != _hp:
136+
raise TigerGraphException(
137+
"Port {0} in host URL conflicts with gsPort={1}. "
138+
"Specify the port in the URL or via gsPort, not both."
139+
.format(hostPort, gsPort), "E-0003")
140+
# Fill in non-explicit ports from URL port
141+
if not _restpp_explicit:
142+
restppPort = _hp
143+
if not _gs_explicit:
144+
gsPort = _hp
145+
# Apply defaults for any ports still unset
146+
if restppPort is None:
147+
restppPort = "9000"
148+
if gsPort is None:
149+
gsPort = "14240"
150+
self.netloc = hostOnly
151+
self.host = "{0}://{1}".format(inputHost.scheme, hostOnly)
110152
if gsqlSecret != "":
111153
self.username = "__GSQL__secret"
112154
self.password = gsqlSecret
@@ -385,14 +427,14 @@ def _prep_req(self, authMode, headers, url, method, data):
385427
# _refresh_auth_headers() keeps these current after every getToken() call.
386428
_headers = dict(self._cached_auth)
387429

430+
if self.responseConfigHeader:
431+
_headers.update(self.responseConfigHeader)
388432
if headers:
389433
_headers.update(headers)
390434
if self.awsIamHeaders:
391435
# version >=4.1 has removed /gsqlserver/
392436
if url.startswith(self.gsUrl + "/gsqlserver/") or (self._versionGreaterThan4_0() and url.startswith(self.gsUrl)):
393437
_headers.update(self.awsIamHeaders)
394-
if self.responseConfigHeader:
395-
_headers.update(self.responseConfigHeader)
396438
if method == "POST" or method == "PUT" or method == "DELETE":
397439
_data = data
398440
else:

‎pyTigerGraph/pyTigerGraph.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ class TigerGraphConnection(pyTigerGraphVertex, pyTigerGraphEdge, pyTigerGraphUDT
2828

2929
def __init__(self, host: str = "http://127.0.0.1", graphname: str = "",
3030
gsqlSecret: str = "", username: str = "tigergraph", password: str = "tigergraph",
31-
tgCloud: bool = False, restppPort: Union[int, str] = "9000",
32-
gsPort: Union[int, str] = "14240", gsqlVersion: str = "", version: str = "",
31+
tgCloud: bool = False, restppPort: Union[int, str] = None,
32+
gsPort: Union[int, str] = None, gsqlVersion: str = "", version: str = "",
3333
apiToken: str = "", useCert: bool = None, certPath: str = None, debug: bool = None,
3434
sslPort: Union[int, str] = "443", gcp: bool = False, jwtToken: str = ""):
3535
super().__init__(host, graphname, gsqlSecret, username, password, tgCloud, restppPort,

‎pyTigerGraph/pyTigerGraphBase.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ def excepthook(type, value, traceback):
5454
class pyTigerGraphBase(PyTigerGraphCore, object):
5555
def __init__(self, host: str = "http://127.0.0.1", graphname: str = "MyGraph",
5656
gsqlSecret: str = "", username: str = "tigergraph", password: str = "tigergraph",
57-
tgCloud: bool = False, restppPort: Union[int, str] = "9000",
58-
gsPort: Union[int, str] = "14240", gsqlVersion: str = "", version: str = "",
57+
tgCloud: bool = False, restppPort: Union[int, str] = None,
58+
gsPort: Union[int, str] = None, gsqlVersion: str = "", version: str = "",
5959
apiToken: str = "", useCert: bool = None, certPath: str = None, debug: bool = None,
6060
sslPort: Union[int, str] = "443", gcp: bool = False, jwtToken: str = ""):
6161
"""Initiate a connection object.
@@ -209,10 +209,10 @@ def _req(self, method: str, url: str, authMode: str = "token", headers: dict = N
209209
"""
210210
_headers, _data, _ = self._prep_req(authMode, headers, url, method, data)
211211

212-
if "GSQL-TIMEOUT" in _headers:
213-
http_timeout = (30, int(int(_headers["GSQL-TIMEOUT"])/1000) + 30)
212+
if "GSQL-TIMEOUT" in _headers and int(_headers["GSQL-TIMEOUT"]) > 0:
213+
http_timeout = int(int(_headers["GSQL-TIMEOUT"])/1000) + 30
214214
else:
215-
http_timeout = (30, None)
215+
http_timeout = None
216216

217217
conn_err = None
218218
try:

‎pyTigerGraph/pyTigerGraphLoading.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
class pyTigerGraphLoading(pyTigerGraphBase):
3939

4040
def runLoadingJobWithDataFrame(self, df: 'pd.DataFrame', fileTag: str, jobName: str, sep: str = None,
41-
eol: str = None, timeout: int = 16000, sizeLimit: int = 128000000, columns: list = None) -> Union[dict, None]:
41+
eol: str = None, timeout: int = 0, sizeLimit: int = 128000000, columns: list = None) -> Union[dict, None]:
4242
"""Execute a loading job with the given pandas DataFrame with optional column list.
4343
4444
The data string will be posted to the TigerGraph server and the value of the appropriate
@@ -87,7 +87,7 @@ def runLoadingJobWithDataFrame(self, df: 'pd.DataFrame', fileTag: str, jobName:
8787
return res
8888

8989
def runLoadingJobWithFile(self, filePath: str, fileTag: str, jobName: str, sep: str = None,
90-
eol: str = None, timeout: int = 16000, sizeLimit: int = 128000000) -> Union[dict, None]:
90+
eol: str = None, timeout: int = 0, sizeLimit: int = 128000000) -> Union[dict, None]:
9191
"""Execute a loading job with the referenced file.
9292
9393
The file will first be uploaded to the TigerGraph server and the value of the appropriate
@@ -130,7 +130,7 @@ def runLoadingJobWithFile(self, filePath: str, fileTag: str, jobName: str, sep:
130130
return res
131131

132132
def runLoadingJobWithData(self, data: str, fileTag: str, jobName: str, sep: str = None,
133-
eol: str = None, timeout: int = 16000, sizeLimit: int = 128000000) -> Union[dict, None]:
133+
eol: str = None, timeout: int = 0, sizeLimit: int = 128000000) -> Union[dict, None]:
134134
"""Execute a loading job with the given data string.
135135
136136
The data string will be posted to the TigerGraph server and the value of the appropriate
@@ -193,7 +193,7 @@ def runLoadingJobWithData(self, data: str, fileTag: str, jobName: str, sep: str
193193

194194
return res
195195

196-
def uploadFile(self, filePath, fileTag, jobName="", sep=None, eol=None, timeout=16000,
196+
def uploadFile(self, filePath, fileTag, jobName="", sep=None, eol=None, timeout=0,
197197
sizeLimit=128000000) -> dict:
198198
"""DEPRECATED
199199

‎pyTigerGraph/pyTigerGraphSchema.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -540,12 +540,16 @@ def createGraph(self, graphName: str,
540540

541541
return res
542542

543-
def dropGraph(self, graphName: str) -> dict:
543+
def dropGraph(self, graphName: str, cascade: bool = False) -> dict:
544544
"""Drops a graph and all its data.
545545
546546
Args:
547547
graphName:
548548
Name of the graph to drop.
549+
cascade:
550+
When True, automatically removes associated queries and loading jobs.
551+
When False (default), the operation fails if related queries or loading
552+
jobs exist.
549553
550554
Returns:
551555
A dict with at least a ``"message"`` key describing the outcome.
@@ -557,12 +561,15 @@ def dropGraph(self, graphName: str) -> dict:
557561
logger.debug("entry: dropGraph")
558562

559563
if self._version_greater_than_4_0():
564+
params = {"cascade": str(cascade).lower()} if cascade else None
560565
res = self._delete(self.gsUrl + "/gsql/v1/schema/graphs/" + graphName,
561-
authMode="pwd", resKey=None,
566+
authMode="pwd", resKey=None, params=params,
562567
headers={'Content-Type': 'application/json'})
563568
else:
564-
res = _wrap_gsql_result(
565-
self.gsql(f"DROP GRAPH {graphName}"))
569+
cmd = f"DROP GRAPH {graphName}"
570+
if cascade:
571+
cmd += " CASCADE"
572+
res = _wrap_gsql_result(self.gsql(cmd))
566573

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

‎pyTigerGraph/pytgasync/pyTigerGraph.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ class AsyncTigerGraphConnection(AsyncPyTigerGraphVertex, AsyncPyTigerGraphEdge,
2929

3030
def __init__(self, host: str = "http://127.0.0.1", graphname: str = "",
3131
gsqlSecret: str = "", username: str = "tigergraph", password: str = "tigergraph",
32-
tgCloud: bool = False, restppPort: Union[int, str] = "9000",
33-
gsPort: Union[int, str] = "14240", gsqlVersion: str = "", version: str = "",
32+
tgCloud: bool = False, restppPort: Union[int, str] = None,
33+
gsPort: Union[int, str] = None, gsqlVersion: str = "", version: str = "",
3434
apiToken: str = "", useCert: bool = None, certPath: str = None, debug: bool = None,
3535
sslPort: Union[int, str] = "443", gcp: bool = False, jwtToken: str = ""):
3636
super().__init__(host, graphname, gsqlSecret, username, password, tgCloud, restppPort,

‎pyTigerGraph/pytgasync/pyTigerGraphBase.py‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
class AsyncPyTigerGraphBase(PyTigerGraphCore):
4444
def __init__(self, host: str = "http://127.0.0.1", graphname: str = "",
4545
gsqlSecret: str = "", username: str = "tigergraph", password: str = "tigergraph",
46-
tgCloud: bool = False, restppPort: Union[int, str] = "9000",
47-
gsPort: Union[int, str] = "14240", gsqlVersion: str = "", version: str = "",
46+
tgCloud: bool = False, restppPort: Union[int, str] = None,
47+
gsPort: Union[int, str] = None, gsqlVersion: str = "", version: str = "",
4848
apiToken: str = "", useCert: bool = None, certPath: str = None, debug: bool = None,
4949
sslPort: Union[int, str] = "443", gcp: bool = False, jwtToken: str = ""):
5050
"""Initiate a connection object (doc string copied from synchronous __init__).
@@ -148,13 +148,11 @@ async def _req(self, method: str, url: str, authMode: str = "token", headers: di
148148

149149
_headers, _data, _ = self._prep_req(authMode, headers, url, method, data)
150150

151-
if "GSQL-TIMEOUT" in _headers:
152-
http_timeout = aiohttp.ClientTimeout(
153-
sock_connect=30,
154-
total=int(int(_headers["GSQL-TIMEOUT"]) / 1000) + 30,
155-
)
151+
if "GSQL-TIMEOUT" in _headers and int(_headers["GSQL-TIMEOUT"]) > 0:
152+
_total = int(int(_headers["GSQL-TIMEOUT"]) / 1000) + 30
153+
http_timeout = aiohttp.ClientTimeout(total=_total)
156154
else:
157-
http_timeout = aiohttp.ClientTimeout(sock_connect=30, total=None)
155+
http_timeout = aiohttp.ClientTimeout(total=None)
158156

159157
conn_err = None
160158
try:

0 commit comments

Comments
 (0)