Skip to content
Open
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
38 changes: 38 additions & 0 deletions .github/workflows/cicd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,44 @@ jobs:
# This is associated with the "sdk-test-prod" user, credentials on 1password
GROUNDLIGHT_API_TOKEN: ${{ secrets.GROUNDLIGHT_API_TOKEN_PROD }}

# Check that the committed `generated/` tree is what `make generate` produces. This gets its own
# job because it is the only test that needs node and java (for openapi-generator-cli) and the
# only one that needs no API token. REQUIRE_OPENAPI_GENERATOR makes the openapi-generator half
# fail rather than skip, so a missing toolchain here can't quietly turn the check into a no-op --
# this is the one place it reliably runs.
test-codegen:
runs-on: ubuntu-latest
steps:
- name: get code
uses: actions/checkout@v4
- name: install python
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: install poetry
uses: snok/install-poetry@v1
with:
version: ${{ env.POETRY_VERSION }}
- name: Setup npm
uses: actions/setup-node@v3
with:
# Not 21 like the docs jobs: openapi-generator-cli 2.x require()s proxy-agent, which is
# ESM-only, so it dies with ERR_REQUIRE_ESM on any node without require(esm) support.
# Verified 2026-08-04: 18.19.1 and 21.7.3 fail, 20.19.0 and 22.14.0 work.
node-version: 22
cache: npm
- name: install java (openapi-generator-cli runs a jar)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"
- name: install dependencies and the SDK generator
run: make install-generator
- name: check generated/ is up to date
env:
REQUIRE_OPENAPI_GENERATOR: "1"
run: make test-codegen

# Check that the docs build. (No broken links, etc.)
test-docs:
runs-on: ubuntu-latest
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ cython_debug/

node_modules/

# Scratch trees written by test_codegen/ while re-running the SDK generators. They live inside
# the repo because datamodel-codegen and black find our black settings by walking up from the
# files they write; the tests clean them up unless a run is killed part-way.
codegen-scratch-*/

*.swp
**/.python-version

Expand Down
16 changes: 15 additions & 1 deletion DEVELOPING.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ it will run them, add the changes, and then ask you to try committing again with
The auto-generated SDK code is in the [generated/](generated) directory. Most of the time, you won't
need to generate code. But if the API specification changes, you may need to generate SDK code. To
re-generate the client code, you'll need to [install npm](https://github.com/nvm-sh/nvm#intro)
first. Then you can install the code generator by running:
first, plus a Java runtime (openapi-generator-cli runs a jar). Use node 20.19+ or 22+ — the
generator CLI fails with `ERR_REQUIRE_ESM` on older node, and on node 21. Then you can install the
code generator by running:

```shell
make install-generator
Expand All @@ -67,6 +69,18 @@ Then you can generate the code by running:
make generate
```

Never hand-edit anything under `generated/` — always change the spec (or the generator
invocation) and re-run `make generate`. `make test` enforces this with the checks in
[test_codegen/](test_codegen), which re-run the generators and diff the result against what is
committed; you can run just those checks, without an API token, with:

```shell
make test-codegen
```

The openapi-generator half of that check needs node and java, so it skips (with a reason) if you
have not run `make install-generator`. It always runs in CI, in the `test-codegen` job.

### Linters

Linters help us find issues before runtime. We're currently using:
Expand Down
13 changes: 10 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: apidocs docs-comprehensive generate html install install-dev install-extras install-generator install-lint install-pre-commit test test-4edge test-integ test-local help
.PHONY: apidocs docs-comprehensive generate html install install-dev install-extras install-generator install-lint install-pre-commit test test-4edge test-codegen test-integ test-local help

help: ## Print all targets with their descriptions
@grep -E '^[a-zA-Z_-]+:.*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {if (NF == 1) {printf "\033[36m%-30s\033[0m %s\n", $$1, ""} else {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}}'
Expand Down Expand Up @@ -46,7 +46,14 @@ PROFILING_ARGS = \
--durations 25 \
--durations-min 0.1

test: install ## Run tests against the prod API (needs GROUNDLIGHT_API_TOKEN)
# Checks that `generated/` is what `make generate` would produce. Kept out of `test/` (and given
# its own target) because everything under `test/` needs a GROUNDLIGHT_API_TOKEN to even collect,
# and this needs no API access. TEST_ARGS is deliberately not passed through: a filter meant for
# the real suite would match nothing here and fail the run.
test-codegen: install ## Check that generated/ matches what `make generate` produces (no API token needed)
${PYTEST} test_codegen

test: install test-codegen ## Run tests against the prod API (needs GROUNDLIGHT_API_TOKEN)
${PYTEST} ${PROFILING_ARGS} ${TEST_ARGS} ${CLOUD_FILTERS} test

test-4edge: install ## Run tests against the prod API via the edge-endpoint (needs GROUNDLIGHT_API_TOKEN)
Expand All @@ -68,7 +75,7 @@ test-docs-integ: install-extras ## Run the example code and tests in our docs a
GROUNDLIGHT_ENDPOINT="https://api.integ.groundlight.ai/" ${PYTEST} --markdown-docs ${TEST_ARGS} docs README.md

# Adjust which paths we lint
LINT_PATHS="src test bin samples"
LINT_PATHS="src test test_codegen bin samples"

lint: install-lint ## Run linter to check formatting and style
./code-quality/lint ${LINT_PATHS}
Expand Down
4 changes: 3 additions & 1 deletion generated/groundlight_openapi_client/model/api_token.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def _from_openapi_data(cls, name, raw_key_snippet, created_at, last_used_at, *ar
name (str): An nickname for the API token. This name must be unique for this user.
raw_key_snippet (str): Since we're storing hashed keys, it can be useful to see the raw prefix snippet of the token.
created_at (datetime): When was this token created?
last_used_at (datetime): The most recent time this API token was used. (Helpful for detecting suspicious activity).
last_used_at (datetime, none_type): The most recent time this API token was used. (Helpful for detecting suspicious activity). Null if the token has never been used.

Keyword Args:
_check_type (bool): if True, values for parameters in openapi_types
Expand Down Expand Up @@ -174,6 +174,7 @@ def _from_openapi_data(cls, name, raw_key_snippet, created_at, last_used_at, *ar
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
expires_at (datetime, none_type): When does this token expire? If Null, the token never expires.. [optional] # noqa: E501
token_ttl (int, none_type): Identity token lifetime policy in whole seconds. Null means tokens minted under this identity never expire (no rotation).. [optional] # noqa: E501
"""

_check_type = kwargs.pop("_check_type", True)
Expand Down Expand Up @@ -265,6 +266,7 @@ def __init__(self, name, *args, **kwargs): # noqa: E501
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
expires_at (datetime, none_type): When does this token expire? If Null, the token never expires.. [optional] # noqa: E501
token_ttl (int, none_type): Identity token lifetime policy in whole seconds. Null means tokens minted under this identity never expire (no rotation).. [optional] # noqa: E501
"""

_check_type = kwargs.pop("_check_type", True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def _from_openapi_data(
name (str): An nickname for the API token. This name must be unique for this user.
raw_key_snippet (str): Since we're storing hashed keys, it can be useful to see the raw prefix snippet of the token.
created_at (datetime): When was this token created?
last_used_at (datetime): The most recent time this API token was used. (Helpful for detecting suspicious activity).
last_used_at (datetime, none_type): The most recent time this API token was used. (Helpful for detecting suspicious activity). Null if the token has never been used.
raw_key (str): The full API token secret. Returned only once, when the token is created.

Keyword Args:
Expand Down Expand Up @@ -180,6 +180,7 @@ def _from_openapi_data(
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
expires_at (datetime, none_type): When does this token expire? If Null, the token never expires.. [optional] # noqa: E501
token_ttl (int, none_type): Identity token lifetime policy in whole seconds. Null means tokens minted under this identity never expire (no rotation).. [optional] # noqa: E501
"""

_check_type = kwargs.pop("_check_type", True)
Expand Down Expand Up @@ -272,6 +273,7 @@ def __init__(self, name, *args, **kwargs): # noqa: E501
through its discriminator because we passed in
_visited_composed_classes = (Animal,)
expires_at (datetime, none_type): When does this token expire? If Null, the token never expires.. [optional] # noqa: E501
token_ttl (int, none_type): Identity token lifetime policy in whole seconds. Null means tokens minted under this identity never expire (no rotation).. [optional] # noqa: E501
"""

_check_type = kwargs.pop("_check_type", True)
Expand Down
22 changes: 17 additions & 5 deletions generated/model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: public-api.yaml
# timestamp: 2026-07-10T01:01:06+00:00
# timestamp: 2026-08-03T21:13:28+00:00

from __future__ import annotations

Expand Down Expand Up @@ -38,14 +38,20 @@ class ApiToken(BaseModel):
created_at: datetime = Field(..., description="When was this token created?")
last_used_at: Optional[datetime] = Field(
...,
description="The most recent time this API token was used. (Helpful for detecting suspicious activity). Null if the token has never been used.",
description=(
"The most recent time this API token was used. (Helpful for detecting suspicious activity). Null if the"
" token has never been used."
),
)
expires_at: Optional[datetime] = Field(
None, description="When does this token expire? If Null, the token never expires."
)
token_ttl: Optional[int] = Field(
None,
description="Identity token lifetime policy in whole seconds. Null means tokens minted under this identity never expire (no rotation).",
description=(
"Identity token lifetime policy in whole seconds. Null means tokens minted under this identity never expire"
" (no rotation)."
),
)


Expand All @@ -64,14 +70,20 @@ class ApiTokenCreateResponse(BaseModel):
created_at: datetime = Field(..., description="When was this token created?")
last_used_at: Optional[datetime] = Field(
...,
description="The most recent time this API token was used. (Helpful for detecting suspicious activity). Null if the token has never been used.",
description=(
"The most recent time this API token was used. (Helpful for detecting suspicious activity). Null if the"
" token has never been used."
),
)
expires_at: Optional[datetime] = Field(
None, description="When does this token expire? If Null, the token never expires."
)
token_ttl: Optional[int] = Field(
None,
description="Identity token lifetime policy in whole seconds. Null means tokens minted under this identity never expire (no rotation).",
description=(
"Identity token lifetime policy in whole seconds. Null means tokens minted under this identity never expire"
" (no rotation)."
),
)
raw_key: str = Field(..., description="The full API token secret. Returned only once, when the token is created.")

Expand Down
Loading