Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 38 additions & 2 deletions .github/workflows/sdk-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ jobs:

- name: Repository contains actual csharp-sdk version
run: |
diff_result=$(git diff --exit-code --name-only example/csharp/aidbox || true)
diff_result=$(git diff --exit-code --name-only examples/csharp/generated || true)

if [ -z "$diff_result" ]; then
echo "✅ Generated SDK is identical to the one stored in repository."
else
echo "❌ Generated SDK differs from the one stored in repository."
echo "Differences:"
git diff example/csharp/aidbox
git diff examples/csharp/generated
exit 1
fi

Expand Down Expand Up @@ -107,3 +107,39 @@ jobs:

- name: Run tests
run: make test-typescript-ccda-example

test-python-sdk-test:
runs-on: ubuntu-latest

strategy:
matrix:
bun-version: [ latest ]

steps:
- uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: ${{ matrix.bun-version }}

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Test Python SDK
run: |
export AIDBOX_LICENSE_ID="${{ secrets.AIDBOX_LICENSE_ID }}"
make test-python-sdk

- name: Repository contains actual python-sdk version
run: |
diff_result=$(git diff --exit-code --name-only examples/python/generated || true)

if [ -z "$diff_result" ]; then
echo "✅ Generated SDK is identical to the one stored in repository."
else
echo "❌ Generated SDK differs from the one stored in repository."
echo "Differences:"
git diff examples/python/generated
exit 1
fi
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ export function mainFunction() {
// Implementation
}

// 5. Helper functions
// 5. PythonHelper functions
function helperFunction() {
// Implementation
}
Expand Down
18 changes: 18 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,21 @@ test-csharp-sdk: typecheck format prepare-aidbox-runme lint
cd examples/csharp && dotnet restore
cd examples/csharp && dotnet build
cd examples/csharp && dotnet test

PYTHON=python3
PYTHON_SDK_EXAMPLE=./examples/python

test-python-sdk: typecheck format prepare-aidbox-runme lint
$(TYPECHECK) --project tsconfig.example-python.json
bun run examples/python/generate.ts

@if [ ! -d "$(PYTHON_SDK_EXAMPLE)/venv" ]; then \
cd $(PYTHON_SDK_EXAMPLE) && \
$(PYTHON) -m venv venv && \
. venv/bin/activate && \
pip install -r generated/requirements.txt; \
fi

cd $(PYTHON_SDK_EXAMPLE) && \
. venv/bin/activate && \
python -m pytest test_sdk.py -v
4 changes: 2 additions & 2 deletions docs/configuration/typescript-generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ export default defineConfig({

- **Build Time**: Value set generation adds minimal overhead to build time
- **Output Size**: Generated files are small and compress well
- **Runtime Impact**: Helper functions are lightweight and optional
- **Runtime Impact**: PythonHelper functions are lightweight and optional
- **Tree Shaking**: Import only the value sets you need

## Security Considerations

- Value set directory path cannot contain `..` path segments
- Generated files use proper TypeScript const assertions
- Helper functions include type guards for runtime safety
- PythonHelper functions include type guards for runtime safety
2 changes: 1 addition & 1 deletion docs/features/value-set-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const patient: Patient = {
// patient.gender = 'invalid'; // Error: Type '"invalid"' is not assignable
```

### With Helper Functions
### With PythonHelper Functions
```typescript
import { isValidAdministrativeGender } from './generated/valuesets/AdministrativeGender.js';

Expand Down
106 changes: 106 additions & 0 deletions examples/python/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import base64
import json
from typing import TypeVar, Type, Dict, Any
import requests
from pydantic import BaseModel
from generated.hl7_fhir_r4_core import DomainResource, Bundle


T = TypeVar("T", bound=DomainResource)


class AuthCredentials(BaseModel):
username: str
password: str


class Auth(BaseModel):
method: str
credentials: AuthCredentials


def to_camel_case(snake_str: str) -> str:
"""Convert snake_case to camelCase"""
components = snake_str.split("_")
return components[0] + "".join(x.title() for x in components[1:])


class Client:
def __init__(
self,
base_url: str,
auth: Auth | None = None,
):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
if auth:
if auth.method == "basic":
self._set_basic_auth(
auth.credentials.username, auth.credentials.password
)
else:
raise ValueError(f"Unsupported auth method: {auth.method}")

def _set_basic_auth(self, username: str, password: str) -> None:
"""Set basic authentication headers"""
credentials = f"{username}:{password}"
encoded = base64.b64encode(credentials.encode()).decode()
self.session.headers.update({"Authorization": f"Basic {encoded}"})

def _get_resource_type(self, resource: DomainResource) -> str:
"""Get the resource type from the class name"""
return resource.__class__.__name__

def create(self, resource: T) -> T:
"""Create a new resource"""
resource_type = self._get_resource_type(resource)
url = f"{self.base_url}/{resource_type}"
data = resource.model_dump(exclude_unset=True, exclude_none=True)
response = self.session.post(url, json=data)
response.raise_for_status()
data = response.json()
if not data.get("id"):
raise ValueError("Response missing required 'id' field")
return resource.__class__.model_validate(data)

def read(self, resource_class: Type[T], resource_id: str) -> T:
"""Read a resource by ID"""
resource_type = resource_class.__name__
url = f"{self.base_url}/{resource_type}/{resource_id}"
response = self.session.get(url)
response.raise_for_status()
data = response.json()
if not data.get("id"):
raise ValueError("Response missing required 'id' field")
return resource_class.model_validate(data)

def update(self, resource: T) -> T:
"""Update an existing resource"""
resource_type = self._get_resource_type(resource)
if not hasattr(resource, "id") or not resource.id:
raise ValueError("Resource must have an ID for update")

url = f"{self.base_url}/{resource_type}/{resource.id}"
data = resource.model_dump(exclude_unset=True, exclude_none=True)
response = self.session.put(url, json=data)
response.raise_for_status()
data = response.json()
if not data.get("id"):
raise ValueError("Response missing required 'id' field")
return resource.__class__.model_validate(data)

def delete(self, resource_type: str, resource_id: str) -> None:
"""Delete a resource"""
url = f"{self.base_url}/{resource_type}/{resource_id}"
response = self.session.delete(url)
response.raise_for_status()

def search(
self, resource_class: Type[T], params: Dict[str, Any] | None = None
) -> Bundle:
"""Search for resources"""
resource_type = resource_class.__name__
url = f"{self.base_url}/{resource_type}"
response = self.session.get(url, params=params)
response.raise_for_status()
return Bundle.model_validate(response.json())
24 changes: 24 additions & 0 deletions examples/python/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { APIBuilder } from "../../src";

if (require.main === module) {
console.log("📦 Generating FHIR R4 Core Types...");

const builder = new APIBuilder()
.verbose()
.throwException()
.fromPackage("hl7.fhir.r4.core", "4.0.1")
.python("./src/api/writer-generator/python/static-files")
.outputTo("./examples/python/generated")
.cleanOutput(true);

const report = await builder.generate();

console.log(report);

if (report.success) {
console.log("✅ FHIR R4 types generated successfully!");
} else {
console.error("❌ FHIR R4 types generation failed.");
process.exit(1);
}
}
Loading
Loading