From 9b6b08f759a7b76bcb1b3bedc9b334cf51cdad0f Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 24 Apr 2026 23:55:18 -0500 Subject: [PATCH 1/2] =?UTF-8?q?docs:=20rebrand=20QURL=20=E2=86=92=20qURL?= =?UTF-8?q?=20across=20user-facing=20surfaces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updates the brand spelling to qURL (case-sensitive) in user-visible strings, documentation, code comments, docstrings, and LangChain tool descriptions/responses. Scope: - README.md, CLAUDE.md, pyproject.toml description - Module/class/function docstrings (sync + async clients, types, errors, langchain) - LangChain tool descriptions, response strings (e.g. "qURL r_… has been revoked.", "No qURLs found.") - CI status header in Slack notify step - Test docstring + assertion updated in lockstep Identifiers, paths, and config keys are intentionally untouched: - Class names: QURL, QURLClient, AsyncQURLClient, QURLError, QURLNetworkError, QURLTimeoutError, QURLToolkit, CreateQURLTool, … - Type aliases: QURLStatus, TokenStatus - Field names: qurl_id, qurl_link, qurl_site, qurl_count, active_qurls, qurls_created, max_active_qurls, … - pip package name (layerv-qurl), module name (layerv_qurl) - Tool names (create_qurl, resolve_qurl, list_qurls, delete_qurl) - OpenAPI schema references (CreateQurlRequest, UpdateQurlRequest, QurlData.status, QurlSummary.status) - Repo URLs and slugs A few test fixtures keep "QURL" (mock error `detail` strings that mirror upstream API responses) — these will be updated alongside the upstream qurl-service rebrand to keep mocks accurate. Verified: ruff, mypy, and 158 pytest tests all pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 8 ++-- CLAUDE.md | 2 +- README.md | 12 +++--- pyproject.toml | 2 +- src/layerv_qurl/__init__.py | 2 +- src/layerv_qurl/_utils.py | 2 +- src/layerv_qurl/async_client.py | 66 +++++++++++++++---------------- src/layerv_qurl/client.py | 70 ++++++++++++++++----------------- src/layerv_qurl/errors.py | 4 +- src/layerv_qurl/langchain.py | 26 ++++++------ src/layerv_qurl/types.py | 16 ++++---- tests/test_client.py | 2 +- tests/test_langchain.py | 2 +- 13 files changed, 107 insertions(+), 107 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0c52b7..62a3cfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -119,16 +119,16 @@ jobs: # Determine status if [[ "$AGGREGATE" == "success" ]]; then COLOR="#36a64f"; EMOJI="white_check_mark" - HEADER="QURL Python SDK Build"; STATUS_TEXT="successful" + HEADER="qURL Python SDK Build"; STATUS_TEXT="successful" elif [[ "$AGGREGATE" == "failure" ]]; then COLOR="#dc3545"; EMOJI="x" - HEADER="QURL Python SDK Build"; STATUS_TEXT="failed" + HEADER="qURL Python SDK Build"; STATUS_TEXT="failed" elif [[ "$AGGREGATE" == "cancelled" ]]; then COLOR="#ffc107"; EMOJI="warning" - HEADER="QURL Python SDK Build"; STATUS_TEXT="cancelled" + HEADER="qURL Python SDK Build"; STATUS_TEXT="cancelled" else COLOR="#ffc107"; EMOJI="warning" - HEADER="QURL Python SDK"; STATUS_TEXT="incomplete" + HEADER="qURL Python SDK"; STATUS_TEXT="incomplete" fi # Determine per-gate emoji so the Slack message shows which diff --git a/CLAUDE.md b/CLAUDE.md index d8128bf..216f8ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ ## Project -Python SDK for the QURL API (`pip install layerv-qurl`). Extracted from `layervai/qurl-integrations`. +Python SDK for the qURL API (`pip install layerv-qurl`). Extracted from `layervai/qurl-integrations`. ## Commands diff --git a/README.md b/README.md index 3640cc7..7c5c9e7 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ [![Python](https://img.shields.io/pypi/pyversions/layerv-qurl)](https://pypi.org/project/layerv-qurl/) [![License](https://img.shields.io/github/license/layervai/qurl-python)](LICENSE) -Python SDK for the [QURL API](https://docs.layerv.ai) — secure, time-limited access links for AI agents. +Python SDK for the [qURL API](https://docs.layerv.ai) — secure, time-limited access links for AI agents. -## Why QURL? +## Why qURL? -AI agents need to access APIs, databases, and internal tools — but permanent credentials are a security risk. QURL creates **time-limited, auditable access links** that automatically expire: +AI agents need to access APIs, databases, and internal tools — but permanent credentials are a security risk. qURL creates **time-limited, auditable access links** that automatically expire: - **Time-limited** — links expire after minutes, hours, or days - **IP-scoped** — firewall opens only for the requesting IP via NHP @@ -47,7 +47,7 @@ print(result.qurl_link) # Share this link access = client.resolve("at_k8xqp9h2sj9lx7r4a") print(f"Access granted to {access.target_url} for {access.access_grant.expires_in}s") -# Extend a QURL's expiration +# Extend a qURL's expiration qurl = client.extend("r_xxx", "7d") # Update metadata and policy @@ -74,7 +74,7 @@ asyncio.run(main()) ## Pagination ```python -# Iterate all active QURLs (auto-paginates) +# Iterate all active qURLs (auto-paginates) for qurl in client.list_all(status="active"): print(f"{qurl.resource_id}: {qurl.target_url}") @@ -136,7 +136,7 @@ All error classes inherit from `QURLError`, so `except QURLError` catches everyt ```python quota = client.get_quota() print(f"Plan: {quota.plan}") -print(f"Active QURLs: {quota.usage.active_qurls}") +print(f"Active qURLs: {quota.usage.active_qurls}") print(f"Rate limit: {quota.rate_limits.create_per_minute}/min") ``` diff --git a/pyproject.toml b/pyproject.toml index a40e598..41b1e4f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "layerv-qurl" version = "0.1.0" -description = "Python SDK for the QURL API - secure, time-limited access links" +description = "Python SDK for the qURL API - secure, time-limited access links" readme = "README.md" license = "MIT" requires-python = ">=3.10" diff --git a/src/layerv_qurl/__init__.py b/src/layerv_qurl/__init__.py index fb05601..042e70f 100644 --- a/src/layerv_qurl/__init__.py +++ b/src/layerv_qurl/__init__.py @@ -1,4 +1,4 @@ -"""QURL Python SDK — secure, time-limited access links for AI agents.""" +"""qURL Python SDK — secure, time-limited access links for AI agents.""" from importlib.metadata import PackageNotFoundError from importlib.metadata import version as _pkg_version diff --git a/src/layerv_qurl/_utils.py b/src/layerv_qurl/_utils.py index 9c840e3..68aff67 100644 --- a/src/layerv_qurl/_utils.py +++ b/src/layerv_qurl/_utils.py @@ -320,7 +320,7 @@ def _parse_access_token(data: dict[str, Any]) -> AccessToken: def parse_qurl(data: dict[str, Any]) -> QURL: - """Parse a QURL resource from API response data.""" + """Parse a qURL resource from API response data.""" tokens = None # API returns "qurls" array; SDK exposes as "access_tokens" for clarity. raw_tokens = data.get("qurls") if "qurls" in data else data.get("access_tokens") diff --git a/src/layerv_qurl/async_client.py b/src/layerv_qurl/async_client.py index 5b23502..ba053a9 100644 --- a/src/layerv_qurl/async_client.py +++ b/src/layerv_qurl/async_client.py @@ -1,4 +1,4 @@ -"""Asynchronous QURL API client. +"""Asynchronous qURL API client. NOTE: Business logic mirrors client.py — keep both in sync. """ @@ -64,7 +64,7 @@ class AsyncQURLClient: - """Asynchronous QURL API client. + """Asynchronous qURL API client. Usage:: @@ -132,7 +132,7 @@ async def create( access_policy: AccessPolicy | None = None, custom_domain: str | None = None, ) -> CreateOutput: - """Create a new QURL. + """Create a new qURL. Returns a :class:`CreateOutput` with the ``resource_id``, ``qurl_link``, ``qurl_site``, and ``expires_at``. Use :meth:`get` to fetch the full @@ -149,13 +149,13 @@ async def create( expires_in: Duration string (e.g. ``"24h"``, ``"7d"``). The API uses ``expires_in`` on create; use :meth:`update` with ``expires_at`` if you need an absolute expiry afterwards. - label: Human-readable label for the QURL. Max length 500. - one_time_use: If True, the QURL is consumed on first access. + label: Human-readable label for the qURL. Max length 500. + one_time_use: If True, the qURL is consumed on first access. max_sessions: Maximum concurrent sessions (0 = unlimited). Must be between 0 and 1000 inclusive. session_duration: Duration string for sessions (e.g. ``"1h"``). access_policy: IP/geo/user-agent access restrictions. - custom_domain: Custom domain for the QURL link. Max length 253. + custom_domain: Custom domain for the qURL link. Max length 253. Raises: ValueError: If any field violates the documented API constraints. @@ -182,14 +182,14 @@ async def create( return parse_create_output(resp) async def get(self, resource_id: str) -> QURL: - """Get a QURL resource and its access tokens. + """Get a qURL resource and its access tokens. - Accepts either a resource ID (``r_`` prefix) or a QURL display ID + Accepts either a resource ID (``r_`` prefix) or a qURL display ID (``q_`` prefix); the API resolves ``q_`` IDs to the parent resource automatically. Args: - resource_id: The resource or QURL display ID. + resource_id: The resource or qURL display ID. """ validate_id(resource_id) resp = await self._request("GET", f"/v1/qurls/{resource_id}") @@ -208,25 +208,25 @@ async def list( expires_before: datetime | str | None = None, expires_after: datetime | str | None = None, ) -> ListOutput: - """List QURLs with optional filters. + """List qURLs with optional filters. Args: limit: Maximum number of results per page. cursor: Pagination cursor from a previous response. - status: Filter by QURL status (``"active"``, ``"revoked"``). + status: Filter by qURL status (``"active"``, ``"revoked"``). q: Search query string. sort: Sort order (e.g. ``"created_at"``, ``"-created_at"``). - created_after: Filter QURLs created after this timestamp. + created_after: Filter qURLs created after this timestamp. Accepts a :class:`datetime` (serialized via ``.isoformat()``) or a string. String values must be ISO 8601 / RFC 3339 format (e.g. ``"2026-04-01T00:00:00Z"``) and are passed through to the API unvalidated — the server rejects malformed timestamps with a 400. - created_before: Filter QURLs created before this timestamp. + created_before: Filter qURLs created before this timestamp. Same format rules as ``created_after``. - expires_before: Filter QURLs expiring before this timestamp. + expires_before: Filter qURLs expiring before this timestamp. Same format rules as ``created_after``. - expires_after: Filter QURLs expiring after this timestamp. + expires_after: Filter qURLs expiring after this timestamp. Same format rules as ``created_after``. """ params = build_list_params( @@ -255,7 +255,7 @@ async def list_all( expires_before: datetime | str | None = None, expires_after: datetime | str | None = None, ) -> AsyncIterator[QURL]: - """Iterate over all QURLs, automatically paginating. + """Iterate over all qURLs, automatically paginating. Yields individual :class:`QURL` objects, fetching pages transparently. @@ -264,17 +264,17 @@ async def list_all( q: Search query string. sort: Sort order. page_size: Number of items per page (default 50). - created_after: Filter QURLs created after this timestamp. + created_after: Filter qURLs created after this timestamp. Accepts a :class:`datetime` (serialized via ``.isoformat()``) or a string. String values must be ISO 8601 / RFC 3339 format (e.g. ``"2026-04-01T00:00:00Z"``) and are passed through to the API unvalidated — the server rejects malformed timestamps with a 400. - created_before: Filter QURLs created before this timestamp. + created_before: Filter qURLs created before this timestamp. Same format rules as ``created_after``. - expires_before: Filter QURLs expiring before this timestamp. + expires_before: Filter qURLs expiring before this timestamp. Same format rules as ``created_after``. - expires_after: Filter QURLs expiring after this timestamp. + expires_after: Filter qURLs expiring after this timestamp. Same format rules as ``created_after``. """ cursor: str | None = None @@ -297,9 +297,9 @@ async def list_all( cursor = page.next_cursor async def delete(self, resource_id: str) -> None: - """Delete (revoke) a QURL resource and all its access tokens. + """Delete (revoke) a qURL resource and all its access tokens. - Only accepts a resource ID (``r_`` prefix), not a QURL display ID + Only accepts a resource ID (``r_`` prefix), not a qURL display ID (``q_`` prefix). Per the OpenAPI spec: *"Requires a resource ID (r_ prefix). To revoke a single token, use DELETE /v1/resources/:id/qurls/:qurl_id"*. @@ -319,15 +319,15 @@ async def delete(self, resource_id: str) -> None: await self._request("DELETE", f"/v1/qurls/{resource_id}") async def extend(self, resource_id: str, duration: str) -> QURL: - """Extend a QURL's expiration. + """Extend a qURL's expiration. Convenience method — equivalent to ``await update(resource_id, extend_by=duration)``. - Accepts either a resource ID (``r_`` prefix) or a QURL display ID + Accepts either a resource ID (``r_`` prefix) or a qURL display ID (``q_`` prefix); the API resolves ``q_`` IDs to the parent resource automatically. Args: - resource_id: Resource or QURL display ID. + resource_id: Resource or qURL display ID. duration: Duration to add (e.g. ``"7d"``, ``"24h"``). """ return await self.update(resource_id, extend_by=duration) @@ -341,9 +341,9 @@ async def update( description: str | None = None, tags: builtins.list[str] | None = None, ) -> QURL: - """Update a QURL — extend expiration, change description, set tags. + """Update a qURL — extend expiration, change description, set tags. - Accepts either a resource ID (``r_`` prefix) or a QURL display ID + Accepts either a resource ID (``r_`` prefix) or a qURL display ID (``q_`` prefix). All fields are optional, but at least one must be provided. ``extend_by`` and ``expires_at`` are mutually exclusive. @@ -355,7 +355,7 @@ async def update( scope, base resource policy unchanged). Args: - resource_id: Resource or QURL display ID. + resource_id: Resource or qURL display ID. extend_by: Duration to add (e.g. ``"7d"``). Mutually exclusive with ``expires_at``. expires_at: New absolute expiry. Mutually exclusive with @@ -420,14 +420,14 @@ async def mint_link( session_duration: str | None = None, access_policy: AccessPolicy | None = None, ) -> MintOutput: - """Mint a new access link for a QURL. + """Mint a new access link for a qURL. - Accepts either a resource ID (``r_`` prefix) or a QURL display ID + Accepts either a resource ID (``r_`` prefix) or a qURL display ID (``q_`` prefix). ``expires_in`` and ``expires_at`` are mutually exclusive — if neither is set, the link defaults to 24 hours. Args: - resource_id: Resource or QURL display ID. + resource_id: Resource or qURL display ID. expires_at: Absolute expiry for the minted link. Mutually exclusive with ``expires_in``. expires_in: Duration string for the link (e.g. ``"24h"``). @@ -468,7 +468,7 @@ async def batch_create( self, items: Sequence[BatchCreateItem], ) -> BatchCreateOutput: - """Create multiple QURLs at once (1-100 items). + """Create multiple qURLs at once (1-100 items). Each item is validated against the same spec constraints as :meth:`create` before the request is sent, with per-item errors @@ -545,7 +545,7 @@ async def batch_create( return parse_batch_create_output(resp) async def resolve(self, access_token: str) -> ResolveOutput: - """Resolve a QURL access token (headless). + """Resolve a qURL access token (headless). Triggers an NHP knock to open firewall access for the caller's IP. Requires ``qurl:resolve`` scope on the API key. diff --git a/src/layerv_qurl/client.py b/src/layerv_qurl/client.py index cd8a885..232f6a4 100644 --- a/src/layerv_qurl/client.py +++ b/src/layerv_qurl/client.py @@ -1,4 +1,4 @@ -"""Synchronous QURL API client. +"""Synchronous qURL API client. NOTE: Business logic mirrors async_client.py — keep both in sync. Input validation, body construction, and error handling must match exactly. @@ -64,7 +64,7 @@ class QURLClient: - """Synchronous QURL API client. + """Synchronous qURL API client. Usage:: @@ -78,13 +78,13 @@ class QURLClient: # Resolve an access token (opens firewall for your IP) access = client.resolve("at_k8xqp9h2sj9lx7r4a") - # Extend a QURL's expiration + # Extend a qURL's expiration qurl = client.extend("r_xxx", "7d") # Update metadata qurl = client.update("r_xxx", description="updated") - # Iterate all active QURLs + # Iterate all active qURLs for qurl in client.list_all(status="active"): print(qurl.resource_id) @@ -147,7 +147,7 @@ def create( access_policy: AccessPolicy | None = None, custom_domain: str | None = None, ) -> CreateOutput: - """Create a new QURL. + """Create a new qURL. Returns a :class:`CreateOutput` with the ``resource_id``, ``qurl_link``, ``qurl_site``, and ``expires_at``. Use :meth:`get` to fetch the full @@ -164,13 +164,13 @@ def create( expires_in: Duration string (e.g. ``"24h"``, ``"7d"``). The API uses ``expires_in`` on create; use :meth:`update` with ``expires_at`` if you need an absolute expiry afterwards. - label: Human-readable label for the QURL. Max length 500. - one_time_use: If True, the QURL is consumed on first access. + label: Human-readable label for the qURL. Max length 500. + one_time_use: If True, the qURL is consumed on first access. max_sessions: Maximum concurrent sessions (0 = unlimited). Must be between 0 and 1000 inclusive. session_duration: Duration string for sessions (e.g. ``"1h"``). access_policy: IP/geo/user-agent access restrictions. - custom_domain: Custom domain for the QURL link. Max length 253. + custom_domain: Custom domain for the qURL link. Max length 253. Raises: ValueError: If any field violates the documented API constraints. @@ -197,14 +197,14 @@ def create( return parse_create_output(resp) def get(self, resource_id: str) -> QURL: - """Get a QURL resource and its access tokens. + """Get a qURL resource and its access tokens. - Accepts either a resource ID (``r_`` prefix) or a QURL display ID + Accepts either a resource ID (``r_`` prefix) or a qURL display ID (``q_`` prefix); the API resolves ``q_`` IDs to the parent resource automatically. Args: - resource_id: The resource or QURL display ID. + resource_id: The resource or qURL display ID. """ validate_id(resource_id) resp = self._request("GET", f"/v1/qurls/{resource_id}") @@ -223,25 +223,25 @@ def list( expires_before: datetime | str | None = None, expires_after: datetime | str | None = None, ) -> ListOutput: - """List QURLs with optional filters. + """List qURLs with optional filters. Args: limit: Maximum number of results per page. cursor: Pagination cursor from a previous response. - status: Filter by QURL status (``"active"``, ``"revoked"``). + status: Filter by qURL status (``"active"``, ``"revoked"``). q: Search query string. sort: Sort order (e.g. ``"created_at"``, ``"-created_at"``). - created_after: Filter QURLs created after this timestamp. + created_after: Filter qURLs created after this timestamp. Accepts a :class:`datetime` (serialized via ``.isoformat()``) or a string. String values must be ISO 8601 / RFC 3339 format (e.g. ``"2026-04-01T00:00:00Z"``) and are passed through to the API unvalidated — the server rejects malformed timestamps with a 400. - created_before: Filter QURLs created before this timestamp. + created_before: Filter qURLs created before this timestamp. Same format rules as ``created_after``. - expires_before: Filter QURLs expiring before this timestamp. + expires_before: Filter qURLs expiring before this timestamp. Same format rules as ``created_after``. - expires_after: Filter QURLs expiring after this timestamp. + expires_after: Filter qURLs expiring after this timestamp. Same format rules as ``created_after``. """ params = build_list_params( @@ -270,7 +270,7 @@ def list_all( expires_before: datetime | str | None = None, expires_after: datetime | str | None = None, ) -> Iterator[QURL]: - """Iterate over all QURLs, automatically paginating. + """Iterate over all qURLs, automatically paginating. Yields individual :class:`QURL` objects, fetching pages transparently. @@ -279,17 +279,17 @@ def list_all( q: Search query string. sort: Sort order. page_size: Number of items per page (default 50). - created_after: Filter QURLs created after this timestamp. + created_after: Filter qURLs created after this timestamp. Accepts a :class:`datetime` (serialized via ``.isoformat()``) or a string. String values must be ISO 8601 / RFC 3339 format (e.g. ``"2026-04-01T00:00:00Z"``) and are passed through to the API unvalidated — the server rejects malformed timestamps with a 400. - created_before: Filter QURLs created before this timestamp. + created_before: Filter qURLs created before this timestamp. Same format rules as ``created_after``. - expires_before: Filter QURLs expiring before this timestamp. + expires_before: Filter qURLs expiring before this timestamp. Same format rules as ``created_after``. - expires_after: Filter QURLs expiring after this timestamp. + expires_after: Filter qURLs expiring after this timestamp. Same format rules as ``created_after``. """ cursor: str | None = None @@ -311,9 +311,9 @@ def list_all( cursor = page.next_cursor def delete(self, resource_id: str) -> None: - """Delete (revoke) a QURL resource and all its access tokens. + """Delete (revoke) a qURL resource and all its access tokens. - Only accepts a resource ID (``r_`` prefix), not a QURL display ID + Only accepts a resource ID (``r_`` prefix), not a qURL display ID (``q_`` prefix). Per the OpenAPI spec: *"Requires a resource ID (r_ prefix). To revoke a single token, use DELETE /v1/resources/:id/qurls/:qurl_id"*. @@ -333,15 +333,15 @@ def delete(self, resource_id: str) -> None: self._request("DELETE", f"/v1/qurls/{resource_id}") def extend(self, resource_id: str, duration: str) -> QURL: - """Extend a QURL's expiration. + """Extend a qURL's expiration. Convenience method — equivalent to ``update(resource_id, extend_by=duration)``. - Accepts either a resource ID (``r_`` prefix) or a QURL display ID + Accepts either a resource ID (``r_`` prefix) or a qURL display ID (``q_`` prefix); the API resolves ``q_`` IDs to the parent resource automatically. Args: - resource_id: Resource or QURL display ID. + resource_id: Resource or qURL display ID. duration: Duration to add (e.g. ``"7d"``, ``"24h"``). """ return self.update(resource_id, extend_by=duration) @@ -355,9 +355,9 @@ def update( description: str | None = None, tags: builtins.list[str] | None = None, ) -> QURL: - """Update a QURL — extend expiration, change description, set tags. + """Update a qURL — extend expiration, change description, set tags. - Accepts either a resource ID (``r_`` prefix) or a QURL display ID + Accepts either a resource ID (``r_`` prefix) or a qURL display ID (``q_`` prefix). All fields are optional, but at least one must be provided. ``extend_by`` and ``expires_at`` are mutually exclusive. @@ -369,7 +369,7 @@ def update( scope, base resource policy unchanged). Args: - resource_id: Resource or QURL display ID. + resource_id: Resource or qURL display ID. extend_by: Duration to add (e.g. ``"7d"``). Mutually exclusive with ``expires_at``. expires_at: New absolute expiry. Mutually exclusive with @@ -434,14 +434,14 @@ def mint_link( session_duration: str | None = None, access_policy: AccessPolicy | None = None, ) -> MintOutput: - """Mint a new access link for a QURL. + """Mint a new access link for a qURL. - Accepts either a resource ID (``r_`` prefix) or a QURL display ID + Accepts either a resource ID (``r_`` prefix) or a qURL display ID (``q_`` prefix). ``expires_in`` and ``expires_at`` are mutually exclusive — if neither is set, the link defaults to 24 hours. Args: - resource_id: Resource or QURL display ID. + resource_id: Resource or qURL display ID. expires_at: Absolute expiry for the minted link. Mutually exclusive with ``expires_in``. expires_in: Duration string for the link (e.g. ``"24h"``). @@ -482,7 +482,7 @@ def batch_create( self, items: Sequence[BatchCreateItem], ) -> BatchCreateOutput: - """Create multiple QURLs at once (1-100 items). + """Create multiple qURLs at once (1-100 items). Each item is validated against the same spec constraints as :meth:`create` before the request is sent, with per-item errors @@ -559,7 +559,7 @@ def batch_create( return parse_batch_create_output(resp) def resolve(self, access_token: str) -> ResolveOutput: - """Resolve a QURL access token (headless). + """Resolve a qURL access token (headless). Triggers an NHP knock to open firewall access for the caller's IP. Requires ``qurl:resolve`` scope on the API key. diff --git a/src/layerv_qurl/errors.py b/src/layerv_qurl/errors.py index 1804ea4..3c72e08 100644 --- a/src/layerv_qurl/errors.py +++ b/src/layerv_qurl/errors.py @@ -1,4 +1,4 @@ -"""Error types for the QURL API client.""" +"""Error types for the qURL API client.""" from __future__ import annotations @@ -35,7 +35,7 @@ class QURLError(Exception): except AuthenticationError: print("Bad API key") except NotFoundError: - print("QURL doesn't exist") + print("qURL doesn't exist") except RateLimitError as e: print(f"Rate limited — retry in {e.retry_after}s") except QURLError as e: diff --git a/src/layerv_qurl/langchain.py b/src/layerv_qurl/langchain.py index 500d93d..87c3d5a 100644 --- a/src/layerv_qurl/langchain.py +++ b/src/layerv_qurl/langchain.py @@ -1,4 +1,4 @@ -"""LangChain tool integration for QURL. +"""LangChain tool integration for qURL. Install with: pip install layerv-qurl[langchain] """ @@ -34,7 +34,7 @@ class CreateQURLTool(BaseTool): name: str = "create_qurl" description: str = ( - "Create a QURL — a secure, time-limited access link. " + "Create a qURL — a secure, time-limited access link. " "Input should be a JSON string with 'target_url' (required), " "and optionally 'expires_in' (e.g. '24h', '7d'), 'label'." ) @@ -53,7 +53,7 @@ def _run( label=label, ) return ( - f"Created QURL {result.resource_id}\n" + f"Created qURL {result.resource_id}\n" f"Link: {result.qurl_link}\n" f"Site: {result.qurl_site}\n" f"Expires: {result.expires_at or 'N/A'}" @@ -61,11 +61,11 @@ def _run( class ResolveQURLTool(BaseTool): - """Resolve a QURL access token to open firewall access.""" + """Resolve a qURL access token to open firewall access.""" name: str = "resolve_qurl" description: str = ( - "Resolve a QURL access token to gain firewall access to the protected resource. " + "Resolve a qURL access token to gain firewall access to the protected resource. " "Input should be the access token string (e.g. 'at_k8xqp9h2sj9lx7r4a')." ) client: Any = None @@ -88,10 +88,10 @@ def _run( class ListQURLsTool(BaseTool): - """List active QURL links.""" + """List active qURL links.""" name: str = "list_qurls" - description: str = "List active QURL links. Optionally filter by status (active, revoked)." + description: str = "List active qURL links. Optionally filter by status (active, revoked)." client: Any = None def _run( @@ -102,7 +102,7 @@ def _run( ) -> str: result = self.client.list(status=status, limit=limit) if not result.qurls: - return "No QURLs found." + return "No qURLs found." lines = [] for q in result.qurls: lines.append(f"- {q.resource_id}: {q.target_url} [{q.status}] expires={q.expires_at}") @@ -110,10 +110,10 @@ def _run( class DeleteQURLTool(BaseTool): - """Revoke a QURL, immediately ending all access.""" + """Revoke a qURL, immediately ending all access.""" name: str = "delete_qurl" - description: str = "Revoke (delete) a QURL by resource ID (e.g. 'r_k8xqp9h2sj9')." + description: str = "Revoke (delete) a qURL by resource ID (e.g. 'r_k8xqp9h2sj9')." client: Any = None def _run( @@ -122,11 +122,11 @@ def _run( run_manager: CallbackManagerForToolRun | None = None, ) -> str: self.client.delete(resource_id) - return f"QURL {resource_id} has been revoked." + return f"qURL {resource_id} has been revoked." class QURLToolkit: - """LangChain toolkit providing all QURL tools. + """LangChain toolkit providing all qURL tools. Usage:: @@ -147,7 +147,7 @@ def __init__(self, client: QURLClient) -> None: self.client = client def get_tools(self) -> list[BaseTool]: - """Return all QURL tools configured with the client.""" + """Return all qURL tools configured with the client.""" return [ CreateQURLTool(client=self.client), ResolveQURLTool(client=self.client), diff --git a/src/layerv_qurl/types.py b/src/layerv_qurl/types.py index 5a5bf7b..062ad2f 100644 --- a/src/layerv_qurl/types.py +++ b/src/layerv_qurl/types.py @@ -1,4 +1,4 @@ -"""Type definitions for the QURL API.""" +"""Type definitions for the qURL API.""" from __future__ import annotations @@ -42,7 +42,7 @@ class AIAgentPolicy: @dataclass class AccessPolicy: - """Access control policy for a QURL.""" + """Access control policy for a qURL.""" ip_allowlist: list[str] | None = None ip_denylist: list[str] | None = None @@ -55,7 +55,7 @@ class AccessPolicy: @dataclass class AccessToken: - """An individual access token within a QURL. + """An individual access token within a qURL. ``status`` uses the wider :data:`TokenStatus` alias — tokens can be ``active``/``consumed``/``expired``/``revoked`` (per ``QurlSummary.status`` @@ -77,7 +77,7 @@ class AccessToken: @dataclass class QURL: - """A QURL resource as returned by the API.""" + """A qURL resource as returned by the API.""" resource_id: str target_url: str @@ -94,11 +94,11 @@ class QURL: @dataclass class CreateOutput: - """Response from creating a QURL. + """Response from creating a qURL. ``resource_id`` identifies the resource container (grouped by target URL). ``qurl_id`` identifies the specific access token created (``q_`` prefix). - Multiple QURLs for the same target URL share one ``resource_id``. + Multiple qURLs for the same target URL share one ``resource_id``. """ resource_id: str @@ -137,7 +137,7 @@ class ResolveOutput: @dataclass class ListOutput: - """Response from listing QURLs.""" + """Response from listing qURLs.""" qurls: list[QURL] = field(default_factory=list) next_cursor: str | None = None @@ -208,7 +208,7 @@ class BatchItemResult: @dataclass class BatchCreateOutput: - """Response from batch creating QURLs.""" + """Response from batch creating qURLs.""" succeeded: int = 0 failed: int = 0 diff --git a/tests/test_client.py b/tests/test_client.py index 1e38638..6f843b8 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,4 +1,4 @@ -"""Tests for the QURL Python client.""" +"""Tests for the qURL Python client.""" from __future__ import annotations diff --git a/tests/test_langchain.py b/tests/test_langchain.py index e1eaf35..7c369bf 100644 --- a/tests/test_langchain.py +++ b/tests/test_langchain.py @@ -122,7 +122,7 @@ def test_list_qurls_tool_empty() -> None: tool = ListQURLsTool(client=client) result = tool._run() - assert result == "No QURLs found." + assert result == "No qURLs found." def test_delete_qurl_tool() -> None: From c3cbd4fb60f8b13bf3cbdb840093df6c2194607f Mon Sep 17 00:00:00 2001 From: Justin Date: Sat, 25 Apr 2026 10:18:15 -0500 Subject: [PATCH 2/2] =?UTF-8?q?docs(brand):=20add=20qURL=E2=84=A2=20tradem?= =?UTF-8?q?ark=20+=20portal=20tagline=20to=20README=20and=20pyproject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ™ on first prominent mention (README opener, pyproject.toml description shown on PyPI) and the portal positioning tagline as a README blockquote. Body prose, docstrings, error messages stay plain. 158 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 4 +++- pyproject.toml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c5c9e7..4913d75 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ [![Python](https://img.shields.io/pypi/pyversions/layerv-qurl)](https://pypi.org/project/layerv-qurl/) [![License](https://img.shields.io/github/license/layervai/qurl-python)](LICENSE) -Python SDK for the [qURL API](https://docs.layerv.ai) — secure, time-limited access links for AI agents. +Python SDK for the [qURL™ API](https://docs.layerv.ai) — secure, time-limited access links for AI agents. + +> **Quantum URL (qURL)** · The internet has a hidden layer. This is how you enter. ## Why qURL? diff --git a/pyproject.toml b/pyproject.toml index 41b1e4f..cf33fe4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "layerv-qurl" version = "0.1.0" -description = "Python SDK for the qURL API - secure, time-limited access links" +description = "Python SDK for the qURL™ API — secure, time-limited access links. Quantum URL is how you enter the hidden layer of the internet." readme = "README.md" license = "MIT" requires-python = ">=3.10"