Skip to content

Release 2.0.2#285

Open
chengbiao-jin wants to merge 11 commits intomasterfrom
release_2.0.2
Open

Release 2.0.2#285
chengbiao-jin wants to merge 11 commits intomasterfrom
release_2.0.2

Conversation

@chengbiao-jin
Copy link
Copy Markdown
Collaborator

@chengbiao-jin chengbiao-jin commented Apr 3, 2026

PR Type

Enhancement, Bug fix, Tests, Documentation


Description

  • Add schema change job APIs

  • createGraph supports vertex/edge types

  • Fix auth init and tgCloud handling

  • Normalize boolean params; improve installQueries


Diagram Walkthrough

flowchart LR
  baseAuth["Auth init and token handling"] --> headers["Single cached auth header (_cached_auth)"]
  schemaAPI["Schema change job APIs (create/get/run/drop)"] --> runSC["runSchemaChange: JSON or GSQL"]
  createG["createGraph with vertex/edge lists"] --> v4path["/gsql/v1/schema/graphs (gsql=true)"]
  bools["Boolean param normalization"] --> endpoints["upsertEdges/rebuildGraph string booleans"]
  installQ["installQueries wait parameter"] --> poll["status polling + timeout"]
  scope["useGraph/useGlobal context manager"] --> restore["Correct scope restore on exit"]
Loading

File Walkthrough

Relevant files
Tests
3 files
test_v202_changes.py
Add comprehensive tests for 2.0.2 changes                               
+846/-0 
test_common_query_helpers.py
Validate query param encoding and typing                                 
+522/-0 
test_common_base.py
Test auth header caching and 401 refresh                                 
+243/-0 
Enhancement
11 files
pyTigerGraphSchema.py
Add schema job APIs and createGraph types                               
+303/-34
pyTigerGraphSchema.py
Schema job APIs; createGraph supports types                           
+301/-34
pyTigerGraphEdge.py
Stringify vertex_must_exist and doc updates                           
+3/-3     
pyTigerGraphEdge.py
Convert vertex_must_exist to lowercase string                       
+3/-3     
pyTigerGraphAuth.py
Track token origin for auto-refresh                                           
+1/-0     
pyTigerGraphUtils.py
Stringify rebuildGraph force parameter                                     
+1/-1     
pyTigerGraphBase.py
Core sync helpers for new endpoints                                           
+19/-12 
pyTigerGraphBase.py
Core async helpers for new endpoints                                         
+13/-0   
pyTigerGraphQuery.py
Add installQueries wait and polling                                           
+30/-3   
pyTigerGraphQuery.py
Async installQueries wait and polling                                       
+25/-14 
gsql.py
Wrap GSQL results and parse graph list                                     
+58/-0   
Bug fix
1 files
base.py
Fix auth init order; add scope helpers                                     
+74/-27 
Documentation
3 files
pyTigerGraphVertex.py
Clarify timeout argument documentation                                     
+5/-5     
pyTigerGraphVertex.py
Update timeout parameter docstrings                                           
+5/-5     
CHANGELOG.md
Document auth, schema, and query updates                                 
+33/-0   
Miscellaneous
1 files
__init__.py
Bump version and export updates                                                   
+1/-1     
Configuration changes
1 files
build.sh
Add conda build integration                                                           
+123/-13
Additional files
8 files
pyTigerGraphGSQL.py +25/-2   
pyTigerGraphLoading.py +27/-10 
pyTigerGraphUtils.py +1/-1     
pyTigerGraphAuth.py +1/-0     
pyTigerGraphGSQL.py +25/-2   
pyTigerGraphLoading.py +27/-10 
pyproject.toml +1/-1     
meta.yaml +17/-3   

- Unify _cached_token_auth and _cached_pwd_auth into single _cached_auth
  with JWT > apiToken > Basic fallback, so GSQL endpoints use apiToken
  when available instead of always falling back to Basic auth
- Add `wait` param to installQueries (sync default=True, async default=False)
  to control blocking/polling behavior
- Track token origin (_token_source) to auto-refresh on 401 for tokens
  generated by getToken(), while raising errors for user-provided tokens
…nager

- installQueries polling: use ret.get() to avoid KeyError, increase
  sleep to 10s, add 1-hour timeout with TigerGraphException
- 401 auto-refresh: add _refreshing_token guard to prevent infinite
  recursion when getToken() itself triggers a 401
- useGlobal context manager: re-capture graphname at __enter__ time
  so deferred use restores correctly
@tg-pr-agent
Copy link
Copy Markdown

tg-pr-agent bot commented Apr 3, 2026

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 Security concerns

Query parameter encoding:
runSchemaChangeJob constructs the query string without URL-encoding user-provided values (e.g., graphName, potentially jobName in other places). This can enable query string injection or malformed requests if special characters are used. Prefer passing params to the request helper or applying proper URL encoding.

⚡ Recommended focus areas for review

Auth Mode Ignored

_prep_req now always uses a single cached auth header (_cached_auth) and ignores the passed authMode, which can break endpoints that require Basic auth when a token is present (or vice versa). Please verify intent or restore authMode-aware header selection.

        return {"Authorization": "Basic {0}".format(self.base64_credential)}

def _refresh_auth_headers(self) -> None:
    """Pre-build the cached auth header dict used by every request.

    Called once at __init__ and again after getToken() updates the
    credentials. Eliminates per-request isinstance checks and string
    formatting in _prep_req's hot path.

    Fallback order: JWT > apiToken (tuple or str) > Basic.
    The "X-User-Agent" header is baked in so _prep_req skips that update too.
    """
    # JWT > apiToken > Basic auth
    if isinstance(self.jwtToken, str) and self.jwtToken.strip():
        token_val = "Bearer " + self.jwtToken
    elif isinstance(self.apiToken, tuple):
        token_val = "Bearer " + self.apiToken[0]
    elif isinstance(self.apiToken, str) and self.apiToken.strip():
        token_val = "Bearer " + self.apiToken
    else:
        token_val = "Basic " + self.base64_credential

    self._cached_auth = {"Authorization": token_val, "X-User-Agent": "pyTigerGraph"}

def _verify_jwt_token_support(self):
    try:
        # Check JWT support for RestPP server
        logger.debug(
            "Attempting to verify JWT token support with getVer() on RestPP server.")
        logger.debug(f"Using auth header: {self.authHeader}")
        version = self.getVer()
        logger.info(f"Database version: {version}")

        # Check JWT support for GSQL server
        if self._version_greater_than_4_0():
            logger.debug(
                f"Attempting to get auth info with URL: {self.gsUrl + '/gsql/v1/auth/simple'}")
            self._get(f"{self.gsUrl}/gsql/v1/auth/simple",
                      authMode="token", resKey=None)
        else:
            logger.debug(
                f"Attempting to get auth info with URL: {self.gsUrl + '/gsqlserver/gsql/simpleauth'}")
            self._get(f"{self.gsUrl}/gsqlserver/gsql/simpleauth",
                      authMode="token", resKey=None)
    except requests.exceptions.ConnectionError as e:
        logger.error(f"Connection error: {e}.")
        raise RuntimeError(f"Connection error: {e}.") from e
    except Exception as e:
        message = "The JWT token might be invalid or expired or DB version doesn't support JWT token. Please generate new JWT token or switch to API token or username/password."
        logger.error(f"Error occurred: {e}. {message}")
        raise RuntimeError(message) from e

def _locals(self, _locals: dict) -> str:
    del _locals["self"]
    return str(_locals)

def _error_check(self, res: dict) -> bool:
    """Checks if the JSON document returned by an endpoint has contains `error: true`. If so,
        it raises an exception.

    Args:
        res:
            The output from a request.

    Returns:
        False if no error occurred.

    Raises:
        TigerGraphException: if request returned with error, indicated in the returned JSON.
    """
    if "error" in res and res["error"] and res["error"] != "false":
        # Endpoint might return string "false" rather than Boolean false
        raise TigerGraphException(
            res["message"], (res["code"] if "code" in res else None)
        )
    return False

def _prep_req(self, authMode, headers, url, method, data):
    logger.debug("entry: _prep_req")
    if logger.level == logging.DEBUG:
        logger.debug("params: " + self._locals(locals()))

    # Shallow-copy the pre-built header dict (auth + X-User-Agent already included).
    # _refresh_auth_headers() keeps these current after every getToken() call.
    _headers = dict(self._cached_auth)

    if headers:
        _headers.update(headers)
Content-Type Mismatch

In createSchemaChangeJob (GSQL path), the request sets Content-Type to text/plain but sends a JSON-serialized payload (json.dumps({"gsql": ...})). Either send the raw GSQL string with text/plain or switch to application/json with a JSON body; align with the v4 API expectation. Check the async variant for the same pattern.

        f"}}"
    )
    params = {"gsql": "true", "graph": gname}
else:
    gsql_cmd = (
        f"CREATE GLOBAL SCHEMA_CHANGE JOB {jobName} {{\n"
        f"    {statements}\n"
        f"}}"
    )
    params = {"gsql": "true", "type": "global"}
res = self._post(url, params=params,
                data=json.dumps({"gsql": gsql_cmd}),
                authMode="pwd", resKey=None,
                headers={'Content-Type': 'text/plain'})
URL Encoding

runSchemaChangeJob builds the query string manually (graph=...&force=...) and does not URL-encode values. Use params in _put or urllib.parse.urlencode to avoid issues with special characters; mirror the fix in the async version as well.

query_parts = []
if gname:
    query_parts.append(f"graph={gname}")
if force:
    query_parts.append("force=true")

url = self.gsUrl + "/gsql/v1/schema/jobs/" + jobName
if query_parts:
    url += "?" + "&".join(query_parts)

res = self._put(url, authMode="pwd", resKey=None)

- Add params argument to _put (sync and async) to match _get/_post/_delete
- runSchemaChangeJob now passes query params via _put(params=...) instead
  of manual query string construction
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant