Skip to content

perf: cut gooddata_sdk import cost by importing only what the SDK uses - #1815

Merged
hkad98 merged 1 commit into
gooddata:masterfrom
hkad98:jkd/lazy-sdk-imports
Sep 16, 2026
Merged

hkad98 merged 1 commit into
gooddata:masterfrom
hkad98:jkd/lazy-sdk-imports

Conversation

@hkad98

@hkad98 hkad98 commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

import gooddata_sdk pulls in 2228 modules / ~0.34s on a warm cache. Three layers each import everything available rather than what is needed, so a consumer that wants one class pays the whole bill.

before after
import gooddata_sdk 0.34s / 2228 mods 0.02s / 160 mods
from gooddata_sdk.visualization import Visualization 0.35s / 2228 mods 0.11s / 967 mods
from gooddata_sdk import GoodDataSdk (whole SDK) 0.34s / 2228 mods 0.22s / 1363 mods

Measured on py3.14, warm .pyc, same interpreter.

What was importing too much

1. gooddata_sdk/__init__.py eagerly re-exported all 253 public names. Touching any leaf module ran the entire SDK import graph first. Now resolved lazily via PEP 562 __getattr__ — the pattern scipy and scikit-learn ship — with __all__ for import * and a TYPE_CHECKING block so type checkers, IDEs and the griffe docs builder still resolve every name statically.

2. The SDK imported all 1402 generated models to use 58. Five modules did import gooddata_api_client.models as afm_models; execution.py did from gooddata_api_client import models. That aggregate's own generated header says not to do this ("import only the models that you directly need"), and leaf imports are safe because openapi-generator emits a per-model lazy_import(), so they don't cascade. The 58 now live in gooddata_sdk/_models.py, keeping the afm_models.X spelling at the call sites and leaving one list to maintain.

3. client.py and catalog_service_base.py imported all ~130 generated API classes to instantiate six. from gooddata_api_client import apis transitively pulls in most of the models too — this, not the models aggregate, was the single biggest contributor. The six now come from gooddata_sdk/_apis.py.

Backwards compatibility

from gooddata_sdk import <anything> is unchanged — all 253 exports verified to resolve, and class identity is unchanged (the shims re-export the same objects from the same leaf modules).

The eager __init__ also populated the whole module tree as a side effect, so import gooddata_sdk followed by gooddata_sdk.catalog.workspace used to resolve. Dropping it broke that, so every subpackage gets a lazy submodule __getattr__ built by gooddata_sdk/_lazy.py. It deliberately does not convert a ModuleNotFoundError raised inside an existing submodule into AttributeError, so a genuinely missing dependency still reports itself instead of looking like a typo.

Two intentional differences remain:

  • dir(gooddata_sdk) now lists the public API plus every submodule, where before it listed only those that happened to have been imported.
  • from gooddata_sdk import * no longer leaks the logging module or the submodule names (catalog, utils, …), because __all__ now states the public API explicitly. Star-importing a library is not a supported pattern and this fails loudly with NameError rather than silently.

Verification

  • gooddata-sdk 465 passed / 2 skipped, plus pandas 343, pipelines 183, flexconnect 83, fdw 43, flight-server 34, dbt 5 — all green.
  • The 6 test_catalog_user_service.py errors on this branch are pre-existing and reproduce identically on unmodified master; they come from a live server returning a DENODO data-source type the checked-in generated client does not know.
  • ruff check/format clean; ty reports the same 5 pre-existing diagnostics as master.
  • The griffe docs JSON is identical apart from the three new underscore-prefixed modules, which python_ref_builder.py filters out.
  • tests/sdk/test_lazy_imports.py guards all three properties. A single convenience import silently undoes the module savings, and a missing lazy hook in a subpackage only surfaces in a fresh interpreter — so the attribute-access cases each run in their own subprocess. (My first pass shared one interpreter and the regression hid behind an earlier import.)

Summary by CodeRabbit

  • Performance

    • SDK components and nested modules now load on demand, improving startup time and reducing unnecessary resource usage.
    • Existing public SDK APIs remain accessible with more efficient module loading.
  • Compatibility

    • Improved consistency when retrieving standard and Arrow-based execution results.
    • Preserved existing filtering, metrics, catalog, and client functionality.
  • Tests

    • Added coverage for lazy loading, public exports, nested module access, error handling, and execution response behavior.

@hkad98
hkad98 requested review from lupko and pcerny as code owners September 16, 2026 12:43
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Essentials

Run ID: 16b57ca5-94cf-45b4-8021-e1978dfe66d1

📥 Commits

Reviewing files that changed from the base of the PR and between 9d14c31 and 8125426.

📒 Files selected for processing (55)
  • packages/gooddata-pandas/pyproject.toml
  • packages/gooddata-pandas/src/gooddata_pandas/dataframe.py
  • packages/gooddata-pandas/tests/dataframe/test_dataframe_for_exec_def_arrow.py
  • packages/gooddata-sdk/src/gooddata_sdk/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/_apis.py
  • packages/gooddata-sdk/src/gooddata_sdk/_lazy.py
  • packages/gooddata-sdk/src/gooddata_sdk/_models.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/ai_lake/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/appearance/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/appearance/entity_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/catalog_service_base.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/action_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/action_model/requests/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/action_model/responses/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/declarative_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/declarative_model/physical_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/entity_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/entity_model/content_objects/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/data_source/validation/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/export/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/common/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/entity_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/organization/layout/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/permission/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/permission/declarative_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/user/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/user/declarative_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/user/entity_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/user/management_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/declarative_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/declarative_model/workspace/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/declarative_model/workspace/analytics_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/declarative_model/workspace/logical_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/declarative_model/workspace/logical_model/dataset/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/declarative_model/workspace/logical_model/dataset_extensions/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/declarative_model/workspace/logical_model/date_dataset/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/content_objects/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/entity_model/graph_objects/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/cli/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/client.py
  • packages/gooddata-sdk/src/gooddata_sdk/compute/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/compute/model/__init__.py
  • packages/gooddata-sdk/src/gooddata_sdk/compute/model/attribute.py
  • packages/gooddata-sdk/src/gooddata_sdk/compute/model/base.py
  • packages/gooddata-sdk/src/gooddata_sdk/compute/model/execution.py
  • packages/gooddata-sdk/src/gooddata_sdk/compute/model/filter.py
  • packages/gooddata-sdk/src/gooddata_sdk/compute/model/metric.py
  • packages/gooddata-sdk/tests/sdk/test_lazy_imports.py
  • scripts/sync_lazy_imports.py

Included review availability: 1 review is currently available. Your included PR review attempts over the past 7 days set your current allowance at 4 reviews per hour.


📝 Walkthrough

Walkthrough

The SDK now loads public exports and nested submodules lazily, with selected API and model re-exports. SDK consumers use these modules instead of aggregate generated namespaces. Pandas execution paths use a direct response import, and tests cover the import behavior.

Changes

SDK lazy import system

Layer / File(s) Summary
Lazy export and re-export core
packages/gooddata-sdk/src/gooddata_sdk/__init__.py, packages/gooddata-sdk/src/gooddata_sdk/_lazy.py, packages/gooddata-sdk/src/gooddata_sdk/_models.py, packages/gooddata-sdk/src/gooddata_sdk/_apis.py, scripts/sync_lazy_imports.py
The SDK resolves public exports and selected generated models and APIs on demand. The synchronization script generates the export tables.
Lazy package submodules
packages/gooddata-sdk/src/gooddata_sdk/catalog/**, packages/gooddata-sdk/src/gooddata_sdk/compute/**, packages/gooddata-sdk/src/gooddata_sdk/cli/__init__.py
Package initializers resolve submodules through submodule_getattr.
SDK consumer import migration
packages/gooddata-sdk/src/gooddata_sdk/client.py, packages/gooddata-sdk/src/gooddata_sdk/catalog/catalog_service_base.py, packages/gooddata-sdk/src/gooddata_sdk/catalog/workspace/content_service.py, packages/gooddata-sdk/src/gooddata_sdk/compute/model/*
SDK consumers use internal API and model re-export modules instead of aggregate generated client namespaces.
Lazy import validation
packages/gooddata-sdk/tests/sdk/test_lazy_imports.py
Tests validate lazy exports, submodule access, import errors, avoided aggregate imports, synchronization, and re-export coverage.

Pandas execution response import

Layer / File(s) Summary
Direct execution response import
packages/gooddata-pandas/src/gooddata_pandas/dataframe.py, packages/gooddata-pandas/tests/dataframe/test_dataframe_for_exec_def_arrow.py, packages/gooddata-pandas/pyproject.toml
Dataframe result paths use a direct AfmExecutionResponse import. The test patches the direct symbol, and ty permits generated leaf-model imports.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Refactor

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant gooddata_sdk
  participant LazyResolver
  participant SDKModule
  Caller->>gooddata_sdk: Access public export or submodule
  gooddata_sdk->>LazyResolver: Resolve missing attribute
  LazyResolver->>SDKModule: Import target module
  SDKModule-->>gooddata_sdk: Provide object or submodule
  gooddata_sdk-->>Caller: Return resolved attribute
Loading

Merge Risk: ⚪ Minimal · up to 81254

The lazy-import migration preserves the reviewed public import and generated-model contracts, with no actionable merge risk identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 50 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: reducing gooddata_sdk import cost through targeted imports.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 50 files. (5 skipped: 1 unsupported, 4 over the file limit.)

  • Fix all pre-merge checks with AI

A rabbit reads each line,
The patch grows clear beneath the moon,
Small changes hop in place,
Tests guard the garden path,
Reviews bloom before the dawn.

Comment @coderabbitai help to get the list of available commands.

@hkad98
hkad98 force-pushed the jkd/lazy-sdk-imports branch from 9d14c31 to 67d13be Compare September 16, 2026 13:44
`import gooddata_sdk` pulled in 2333 modules and ~0.58s on a warm cache, because
three separate layers imported everything available rather than what was needed.
A consumer that wants a single class (e.g. `Visualization`) paid the whole bill.

1. `gooddata_sdk/__init__.py` eagerly re-exported every public name, so touching
   any leaf module ran the entire SDK's import graph first. The re-exports are now
   resolved lazily via PEP 562 `__getattr__` (the pattern scipy/sklearn use), with
   `__all__` for `import *` and a `TYPE_CHECKING` block so type checkers and the
   griffe-based docs builder still resolve every name statically.

2. Five modules did `import gooddata_api_client.models as afm_models`, and
   `execution.py` did `from gooddata_api_client import models`. That aggregate
   imports all 1402 generated model classes; the SDK references 58. The generated
   header of that file says as much ("import only the models that you directly
   need"), and it is safe because openapi-generator emits a per-model
   `lazy_import()`, so leaf imports do not cascade. The 58 now live in
   `gooddata_sdk/_models.py`, which keeps the `afm_models.X` spelling at the call
   sites and gives a single list to maintain.

3. `client.py` and `catalog_service_base.py` did `from gooddata_api_client import
   apis`, importing all ~130 generated API classes (and transitively most models)
   to instantiate six. Those six now come from `gooddata_sdk/_apis.py`.

Measured on py3.14, warm .pyc, best of 7 with the two versions interleaved:

    import gooddata_sdk                      0.58s / 2333 mods -> 0.04s /  160 mods
    from gooddata_sdk.visualization import
        Visualization                        0.54s / 2333 mods -> 0.19s /  991 mods
    from gooddata_sdk import GoodDataSdk     0.65s / 2333 mods -> 0.33s / 1402 mods

The module counts are the stable figure; the wall times are this machine and move
with load, so they are interleaved rather than compared across runs.

Keeping attribute access working
--------------------------------

The eager `__init__` used to populate the whole module tree as a side effect, so
`import gooddata_sdk` then `gooddata_sdk.catalog.workspace` resolved. Dropping it
broke that, so every subpackage now gets a lazy submodule `__getattr__` built by
`gooddata_sdk/_lazy.py`. It deliberately does not translate a `ModuleNotFoundError`
raised *inside* an existing submodule into `AttributeError`, so a genuinely missing
dependency still reports itself rather than looking like a typo.

Two intentional differences remain. `dir(gooddata_sdk)` now lists the public API
plus every submodule (previously only the ones that happened to be imported), and
`from gooddata_sdk import *` no longer leaks the `logging` module or the submodule
names, since `__all__` now states the public API explicitly.

Maintaining the lazy-import tables
----------------------------------

PEP 562 needs the name -> module mapping as data, and hand-maintaining ~260 string
pairs would drift from the real imports invisibly: a missing entry means
`from gooddata_sdk import NewThing` raises AttributeError at runtime, and a stale
module path is just a wrong string.

So the `TYPE_CHECKING` block is the single source of truth. It holds ordinary
`from x import Y` statements that IDEs autocomplete, refactorings rewrite and
`ty`/pyright verify, and `scripts/sync_lazy_imports.py` derives `_LAZY_IMPORTS` and
`__all__` from it by AST into a marker-delimited region that is never edited by
hand. Adding a public export is one normal import plus `--update`.

Drift is caught in two independent places, verified against three scenarios (export
added, module renamed, export removed):

- the script with no flags reports drift and exits non-zero, and
  `test_lazy_imports_map_matches_type_checking_block` runs it, so CI fails with the
  command to fix it;
- `test_all_lazy_exports_resolve` covers the other half -- a module that moved
  without the `TYPE_CHECKING` block being updated.

The rest of `tests/sdk/test_lazy_imports.py` guards the module savings themselves,
since one convenience import silently undoes them. The attribute-access cases each
run in their own interpreter: a missing lazy hook in a subpackage is masked as soon
as anything else has imported the tree.

gooddata-pandas used the models aggregate for one class; it now imports it
directly, and the test that patched `gooddata_pandas.dataframe.models.X` patches
`gooddata_pandas.dataframe.X`.

Its `[tool.ty.analysis]` allowed-unresolved-imports needed the `.**` glob that
gooddata-sdk already uses: the bare `gooddata_api_client` entry covered the old
aggregate import but not a leaf `gooddata_api_client.model.*` module.
@hkad98
hkad98 force-pushed the jkd/lazy-sdk-imports branch from 67d13be to 8125426 Compare September 16, 2026 13:50
@hkad98
hkad98 enabled auto-merge September 16, 2026 13:52
@codecov

codecov Bot commented Sep 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.38220% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.55%. Comparing base (b145e1a) to head (8125426).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
packages/gooddata-sdk/src/gooddata_sdk/__init__.py 88.23% 2 Missing ⚠️
...ta_source/entity_model/content_objects/__init__.py 0.00% 2 Missing ⚠️
packages/gooddata-sdk/src/gooddata_sdk/_lazy.py 91.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1815      +/-   ##
==========================================
+ Coverage   82.49%   82.55%   +0.05%     
==========================================
  Files         283      324      +41     
  Lines       20448    20543      +95     
==========================================
+ Hits        16869    16959      +90     
- Misses       3579     3584       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@hkad98
hkad98 merged commit b1437c0 into gooddata:master Sep 16, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants