Skip to content

Commit f053d3a

Browse files
authored
Add UUID support for organization IDs and statement IDs in APIConnection and related models (#10)
* Add UUID support for organization IDs and statement IDs in APIConnection and related models - Introduced UUID type for organization_id in APIConnection, ResultSetContext, and related models. - Updated handling of organization_id to accept both string and UUID formats, with validation for string UUIDs. - Modified StatementHandler and other components to ensure proper conversion between UUID and string representations. - Added tests to verify UUID handling and conversion in APIConnection and StatementRequest. - Created a new script for generating OpenAPI v2 client code from YAML specification. * Add Makefile for build and CI management - Introduced a Makefile to streamline common development tasks including installation, linting, formatting, type checking, testing, and building the package. - Updated GitHub Actions workflow to utilize the Makefile for running CI checks, enhancing consistency in the build process. - Added a target for running Jupyter Lab within the Makefile for improved development experience. * Update CONTRIBUTING.md to reflect Makefile usage for dependency management and testing - Changed instructions from `uv` commands to `make` commands for installing dependencies, running tests, linting, formatting, and type checking. - Added additional Makefile targets for CI checks, unit tests, and build processes to streamline development workflows.
1 parent d1aa962 commit f053d3a

20 files changed

+643
-103
lines changed

.github/workflows/python.yml

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@ jobs:
1616
uses: actions/setup-python@v4
1717
with:
1818
python-version: ${{ matrix.python-version }}
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v4
21+
with:
22+
version: "latest"
1923
- name: Install dependencies
2024
run: |
21-
python -m pip install --upgrade pip
22-
pip install uv
23-
uv sync --group dev
24-
- name: Test with pytest
25-
run: |
26-
uv run pytest
27-
- name: Test build
25+
uv sync --all-groups
26+
- name: Run CI checks
2827
run: |
29-
uv build
28+
make ci

CONTRIBUTING.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ Thank you for contributing to the DeltaStream Python client.
1111
- NPM: `npx @openapitools/openapi-generator-cli`
1212
- Docker image: `openapitools/openapi-generator-cli`
1313

14-
Sync dependencies:
14+
Install dependencies:
1515

1616
```bash
17-
uv sync
17+
make install
1818
```
1919

2020
#### Regenerating the controlplane OpenAPI client (apiv2)
@@ -36,10 +36,18 @@ Notes:
3636
#### Tests / Lint / Types
3737

3838
```bash
39-
uv run pytest
40-
uv run ruff check --fix
41-
uv run ruff format
42-
uv run mypy
39+
make test # Run all tests
40+
make lint # Run linting checks
41+
make format # Format code
42+
make mypy # Run type checking
43+
make ci # Run all CI checks (lint, format, mypy, unit-tests, build)
4344
```
4445

45-
46+
For additional options:
47+
```bash
48+
make unit-tests # Run unit tests only (exclude integration tests)
49+
make check-format # Check if code formatting is correct
50+
make build # Build the package
51+
make clean # Clean build artifacts
52+
make help # Show all available targets
53+
```

Makefile

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
.PHONY: help install lint format check-format mypy test unit-tests build ci clean jupyter
2+
3+
# Default target
4+
help:
5+
@echo "Available targets:"
6+
@echo " install Install all dependencies with uv"
7+
@echo " lint Run ruff linting checks"
8+
@echo " format Format code with ruff"
9+
@echo " check-format Check if code formatting is correct"
10+
@echo " mypy Run mypy type checking"
11+
@echo " test Run all tests with pytest"
12+
@echo " unit-tests Run unit tests only (exclude integration tests)"
13+
@echo " build Build the package"
14+
@echo " ci Run all CI checks (lint, format, mypy, unit-tests, build)"
15+
@echo " clean Clean build artifacts"
16+
@echo " jupyter Run Jupyter Lab"
17+
18+
# Install dependencies
19+
install:
20+
uv sync --all-groups
21+
22+
# Linting
23+
lint:
24+
uv run ruff check
25+
26+
# Format code
27+
format:
28+
uv run ruff format
29+
30+
# Check formatting
31+
check-format:
32+
uv run ruff format --check
33+
34+
# Type checking
35+
mypy:
36+
uv run mypy
37+
38+
# Unit tests
39+
test:
40+
uv run pytest
41+
42+
# Unit tests only (exclude integration tests)
43+
unit-tests:
44+
uv run pytest -m "not integration"
45+
46+
# Build package
47+
build:
48+
uv build
49+
50+
# Run all CI checks
51+
ci: lint check-format mypy unit-tests build
52+
@echo "All CI checks passed!"
53+
54+
# Clean build artifacts
55+
clean:
56+
rm -rf dist/
57+
rm -rf build/
58+
rm -rf *.egg-info/
59+
find . -type d -name __pycache__ -exec rm -rf {} +
60+
find . -type f -name "*.pyc" -delete
61+
62+
jupyter:
63+
uv run --with jupyter jupyter lab

mypy.ini

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,16 @@ no_implicit_reexport = true
5656
warn_return_any = true
5757

5858
# Configuration for specific modules
59-
[mypy-src.api.controlplane.openapi_client.*]
59+
[mypy-src.deltastream.api.controlplane.openapi_client.*]
6060
ignore_errors = true
6161

62-
[mypy-api.controlplane.openapi_client.*]
62+
[mypy-deltastream.api.controlplane.openapi_client.*]
6363
ignore_errors = true
6464

65-
[mypy-src.api.dataplane.openapi_client.*]
65+
[mypy-src.deltastream.api.dataplane.openapi_client.*]
6666
ignore_errors = true
6767

68-
[mypy-api.dataplane.openapi_client.*]
68+
[mypy-deltastream.api.dataplane.openapi_client.*]
6969
ignore_errors = true
7070

7171
[mypy-websockets.*]

scripts/generate_apiv2.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from __future__ import annotations
2+
3+
import os
4+
import shutil
5+
import subprocess
6+
import sys
7+
from pathlib import Path
8+
from typing import List
9+
10+
11+
def _find_openapi_generator() -> List[str]:
12+
"""Return the base command to run OpenAPI Generator.
13+
14+
Prefers the `openapi-generator` binary (Homebrew install),
15+
falls back to `openapi-generator-cli` if available.
16+
"""
17+
candidates = ["openapi-generator", "openapi-generator-cli"]
18+
for candidate in candidates:
19+
if shutil.which(candidate):
20+
return [candidate]
21+
22+
# Optional: support docker if available and the image exists locally
23+
if shutil.which("docker"):
24+
return [
25+
"docker",
26+
"run",
27+
"--rm",
28+
"-u",
29+
f"{os.getuid()}:{os.getgid()}",
30+
"-v",
31+
f"{Path.cwd()}:{Path.cwd()}",
32+
"-w",
33+
f"{Path.cwd()}",
34+
"openapitools/openapi-generator-cli",
35+
]
36+
37+
raise FileNotFoundError(
38+
"OpenAPI Generator CLI not found. Install with `brew install openapi-generator` "
39+
"or use `npx @openapitools/openapi-generator-cli`, or ensure Docker image "
40+
"openapitools/openapi-generator-cli is available."
41+
)
42+
43+
44+
def main() -> int:
45+
"""Generate the controlplane OpenAPI Python client in-place.
46+
47+
This regenerates `deltastream.api.controlplane.openapi_client` from
48+
`src/deltastream/api/controlplane/api-server-v2.yaml`.
49+
"""
50+
repo_root = Path(__file__).resolve().parent.parent
51+
src_dir = repo_root / "src"
52+
spec_path = src_dir / "deltastream/api/controlplane/api-server-v2.yaml"
53+
54+
if not spec_path.exists():
55+
print(f"Spec file not found: {spec_path}", file=sys.stderr)
56+
return 1
57+
58+
base_cmd = _find_openapi_generator()
59+
60+
# Use python generator with pydantic v2 and lazy imports
61+
cmd = base_cmd + [
62+
"generate",
63+
"-i",
64+
str(spec_path),
65+
"-g",
66+
"python",
67+
"-o",
68+
str(src_dir), # generate inside src so package path is respected
69+
"--skip-validate-spec",
70+
"--additional-properties",
71+
(
72+
"generateSourceCodeOnly=true,"
73+
"lazyImport=true,"
74+
"enumClassPrefix=true,"
75+
"modelPropertyNaming=original,"
76+
"packageName=deltastream.api.controlplane.openapi_client"
77+
),
78+
]
79+
80+
# Ensure output dir exists
81+
(src_dir / "deltastream").mkdir(parents=True, exist_ok=True)
82+
83+
print("Running:", " ".join(cmd))
84+
try:
85+
subprocess.run(cmd, check=True)
86+
except subprocess.CalledProcessError as exc:
87+
print(
88+
f"OpenAPI Generator failed with exit code {exc.returncode}", file=sys.stderr
89+
)
90+
return exc.returncode
91+
92+
print("OpenAPI client regenerated at: deltastream/api/controlplane/openapi_client")
93+
return 0
94+
95+
96+
if __name__ == "__main__":
97+
raise SystemExit(main())

src/deltastream/api/conn.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from .handlers import StatementHandler, map_error_response
2424
from deltastream.api.controlplane.openapi_client.configuration import Configuration
25+
from uuid import UUID
2526

2627

2728
class APIConnection:
@@ -31,7 +32,7 @@ def __init__(
3132
token_provider: Callable[[], Awaitable[str]],
3233
session_id: Optional[str],
3334
timezone: str,
34-
organization_id: Optional[str],
35+
organization_id: Optional[Union[str, UUID]],
3536
role_name: Optional[str],
3637
database_name: Optional[str],
3738
schema_name: Optional[str],
@@ -42,8 +43,24 @@ def __init__(
4243
self.server_url = server_url
4344
self.session_id = session_id
4445
self.timezone = timezone
46+
# Convert to UUID if provided and valid
47+
org_uuid = None
48+
if organization_id is not None:
49+
if isinstance(organization_id, UUID):
50+
org_uuid = organization_id
51+
elif isinstance(organization_id, str):
52+
try:
53+
org_uuid = UUID(organization_id)
54+
except ValueError as e:
55+
raise ValueError(
56+
f"Invalid organization_id: '{organization_id}' is not a valid UUID string"
57+
) from e
58+
else:
59+
raise TypeError(
60+
f"organization_id must be a string or UUID, got {type(organization_id)}"
61+
)
4562
self.rsctx = ResultSetContext(
46-
organization_id=organization_id,
63+
organization_id=org_uuid,
4764
role_name=role_name,
4865
database_name=database_name,
4966
schema_name=schema_name,
@@ -145,11 +162,13 @@ async def query(self, query: str, attachments: Optional[List[Blob]] = None) -> R
145162
)
146163

147164
if dp_req.request_type == "result-set":
148-
dp_rs = await dpconn.get_statement_status(dp_req.statement_id, 0)
165+
dp_rs = await dpconn.get_statement_status(
166+
UUID(dp_req.statement_id), 0
167+
)
149168
cp_rs = self._dataplane_to_controlplane_resultset(dp_rs)
150169

151170
async def cp_get_statement_status(
152-
statement_id: str, partition_id: int
171+
statement_id: UUID, partition_id: int
153172
) -> CPResultSet:
154173
dp_result = await dpconn.get_statement_status(
155174
statement_id, partition_id
@@ -169,7 +188,7 @@ async def cp_get_statement_status(
169188
cp_rs = self._dataplane_to_controlplane_resultset(rs)
170189

171190
async def cp_get_statement_status(
172-
statement_id: str, partition_id: int
191+
statement_id: UUID, partition_id: int
173192
) -> CPResultSet:
174193
result = await self.statement_handler.get_statement_status(
175194
statement_id, partition_id
@@ -306,7 +325,7 @@ def _dataplane_to_controlplane_resultset(dp_rs) -> CPResultSet:
306325
cp_meta = CPResultSetMetadata(
307326
encoding=getattr(meta, "encoding", ""),
308327
partitionInfo=getattr(meta, "partition_info", None),
309-
columns=getattr(meta, "columns", None),
328+
columns=getattr(meta, "columns", []),
310329
dataplaneRequest=getattr(meta, "dataplane_request", None),
311330
context=getattr(meta, "context", None),
312331
)
@@ -330,7 +349,7 @@ async def submit_statement(
330349
raise
331350

332351
async def get_statement_status(
333-
self, statement_id: str, partition_id: int
352+
self, statement_id: UUID, partition_id: int
334353
) -> CPResultSet:
335354
try:
336355
await self._set_auth_header()

src/deltastream/api/dpconn.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Optional
22
from urllib.parse import urlparse, parse_qs
33
import asyncio
4+
from uuid import UUID
45

56
from deltastream.api.dataplane.openapi_client import (
67
Configuration,
@@ -34,14 +35,14 @@ def __init__(
3435

3536
self.api = DataplaneApi(config)
3637

37-
def _get_statement_status_api(self, statement_id: str, partition_id: int):
38+
def _get_statement_status_api(self, statement_id: UUID, partition_id: int):
3839
resp = self.api.get_statement_status(
3940
statement_id=statement_id, partition_id=partition_id
4041
)
4142
return resp
4243

4344
async def get_statement_status(
44-
self, statement_id: str, partition_id: int
45+
self, statement_id: UUID, partition_id: int
4546
) -> ResultSet:
4647
try:
4748
resp = self._get_statement_status_api(statement_id, partition_id)
@@ -65,7 +66,11 @@ async def get_statement_status(
6566
statement_status.statement_id, partition_id
6667
)
6768
else:
68-
raise SQLError("Invalid statement status", "", "")
69+
raise SQLError(
70+
"Invalid statement status",
71+
"",
72+
UUID("00000000-0000-0000-0000-000000000000"),
73+
)
6974
else:
7075
result_set = resp.body
7176
if result_set.sql_state == SqlState.SQL_STATE_SUCCESSFUL_COMPLETION:
@@ -79,7 +84,7 @@ async def get_statement_status(
7984
except Exception as exc:
8085
raise RuntimeError(str(exc))
8186

82-
async def wait_for_completion(self, statement_id: str) -> ResultSet:
87+
async def wait_for_completion(self, statement_id: UUID) -> ResultSet:
8388
result_set = await self.get_statement_status(statement_id, 0)
8489
if result_set.sql_state == SqlState.SQL_STATE_SUCCESSFUL_COMPLETION:
8590
return result_set

src/deltastream/api/error.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from enum import Enum
2+
from uuid import UUID
23

34

45
class InterfaceError(Exception):
@@ -32,7 +33,7 @@ def __init__(self, message: str):
3233

3334

3435
class SQLError(Exception):
35-
def __init__(self, message: str, code: str, statement_id: str):
36+
def __init__(self, message: str, code: str, statement_id: UUID):
3637
super().__init__(message)
3738
self.name = "SQLError"
3839
try:

0 commit comments

Comments
 (0)