Monorepo for the GoodData Cloud Python SDK: nine published distributions plus shared test
helpers, managed as one uv workspace on a single shared version number.
- Python —
>=3.10is the published floor; develop on 3.14. Tests run py310–py314. - uv workspace (
uv ~= 0.12) — one lock file at the root covers every package. - ruff — lint and format. Line length 120, Google docstring convention.
- ty — type checking. Not mypy;
make type-checkrunsuv run ty check. - tox + pytest — per-package test matrix. vcrpy records HTTP against a real backend.
- pre-commit — ruff, copyright headers,
uv lock, gitlint.
Every package carries the same version, bumped together by tbump. See MAINTENANCE.md
for the release process and CONTRIBUTING.md for first-time setup.
| Path | Contents |
|---|---|
packages/gooddata-* |
The published packages — each has its own AGENTS.md |
packages/tests-support |
Shared test helpers. Workspace member, not published |
gooddata-api-client/ |
Generated from the OpenAPI spec — never hand-edit |
schemas/ |
OpenAPI specs the client is generated from |
scripts/ |
Client generation, copyright, docs builders, release helpers |
docs/ |
Hugo site for the public documentation |
docker-compose.yaml |
Full local GoodData stack, needed to record VCR cassettes |
Run from the workspace root; every target also exists per package via
make -C packages/<name> <target>.
| Target | Effect |
|---|---|
make dev |
uv sync --all-groups + install pre-commit and commit-msg hooks |
make format / make format-fix |
ruff format, check-only / rewriting |
make lint / make lint-fix |
ruff check, report-only / autofixing |
make type-check |
ty check (alias: make types) |
make test |
tox → pytest across every supported Python version |
make test-ci |
the same suite inside the CI Docker images |
Test runs are controlled by environment variables rather than flags:
TEST_ENVS=py314 make test # one interpreter instead of all five
ADD_ARGS="-k test_catalog" make test # pass args through to pytest
RECREATE_ENVS=1 make test # force tox to rebuild its venvsNever invoke bare python or pip — this is a uv workspace, so use uv run, and prefer
the make targets, which already wrap it.
Run this before reporting a change complete, and again before pushing. Do not predict the result — run the commands and read the output.
| Changed | Validate with |
|---|---|
packages/<name>/** |
the per-package sequence below, once for each package touched |
scripts/** or root config |
make format-fix lint-fix from the workspace root |
scripts/docs/** |
also make test-docs-scripts |
gooddata-api-client/** |
nothing — see the note below |
The generated client is excluded from make lint, make format and pre-commit, and no
ty target covers it. Do not point ruff at it directly to "check" it —
ruff check gooddata-api-client bypasses the exclude and reports around 17k errors in
generated code. There is nothing to fix there; regenerate instead.
make -C packages/<name> format-fix lint-fix type-checkFix the first failure before looking at anything after it; a later step's output is meaningless once an earlier one has failed.
TEST_ENVS=py314 make -C packages/<name> testpy314 is the fastest useful signal. Drop TEST_ENVS to run the full py310–py314 matrix
the way CI does — slow, and worth it only when the change could be version-sensitive.
gooddata-sdk is a dependency of every other package except gooddata-flight-server and
tests-support, and gooddata-flight-server is a dependency of gooddata-flexconnect. A
change to either means validating at least one consumer as well, not just the package you
edited.
- The
*-fixtargets rewrite files, and whatever they rewrite has to be committed. CI runs the check-onlymake formatandmake lint, which fail on anything left unformatted. - Never describe a change as validated without having run these commands in this session. If you skipped a step because it was slow, say which one you skipped.
make testis verbose. Capture it and watch a tail:make -C packages/<name> test 2>&1 | tee /tmp/sdk-test.log | tail -40. On failure read the log — do not re-run the command to see more output.- A test failing on a recorded HTTP response is a cassette to re-record, never an assertion to loosen. See the cassette section above.
- Do not add a
# type: ignore, a ruffnoqa, or an entry to atyallowed-unresolved list to get a step to pass. Fix the cause, or say why you cannot.
CI runs make format, make lint, make type-check, make test-ci and
make test-docs-scripts — the check-only variants are what gate the merge. Nothing in CI
validates commit messages; that is local only, via the commit-msg hook.
Most tests replay recorded HTTP traffic through vcrpy rather than calling a live backend.
Cassettes live in packages/*/tests/**/fixtures/*.yaml and are by far the highest-churn
files in the repository — treat re-recording as a routine, deliberate step, not an accident.
Recording requires the local stack from docker-compose.yaml:
aws ecr get-login-password | docker login --username AWS --password-stdin \
020413372491.dkr.ecr.us-east-1.amazonaws.com
mkdir -p build && echo "<license-key>" > build/license # auth-service reads this
docker compose up -d
docker compose wait layout-uploader # blocks until bootstrap finishes
make remove-cassettes # or delete just the ones you need
make testdocker compose wait returns when the one-shot bootstrap container exits, and its exit code
is the bootstrap's. Do not substitute docker compose logs -f to watch for the "Layout upload
completed successfully!" line — logs -f follows indefinitely and never returns, so it hangs
rather than proceeding to the next step.
The API is then at http://localhost:3000, admin token YWRtaW46Ym9vdHN0cmFwOmFkbWluMTIz.
gooddata-fdw tests need one extra service: docker compose --profile fdw up -d.
The data-loader runs with --no-schema-versioning on purpose. Without it, schema and
fixture names get hash suffixes and the recorded cassettes stop being reproducible.
When a backend change alters a response, re-record the cassette — never loosen the
assertion or bypass VCR to make a test pass against a live host. A cassette is the
snapshot that lets everyone else run the suite offline; skipping it moves the failure
onto the next person. Full fixture inventory, including the layouts that some catalog
tests replace rather than read, is documented at the end of CONTRIBUTING.md.
GD_TEST_ENV=staging points the suite at a shared deployment instead of docker-compose,
which is how cassettes and the generated client get refreshed against a real backend.
Locally these need STAGING_ADMIN_TOKEN and STAGING_DS_PASSWORD, read from a gitignored
.env at the root or passed on the command line:
make clean-staging # drop the previous run's data
make load-staging # upload the demo layout
make test-staging # TEST_ENVS= and ADD_ARGS= work here tooOn a PR, the same run is triggered by the test-staging label or a /test-staging
comment. Only one staging run executes at a time and it does not cancel in progress.
The CI workflow currently exports only TOKEN, not the two names the Makefile requires, so
a triggered run fails at its first step; recent runs have been skipped rather than executed,
which is why nobody has hit it. CONTRIBUTING.md has the details. Until that is fixed,
staging runs are effectively local-only.
Extend the existing tests when a change is incremental — a new field, enum value or property belongs in the fixture and assertions that already cover that round trip. A new test file is for genuinely new behavior. Cover null/empty inputs, boundary values and the error paths, not only the happy one.
gooddata-api-client/ is generated by the OpenAPI generator and must never be hand-edited;
the next regeneration would silently discard the change. To refresh it:
make api-client # download specs from localhost:3000, then generate
make api-client STAGING=1 # download from staging instead
make api-client-local # regenerate from the specs already in schemas/Regeneration usually needs a follow-up commit adapting gooddata-sdk to the new surface.
Generator config and custom templates are under .openapi-generator/; that directory's
README covers version upgrades and per-client invocation.
<type>(<scope>): <subject>
<body>
jira: <TICKET-ID>
risk: nonprod|low|high
- type — one of
feat,fix,chore,docs,style,refactor,perf,test,revert,ci,build. - scope — optional, but when given it must come from the allowed list in
.gitlint: a package name, orci/docs/repofor changes that span the workspace. Add new packages to that list when they appear. - subject — imperative mood, no trailing period, whole line at most 70 characters.
- body — required, and it is where the rationale for the change belongs, never in a source comment. Wrap at 72 characters.
- jira — required, lowercase. A ticket id, or
jira: trivialwhen none applies. - risk — lowercase.
nonprodfor tests/docs/CI-only,lowfor a routine change to shipped code,highfor breaking or otherwise risky behavior changes.
Keep jira: and risk: on consecutive lines in the message's last paragraph. A blank line
above a trailing Co-Authored-By: orphans them from the trailer block, which is why the
hook rejects that shape.
gitlint runs at the commit-msg stage, installed by make dev. It checks the type, the
scope against its allowed list, that a body is present, and both trailers. It does not
check subject length — that one is convention, not enforcement.
One logical change per commit. Address review feedback inside the commit whose scope it
belongs to — git commit --fixup=<sha> then git rebase -i --autosquash <sha>^ — rather
than appending a trailing fix commit.
Title follows the commit subject rules. The body needs a summary, a test plan, and the same risk assessment as the commit.
- Search before inventing. Similar code almost certainly exists — do not design a second way to do something the repo already does.
- Do not generate, rename or refactor anything that was not asked for.
- Never create summary or documentation markdown files unless they were requested.
- Break large tasks into independently reviewable units.
- Absolute imports only.
from gooddata_sdk.client import GoodDataSdk, neverfrom .client import ...— relative imports break IDE navigation. - All imports at the top of the file, after the docstring and any
from __future__ import annotations. ruff'sPLC0415enforces this. - Annotate every function and any local whose type is not obvious, especially empty collection initializers. Type dataclasses fully.
- Treat YAML/JSON loader output as
Any: guard withisinstance, narrow once, reuse the narrowed value. Prefer aTypedDictor dataclass when the schema is known. - Prefer one authoritative
cast(...)over repeated inline casts, andtyping.assert_neverfor exhaustive branches. - Google-style docstrings on public APIs.
- Aim to be
ty-clean on the first pass rather than writing then fixing.
A comment explains what the code means to someone who never saw the diff. Rationale for a change goes in the commit body or PR description.
- Do not narrate the change.
used to,no longer,previously,before this change,regression, and pasted benchmark numbers all rot the moment the code moves on, andgit blamealready answers the question better. - Do not put a JIRA key, ticket URL or PR number in a comment, docstring or test name. The
one exception is a forward-looking
# TODO(ABC-1234):for work still outstanding. - Do write down a live constraint the next editor must not break.
- Internal packages first —
gooddata-sdkfor platform access,gooddata-api-clientfor raw endpoints,tests-supportfor shared test helpers. - Then the already-approved stack: pandas, attrs/cattrs, requests, pydantic, pyarrow, pytest, vcrpy — at the versions the lock file resolves.
- Anything new or unfamiliar: search the web for its current API before proposing it. Your training data is likely stale, and this repo pins narrow ranges.
Internal cross-package dependencies use ~={current_version}, which tbump rewrites on
release. Add a dependency to the package's pyproject.toml, then run uv lock.
Three entries in the root pyproject.toml — [project].dependencies,
[tool.uv.sources], and [tool.uv.workspace].members (listed explicitly, not globbed, so
a stray directory cannot break every uv command). Then add it to COMPONENTS in both
.github/workflows/dev-release.yaml and build-release.yaml, to the Codecov file list in
rw-python-tests.yaml, and to scopes in .gitlint.
The public site is Hugo, under docs/. make new-docs serves it locally. Method pages are
generated from docstrings via the {{< python "..." >}} shortcode, so a new public method
needs its own .md page plus a link in the sibling _index.md. CONTRIBUTING.md has the
worked example.
Each package's AGENTS.md states what it owns and, importantly, what it does not.
packages/gooddata-sdk— core SDK,gdcCLI, Analytics-as-Code, declarative layoutspackages/gooddata-pandas— Series and DataFrame access on top of the SDKpackages/gooddata-dbt— dbt metadata to GoodData semantic modelpackages/gooddata-pipelines— provisioning, backup/restore, LDM extension workflowspackages/gooddata-flight-server— reusable Arrow Flight RPC server runtimepackages/gooddata-flexconnect— custom data source framework on the Flight serverpackages/gooddata-fdw— PostgreSQL foreign data wrapper, built on multicornpackages/gooddata-eval—gd-evalCLI for evaluating the GoodData AI agentpackages/tests-support— shared VCR, comparison and file helpers for the test suites