dtex 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dtex-0.2.0 → dtex-0.2.2}/PKG-INFO +21 -2
- {dtex-0.2.0 → dtex-0.2.2}/README.md +20 -1
- {dtex-0.2.0 → dtex-0.2.2}/docs/03-connector-contract.md +1 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/__init__.py +4 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/bigquery/client.py +67 -8
- {dtex-0.2.0 → dtex-0.2.2}/dtex/engine/runner.py +1 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/registry.py +13 -2
- {dtex-0.2.0 → dtex-0.2.2}/dtex/skills/dtex-write-connector.md +74 -2
- dtex-0.2.2/dtex/sources/stripe/README.md +188 -0
- dtex-0.2.2/dtex/sources/stripe/queries/charges_daily.sql +24 -0
- dtex-0.2.2/dtex/sources/stripe/queries/invoices_paid.sql +21 -0
- dtex-0.2.2/dtex/sources/stripe/queries/subscriptions_active.sql +17 -0
- dtex-0.2.2/dtex/sources/stripe/register.yaml +322 -0
- dtex-0.2.2/dtex/sources/stripe/sigma_client.py +389 -0
- dtex-0.2.2/dtex/sources/stripe/source.py +439 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/types.py +60 -0
- {dtex-0.2.0 → dtex-0.2.2}/pyproject.toml +1 -1
- {dtex-0.2.0 → dtex-0.2.2}/tests/connectors/test_stripe.py +229 -1
- {dtex-0.2.0 → dtex-0.2.2}/tests/destinations/test_bigquery.py +73 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_registry.py +3 -1
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_types.py +42 -0
- dtex-0.2.0/dtex/sources/stripe/README.md +0 -128
- dtex-0.2.0/dtex/sources/stripe/register.yaml +0 -206
- dtex-0.2.0/dtex/sources/stripe/source.py +0 -222
- {dtex-0.2.0 → dtex-0.2.2}/.gitignore +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/CONTRIBUTING.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/LICENSE +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/00-vision-and-naming.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/01-landscape-and-comparison.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/02-architecture.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/04-connector-body.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/05-destinations-and-state.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/06-project-anatomy.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/07-cli-and-library-api.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/08-security.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/09-logging-and-observability.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/10-roadmap-and-scope.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/11-open-questions.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/12-configs.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/README.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/_archive/stripe-research-2026-05.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/_internal/release.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/docs/_internal/streams-redesign-plan.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/cli/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/cli/_discovery.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/cli/_format.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/cli/_runs.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/cli/_scaffold.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/cli/_secrets.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/cli/_skills.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/cli/_state.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/bigquery/README.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/bigquery/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/bigquery/ddl.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/bigquery/destination.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/bigquery/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/duckdb/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/duckdb/ddl.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/duckdb/destination.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/destinations/duckdb/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/engine/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/engine/config.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/engine/configs.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/engine/discovery.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/engine/logger.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/engine/normalize.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/py.typed +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/secrets/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/secrets/_aws.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/secrets/_gcp.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/secrets/_vault.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/secrets/resolvers.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/skills/dtex-debug.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/skills/dtex-write-config.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/filesystem/README.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/filesystem/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/filesystem/backends.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/filesystem/readers.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/filesystem/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/filesystem/source.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/postgres/README.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/postgres/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/postgres/client.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/postgres/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/postgres/source.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/postgres/type_mapping.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/rest/README.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/rest/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/rest/client.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/rest/extractors.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/rest/pagination.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/rest/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/rest/source.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/shiphero/README.md +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/shiphero/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/shiphero/client.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/shiphero/pagination.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/shiphero/queries.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/shiphero/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/shiphero/source.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/shiphero/windows.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/stripe/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/stripe/client.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/dtex/sources/stripe/pagination.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/conftest.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/connectors/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/connectors/test_filesystem.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/connectors/test_postgres.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/connectors/test_rest.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/connectors/test_shiphero.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/destinations/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/configs/echo.yml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/destinations/lockedfake/destination.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/destinations/lockedfake/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/dtex_project.yml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/profiles.yml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/sources/echo/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/sources/echo/source.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/sources/multifile_echo/__init__.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/sources/multifile_echo/helper.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/sources/multifile_echo/register.yaml +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/fixtures/sources/multifile_echo/source.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_cli.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_configs.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_duckdb_destination.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_engine.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_normalize.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_run_records.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_secret_resolver_aws.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_secret_resolver_gcp.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_secret_resolver_vault.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_secret_resolvers.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_skeleton.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_skills_install.py +0 -0
- {dtex-0.2.0 → dtex-0.2.2}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dtex
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: dtex (data extraction tool) — an open-source Python EL tool: pipelines as configs, connectors as folders, CLI-first.
|
|
5
5
|
Project-URL: Homepage, https://github.com/vej-ai/dtex
|
|
6
6
|
Project-URL: Documentation, https://github.com/vej-ai/dtex/tree/main/docs
|
|
@@ -104,7 +104,7 @@ params. Run it with `dtex run -p <config>`. The library equivalent is
|
|
|
104
104
|
**Sources:** `filesystem` (CSV/JSONL/Parquet from local, GCS, or S3),
|
|
105
105
|
`rest` (paginated REST APIs — 4 pagination strategies, 4 auth modes),
|
|
106
106
|
`postgres` (keyset pagination, no `OFFSET`), `shiphero` (GraphQL),
|
|
107
|
-
`stripe` (resource-as-stream
|
|
107
|
+
`stripe` (REST resource-as-stream + opt-in Sigma SQL-as-stream).
|
|
108
108
|
|
|
109
109
|
**Destinations:** `duckdb` (zero-config dev default, all 5 capabilities) and
|
|
110
110
|
`bigquery` (production warehouse — Parquet-staged via GCS + LOAD jobs,
|
|
@@ -119,6 +119,25 @@ per-destination caps.
|
|
|
119
119
|
**Secret managers:** GCP Secret Manager, AWS Secrets Manager, HashiCorp
|
|
120
120
|
Vault — each as an opt-in extra.
|
|
121
121
|
|
|
122
|
+
## Bundled Claude skills
|
|
123
|
+
|
|
124
|
+
dtex ships three [Claude](https://claude.com) skills inside the wheel:
|
|
125
|
+
`dtex-write-config`, `dtex-write-connector`, `dtex-debug`. They teach
|
|
126
|
+
Claude (or any other agent runtime that reads `.claude/skills/`) the
|
|
127
|
+
post-streams-redesign config schema, the `@stream` decorator pattern, and
|
|
128
|
+
the debugging playbook. Install them into a dtex project with:
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
dtex skills install # copies into ./.claude/skills/dtex/
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The first `dtex` command run inside a project that lacks installed skills
|
|
135
|
+
prints a one-line hint (suppressed thereafter). Run `dtex skills list` to
|
|
136
|
+
see which ship with this dtex version and which are installed locally.
|
|
137
|
+
|
|
138
|
+
If you're not using Claude, just ignore the hint — the skills are inert
|
|
139
|
+
markdown until installed.
|
|
140
|
+
|
|
122
141
|
## Documentation
|
|
123
142
|
|
|
124
143
|
The full design handbook lives in [`docs/`](https://github.com/vej-ai/dtex/tree/main/docs).
|
|
@@ -54,7 +54,7 @@ params. Run it with `dtex run -p <config>`. The library equivalent is
|
|
|
54
54
|
**Sources:** `filesystem` (CSV/JSONL/Parquet from local, GCS, or S3),
|
|
55
55
|
`rest` (paginated REST APIs — 4 pagination strategies, 4 auth modes),
|
|
56
56
|
`postgres` (keyset pagination, no `OFFSET`), `shiphero` (GraphQL),
|
|
57
|
-
`stripe` (resource-as-stream
|
|
57
|
+
`stripe` (REST resource-as-stream + opt-in Sigma SQL-as-stream).
|
|
58
58
|
|
|
59
59
|
**Destinations:** `duckdb` (zero-config dev default, all 5 capabilities) and
|
|
60
60
|
`bigquery` (production warehouse — Parquet-staged via GCS + LOAD jobs,
|
|
@@ -69,6 +69,25 @@ per-destination caps.
|
|
|
69
69
|
**Secret managers:** GCP Secret Manager, AWS Secrets Manager, HashiCorp
|
|
70
70
|
Vault — each as an opt-in extra.
|
|
71
71
|
|
|
72
|
+
## Bundled Claude skills
|
|
73
|
+
|
|
74
|
+
dtex ships three [Claude](https://claude.com) skills inside the wheel:
|
|
75
|
+
`dtex-write-config`, `dtex-write-connector`, `dtex-debug`. They teach
|
|
76
|
+
Claude (or any other agent runtime that reads `.claude/skills/`) the
|
|
77
|
+
post-streams-redesign config schema, the `@stream` decorator pattern, and
|
|
78
|
+
the debugging playbook. Install them into a dtex project with:
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
dtex skills install # copies into ./.claude/skills/dtex/
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The first `dtex` command run inside a project that lacks installed skills
|
|
85
|
+
prints a one-line hint (suppressed thereafter). Run `dtex skills list` to
|
|
86
|
+
see which ship with this dtex version and which are installed locally.
|
|
87
|
+
|
|
88
|
+
If you're not using Claude, just ignore the hint — the skills are inert
|
|
89
|
+
markdown until installed.
|
|
90
|
+
|
|
72
91
|
## Documentation
|
|
73
92
|
|
|
74
93
|
The full design handbook lives in [`docs/`](https://github.com/vej-ai/dtex/tree/main/docs).
|
|
@@ -420,6 +420,7 @@ objects the function asks for. A function that needs nothing extra can be
|
|
|
420
420
|
| `state` | `State` | Per-stream persisted state (chapter 04). Free-form key/value scratch space that survives between runs. |
|
|
421
421
|
| `cursor` | `Cursor` | Incremental cursor helper, present only if the stream declares an `incremental` block. See §3.2. |
|
|
422
422
|
| `log` | `Logger` | Structured logger. `log.info(...)`, `log.warning(...)`. Correlation ids attached by the engine. |
|
|
423
|
+
| `stream_def` | `StreamDef` | The parsed `register.yaml` entry for this stream. Lets a connector introspect its own declaration — e.g. the baked `stripe` connector reads `stream_def.sigma.query` to decide which SQL file to submit for Sigma-backed streams. Use when one connector serves more than one extraction surface and the surface is declared in the manifest. |
|
|
423
424
|
|
|
424
425
|
#### What a `@stream` function yields
|
|
425
426
|
|
|
@@ -70,9 +70,11 @@ from dtex.types import (
|
|
|
70
70
|
RunStatus,
|
|
71
71
|
Schema,
|
|
72
72
|
SchemaContract,
|
|
73
|
+
SigmaConfig,
|
|
73
74
|
State,
|
|
74
75
|
StateBackend,
|
|
75
76
|
StateRecord,
|
|
77
|
+
StreamDef,
|
|
76
78
|
StreamMeta,
|
|
77
79
|
StreamMode,
|
|
78
80
|
StreamResult,
|
|
@@ -136,9 +138,11 @@ __all__ = [
|
|
|
136
138
|
"RunStatus",
|
|
137
139
|
"Schema",
|
|
138
140
|
"SchemaContract",
|
|
141
|
+
"SigmaConfig",
|
|
139
142
|
"State",
|
|
140
143
|
"StateBackend",
|
|
141
144
|
"StateRecord",
|
|
145
|
+
"StreamDef",
|
|
142
146
|
"StreamMode",
|
|
143
147
|
"StreamRunConfig",
|
|
144
148
|
"StreamMeta",
|
|
@@ -139,6 +139,56 @@ def _status_code(exc: BaseException) -> int | None:
|
|
|
139
139
|
return None
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def _is_retryable_network_error(exc: BaseException) -> bool:
|
|
143
|
+
"""Whether ``exc`` is a transient network-layer failure worth retrying.
|
|
144
|
+
|
|
145
|
+
Long-running runs (a backfill against a high-row-count source like
|
|
146
|
+
RevenueCat that involves hundreds of sequential LOAD jobs) routinely
|
|
147
|
+
hit stale-TCP-socket failures: a keep-alive connection held open by
|
|
148
|
+
the google-cloud-bigquery client gets killed by an intermediary,
|
|
149
|
+
and the next reuse raises a ``requests.exceptions.ConnectionError``
|
|
150
|
+
(typically wrapping ``http.client.RemoteDisconnected``). These have
|
|
151
|
+
NO HTTP status code (the connection died before any response
|
|
152
|
+
arrived), so the status-code path in :func:`run_with_retries` would
|
|
153
|
+
re-raise them immediately. This helper recognises them so the
|
|
154
|
+
retry loop covers them.
|
|
155
|
+
|
|
156
|
+
Genuine programming errors (``KeyError``, ``TypeError``, etc.) are
|
|
157
|
+
NOT matched — they should surface immediately, not get masked under
|
|
158
|
+
a few backoff cycles. The match is by concrete network-exception
|
|
159
|
+
type, not "any statusless exception."
|
|
160
|
+
"""
|
|
161
|
+
# requests-level connection/timeout failures (ConnectionError /
|
|
162
|
+
# Timeout / ConnectTimeout / ReadTimeout / ChunkedEncodingError /
|
|
163
|
+
# ProxyError / SSLError — all RequestException subclasses).
|
|
164
|
+
import requests # noqa: PLC0415 — narrow runtime import
|
|
165
|
+
|
|
166
|
+
if isinstance(
|
|
167
|
+
exc,
|
|
168
|
+
(
|
|
169
|
+
requests.exceptions.ConnectionError,
|
|
170
|
+
requests.exceptions.Timeout,
|
|
171
|
+
requests.exceptions.ChunkedEncodingError,
|
|
172
|
+
),
|
|
173
|
+
):
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
# google-api-core wraps some of the above under its own
|
|
177
|
+
# ServiceUnavailable / RetryError. These usually DO carry a 503
|
|
178
|
+
# status code so the existing path catches them, but RetryError
|
|
179
|
+
# (raised when google's own internal retry exhausts) sometimes
|
|
180
|
+
# surfaces without one.
|
|
181
|
+
try:
|
|
182
|
+
from google.api_core import exceptions as gapic_exc # noqa: PLC0415
|
|
183
|
+
|
|
184
|
+
if isinstance(exc, gapic_exc.RetryError):
|
|
185
|
+
return True
|
|
186
|
+
except ImportError: # pragma: no cover — google-api-core is installed
|
|
187
|
+
pass
|
|
188
|
+
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
|
|
142
192
|
def run_with_retries(
|
|
143
193
|
operation: Any,
|
|
144
194
|
*,
|
|
@@ -146,14 +196,16 @@ def run_with_retries(
|
|
|
146
196
|
backoff_seconds: float,
|
|
147
197
|
sleep: Any = time.sleep,
|
|
148
198
|
) -> Any:
|
|
149
|
-
"""Run ``operation()`` with exponential backoff on transient
|
|
199
|
+
"""Run ``operation()`` with exponential backoff on transient failures.
|
|
150
200
|
|
|
151
201
|
``operation`` is a zero-arg callable — typically a ``lambda: job.result(
|
|
152
202
|
timeout=...)`` or a ``lambda: client.load_table_from_uri(...).result(
|
|
153
|
-
timeout=...)``. A retryable failure (status in :data:`_RETRYABLE_STATUS`
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
203
|
+
timeout=...)``. A retryable failure (status in :data:`_RETRYABLE_STATUS`
|
|
204
|
+
OR a connection-class network error matched by
|
|
205
|
+
:func:`_is_retryable_network_error`) sleeps ``backoff_seconds * 2**attempt``
|
|
206
|
+
and retries; a non-retryable failure re-raises immediately. The
|
|
207
|
+
``sleep`` parameter is injectable so unit tests can avoid real
|
|
208
|
+
wall-clock delays.
|
|
157
209
|
"""
|
|
158
210
|
if max_attempts < 1:
|
|
159
211
|
max_attempts = 1
|
|
@@ -163,16 +215,23 @@ def run_with_retries(
|
|
|
163
215
|
return operation()
|
|
164
216
|
except Exception as exc: # noqa: BLE001 — re-raises non-retryable below
|
|
165
217
|
status = _status_code(exc)
|
|
166
|
-
|
|
218
|
+
retryable_by_status = status is not None and status in _RETRYABLE_STATUS
|
|
219
|
+
retryable_by_network = _is_retryable_network_error(exc)
|
|
220
|
+
if not (retryable_by_status or retryable_by_network):
|
|
167
221
|
raise
|
|
168
222
|
last_exc = exc
|
|
169
223
|
if attempt == max_attempts - 1:
|
|
170
224
|
raise
|
|
171
225
|
delay = backoff_seconds * (2 ** attempt)
|
|
226
|
+
reason = (
|
|
227
|
+
f"status={status}"
|
|
228
|
+
if retryable_by_status
|
|
229
|
+
else f"network={type(exc).__name__}"
|
|
230
|
+
)
|
|
172
231
|
logger.warning(
|
|
173
|
-
"BigQuery transient error (
|
|
232
|
+
"BigQuery transient error (%s, attempt=%d/%d): %s — "
|
|
174
233
|
"retrying in %.1fs",
|
|
175
|
-
|
|
234
|
+
reason,
|
|
176
235
|
attempt + 1,
|
|
177
236
|
max_attempts,
|
|
178
237
|
exc,
|
|
@@ -102,12 +102,23 @@ __all__ = [
|
|
|
102
102
|
# Injection — the parameter names the engine may inject (docs/03 §3.1)
|
|
103
103
|
# ---------------------------------------------------------------------------
|
|
104
104
|
|
|
105
|
-
STREAM_INJECTABLES: frozenset[str] = frozenset(
|
|
105
|
+
STREAM_INJECTABLES: frozenset[str] = frozenset(
|
|
106
|
+
{"config", "state", "cursor", "log", "stream_def"}
|
|
107
|
+
)
|
|
106
108
|
"""The parameter names the engine injects into a ``@stream`` function — docs/03 §3.1.
|
|
107
109
|
|
|
108
110
|
The engine inspects the function signature and injects, *by name*, only the
|
|
109
111
|
objects the function declares. A ``@stream`` function may declare any subset of
|
|
110
|
-
these
|
|
112
|
+
these; declaring anything else is a discovery-time error (docs/03 §7 rule 8).
|
|
113
|
+
|
|
114
|
+
* ``config`` — resolved :class:`dtex.Config` (params + secrets, per-stream layered).
|
|
115
|
+
* ``state`` — the per-stream state blob (a :class:`dtex.State` proxy).
|
|
116
|
+
* ``cursor`` — the per-stream :class:`dtex.Cursor` (incremental streams only).
|
|
117
|
+
* ``log`` — a standard logging-style logger pre-configured with run-correlation.
|
|
118
|
+
* ``stream_def`` — the parsed :class:`dtex.StreamDef` declaration for this
|
|
119
|
+
stream (its own ``register.yaml`` entry). Lets a connector introspect its
|
|
120
|
+
own metadata — e.g. the ``stripe`` connector dispatches on
|
|
121
|
+
``stream_def.sigma`` to choose REST-vs-Sigma extraction per stream.
|
|
111
122
|
"""
|
|
112
123
|
|
|
113
124
|
# NOTE: docs/03 §4 says a ``@stream_method`` follows "the same injection rules
|
|
@@ -131,8 +131,8 @@ def customers(config: Config, state: State, cursor: Cursor, log) -> Iterator[Bat
|
|
|
131
131
|
### Injected arguments
|
|
132
132
|
|
|
133
133
|
The engine declares which arguments it can inject — `config`, `state`,
|
|
134
|
-
`cursor`, `log` — and supplies only the ones the function
|
|
135
|
-
only what you use; the engine matches by name.
|
|
134
|
+
`cursor`, `log`, `stream_def` — and supplies only the ones the function
|
|
135
|
+
names. List only what you use; the engine matches by name.
|
|
136
136
|
|
|
137
137
|
- **`config`** — the resolved `Config` for this run. Read params via
|
|
138
138
|
`config.get("key")` or attribute access. Read secrets via
|
|
@@ -143,6 +143,12 @@ only what you use; the engine matches by name.
|
|
|
143
143
|
that need to remember more than a cursor.
|
|
144
144
|
- **`log`** — a standard logging-style logger. `log.info(...)`,
|
|
145
145
|
`log.warning(...)`.
|
|
146
|
+
- **`stream_def`** — the parsed `StreamDef` for this stream — its own
|
|
147
|
+
`register.yaml` entry. Lets a connector introspect its own
|
|
148
|
+
declaration. Useful when one connector serves more than one
|
|
149
|
+
extraction surface (REST + Sigma SQL, GA + Reporting API, etc.) and
|
|
150
|
+
the surface choice is declared in the manifest. See "Dual-API
|
|
151
|
+
connectors" below.
|
|
146
152
|
|
|
147
153
|
### Three rules to drill in
|
|
148
154
|
|
|
@@ -162,6 +168,72 @@ but it does not reshape nested dicts into flat columns. If the API returns
|
|
|
162
168
|
`{"attributes": {"country": "US"}}` and your schema declares
|
|
163
169
|
`country: STRING`, you must flatten in the `@stream` function before yielding.
|
|
164
170
|
|
|
171
|
+
## Dual-API connectors — when one source serves two surfaces
|
|
172
|
+
|
|
173
|
+
Some APIs come in two flavors that the same operator pulls from in one
|
|
174
|
+
pipeline. Stripe is the canonical example: a standard REST API
|
|
175
|
+
(resource-as-stream, GA) AND a Sigma SQL API (query-as-stream, paid,
|
|
176
|
+
~3-hour lag). Both belong to one "Stripe" connector, not two.
|
|
177
|
+
|
|
178
|
+
The dtex pattern is **one source folder, two extraction surfaces, per-stream
|
|
179
|
+
opt-in**. The baked `stripe` connector demonstrates this:
|
|
180
|
+
|
|
181
|
+
```yaml
|
|
182
|
+
# dtex/sources/stripe/register.yaml — abbreviated
|
|
183
|
+
streams:
|
|
184
|
+
- name: charges # REST — no `sigma:` block
|
|
185
|
+
table: stripe_charges
|
|
186
|
+
incremental: {cursor_field: created, cursor_type: int}
|
|
187
|
+
schema: [...]
|
|
188
|
+
|
|
189
|
+
- name: charges_daily # Sigma — opted in via `sigma:` block
|
|
190
|
+
table: charges
|
|
191
|
+
sigma:
|
|
192
|
+
query: queries/charges_daily.sql
|
|
193
|
+
incremental: {cursor_field: created, cursor_type: timestamp}
|
|
194
|
+
schema: [...]
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
In `source.py`, each `@stream` function knows which surface it belongs
|
|
198
|
+
to by which helper it calls. The Sigma functions take the `stream_def`
|
|
199
|
+
injectable, read `stream_def.sigma.query`, and load the SQL from the
|
|
200
|
+
named file:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
@stream(name="charges_daily")
|
|
204
|
+
def charges_daily(
|
|
205
|
+
stream_def: StreamDef, config: Config, cursor: Cursor, log
|
|
206
|
+
) -> Iterator[Batch]:
|
|
207
|
+
yield from _extract_sigma_stream(stream_def, config, cursor, log)
|
|
208
|
+
|
|
209
|
+
# _extract_sigma_stream(stream_def, ...) reads stream_def.sigma.query
|
|
210
|
+
# from the connector folder, binds {since} from the cursor, submits to
|
|
211
|
+
# Sigma, downloads CSV, yields batches.
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The decision rules:
|
|
215
|
+
|
|
216
|
+
* **One connector, not two.** Users binding `source: stripe` see one
|
|
217
|
+
surface; the `sigma:` block is the opt-in marker per stream. Don't
|
|
218
|
+
fork into `stripe` and `stripe_sigma`.
|
|
219
|
+
* **Sigma streams declare `sigma: {query: <path>}`.** Path is relative
|
|
220
|
+
to the connector folder. The `@stream` function reads it from
|
|
221
|
+
`stream_def.sigma.query` (via the `stream_def` injectable) and
|
|
222
|
+
loads the SQL.
|
|
223
|
+
* **REST streams don't change.** No `sigma:` block, no `stream_def`
|
|
224
|
+
arg required — existing connectors stay shaped exactly as they were.
|
|
225
|
+
* **Shared client params, shared auth.** A single Stripe restricted
|
|
226
|
+
key with both REST + Sigma scopes drives both surfaces from one
|
|
227
|
+
`api_key` secret. Keep it that way; don't introduce per-surface
|
|
228
|
+
secrets unless the API genuinely demands separate keys.
|
|
229
|
+
|
|
230
|
+
The same pattern fits other APIs with a "live REST + analytical SQL"
|
|
231
|
+
shape — Shopify GraphQL + Shopify Bulk Operations, Salesforce REST +
|
|
232
|
+
SOQL, GA Data API + BigQuery export, etc. The `sigma:` block is
|
|
233
|
+
Stripe-specific naming today; future dual-surface connectors should
|
|
234
|
+
either reuse it (if they have similar SQL semantics) or add a parallel
|
|
235
|
+
marker (`bulk:`, `soql:`) following the same opt-in shape.
|
|
236
|
+
|
|
165
237
|
## The `client.py` pattern
|
|
166
238
|
|
|
167
239
|
```python
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Stripe — baked source connector
|
|
2
|
+
|
|
3
|
+
One Stripe connector, **two extraction surfaces**:
|
|
4
|
+
|
|
5
|
+
* **REST** (default) — resource-as-stream over Stripe's GA REST API at
|
|
6
|
+
`https://api.stripe.com/v1`. One stream per resource type
|
|
7
|
+
(`charges`, `invoices`, `customers`, `subscriptions`), incremental on
|
|
8
|
+
the Unix `created` timestamp, paginated via Stripe's cursor model
|
|
9
|
+
(`starting_after` + `has_more`). Cheap, near-live, no extra
|
|
10
|
+
subscription.
|
|
11
|
+
* **Sigma SQL** (opt-in per stream) — query-as-stream over Stripe's
|
|
12
|
+
Sigma API. Each Sigma stream's `register.yaml` entry carries a
|
|
13
|
+
`sigma: {query: queries/<name>.sql}` block; the connector reads the
|
|
14
|
+
SQL body, submits it via `POST /v2/data/reporting/query_runs`, polls
|
|
15
|
+
to completion, downloads the CSV result, and yields batches. Lets a
|
|
16
|
+
single stream JOIN/aggregate across Stripe's internal tables in
|
|
17
|
+
Stripe-side compute. Requires a **paid Sigma subscription** and
|
|
18
|
+
carries a **~3-hour data lag**.
|
|
19
|
+
|
|
20
|
+
The two surfaces share one source folder, one set of connector params,
|
|
21
|
+
and one `api_key` — a single Stripe restricted key with the relevant
|
|
22
|
+
read scopes drives both. Configs mix REST and Sigma streams freely.
|
|
23
|
+
|
|
24
|
+
## Authentication
|
|
25
|
+
|
|
26
|
+
The connector reads a **restricted Stripe secret key** with **read-only
|
|
27
|
+
scopes** from the `STRIPE_API_KEY` environment variable:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
export STRIPE_API_KEY="rk_live_..."
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Recommended restricted-key scopes:
|
|
34
|
+
|
|
35
|
+
| Surface | Resource | Scope |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| REST | `/charges` | `charge:read` |
|
|
38
|
+
| REST | `/invoices` | `invoice:read` |
|
|
39
|
+
| REST | `/customers` | `customer:read` |
|
|
40
|
+
| REST | `/subscriptions` | `subscription:read` |
|
|
41
|
+
| Sigma | `POST /v2/data/reporting/query_runs` | `sigma:read`, `reporting:read` |
|
|
42
|
+
|
|
43
|
+
If your config only uses REST streams you don't need the Sigma scopes,
|
|
44
|
+
and vice versa.
|
|
45
|
+
|
|
46
|
+
The key is sent as `Authorization: Bearer <key>` on every request. It is
|
|
47
|
+
**never logged** — the `Authorization` header is redacted at each client's
|
|
48
|
+
single logging choke point.
|
|
49
|
+
|
|
50
|
+
## Supported resources (v1)
|
|
51
|
+
|
|
52
|
+
Four streams, all `write_disposition: merge` on `primary_key: id`:
|
|
53
|
+
|
|
54
|
+
- **`charges`** → `stripe_charges`
|
|
55
|
+
- **`invoices`** → `stripe_invoices`
|
|
56
|
+
- **`customers`** → `stripe_customers`
|
|
57
|
+
- **`subscriptions`** → `stripe_subscriptions`
|
|
58
|
+
|
|
59
|
+
Each declares an explicit schema covering the common columns (`id`, `object`,
|
|
60
|
+
`created`, `livemode`, …) plus the resource-specific scalars. Nested
|
|
61
|
+
sub-objects (e.g. `payment_method_details`, `lines`, `items`, `metadata`)
|
|
62
|
+
land as `JSON` columns — the dtex convention for nested data
|
|
63
|
+
(docs/04 §"Nested data").
|
|
64
|
+
|
|
65
|
+
## Incremental model
|
|
66
|
+
|
|
67
|
+
Every stream uses:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
incremental:
|
|
71
|
+
cursor_field: created
|
|
72
|
+
cursor_type: int # Stripe `created` is a Unix-timestamp integer
|
|
73
|
+
lookback: 6h # declared hint
|
|
74
|
+
initial_value: "1704067200" # 2024-01-01 UTC
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
On each run the connector translates the engine's `cursor.start_value()` into
|
|
78
|
+
a `created[gte]=<ts>` query parameter, walks every page with
|
|
79
|
+
`starting_after=<last_id>`, and `cursor.observe()`s each record so the engine
|
|
80
|
+
can advance the cursor.
|
|
81
|
+
|
|
82
|
+
Stripe lists are returned **newest first** within the new-records window;
|
|
83
|
+
`cursor.observe()` takes the *max*, so the final cursor is correct regardless
|
|
84
|
+
of arrival order. Per dtex's commit model the cursor is committed only
|
|
85
|
+
after every batch durably lands, so a mid-stream crash safely re-runs the
|
|
86
|
+
whole window — no lost or skipped rows.
|
|
87
|
+
|
|
88
|
+
`merge` write disposition means re-fetched objects (an invoice whose status
|
|
89
|
+
changed after it was first loaded) upsert in place, so a periodic
|
|
90
|
+
`lookback`-style re-fetch is idempotent.
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
`register.yaml` declares the connector; the project binds it to a
|
|
95
|
+
destination via its `destination:` block or via the project-level
|
|
96
|
+
`default_destination`. Running it is the standard dtex one-liner:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import dtex
|
|
100
|
+
|
|
101
|
+
result = dtex.run(
|
|
102
|
+
connector="stripe",
|
|
103
|
+
target="prod",
|
|
104
|
+
# Run a single stream:
|
|
105
|
+
select=("charges",),
|
|
106
|
+
# Or override params per invocation:
|
|
107
|
+
params={"page_size": 100, "requests_per_second": 50},
|
|
108
|
+
)
|
|
109
|
+
print(result.status, result.rows_loaded)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
CLI equivalent:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
dtex run stripe --target prod --select charges
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Operational notes
|
|
119
|
+
|
|
120
|
+
- **Rate limit.** Default `requests_per_second: 25` is conservative against
|
|
121
|
+
Stripe's documented ~100/sec live-mode limit. Tune up via `params` if you
|
|
122
|
+
control the account's throttling budget.
|
|
123
|
+
- **Retries.** `429` honors `Retry-After` exactly; `5xx` retries with
|
|
124
|
+
exponential backoff up to `max_retries: 5`; other `4xx` (`401`, `403`,
|
|
125
|
+
`404`) raise immediately — a bad request will not get better on its own.
|
|
126
|
+
- **Dependencies.** `requests>=2.31` (declared in `pyproject.toml`); no
|
|
127
|
+
Stripe SDK — raw HTTP keeps the dependency surface minimal and avoids
|
|
128
|
+
version coupling to the SDK's release cadence.
|
|
129
|
+
- **API version pin.** `api_version` defaults to `2024-12-18.acacia`; see
|
|
130
|
+
[docs/connectors/stripe-research.md](../../../docs/connectors/stripe-research.md)
|
|
131
|
+
for the source of that pin and where to check for the current GA.
|
|
132
|
+
|
|
133
|
+
## Sigma SQL streams
|
|
134
|
+
|
|
135
|
+
Three Sigma streams ship in `register.yaml`:
|
|
136
|
+
|
|
137
|
+
- **`charges_daily`** → `charges` (incremental on `created`)
|
|
138
|
+
- **`subscriptions_active`** → `subscriptions` (full refresh)
|
|
139
|
+
- **`invoices_paid`** → `invoices` (incremental on `status_transitions_paid_at`)
|
|
140
|
+
|
|
141
|
+
The SQL bodies live under `queries/<stream_name>.sql`. The connector binds
|
|
142
|
+
the `{since}` placeholder from each stream's `Cursor.start_value()` (or
|
|
143
|
+
the connector-level `sigma_initial_since` for non-incremental streams) as a
|
|
144
|
+
Presto `TIMESTAMP 'YYYY-MM-DD HH:MM:SS'` literal — Sigma rejects ISO-8601's
|
|
145
|
+
`T` separator and timezone suffixes.
|
|
146
|
+
|
|
147
|
+
### Adding a 4th Sigma stream
|
|
148
|
+
|
|
149
|
+
Two files + one function:
|
|
150
|
+
|
|
151
|
+
1. Drop your query at `queries/<new_stream>.sql`. Use `{since}` if it's
|
|
152
|
+
incremental.
|
|
153
|
+
2. Append a stream to `register.yaml` declaring the schema and the
|
|
154
|
+
`sigma:` block:
|
|
155
|
+
```yaml
|
|
156
|
+
- name: my_new_stream
|
|
157
|
+
table: my_new_table
|
|
158
|
+
primary_key: id
|
|
159
|
+
write_disposition: merge
|
|
160
|
+
sigma:
|
|
161
|
+
query: queries/my_new_stream.sql
|
|
162
|
+
incremental:
|
|
163
|
+
cursor_field: <col>
|
|
164
|
+
cursor_type: timestamp
|
|
165
|
+
initial_value: "2024-01-01T00:00:00Z"
|
|
166
|
+
schema:
|
|
167
|
+
- {name: id, type: STRING, mode: REQUIRED}
|
|
168
|
+
- {name: <col>, type: TIMESTAMP, mode: REQUIRED}
|
|
169
|
+
- ...
|
|
170
|
+
```
|
|
171
|
+
3. Add a one-line `@stream` wrapper in `source.py` that delegates to
|
|
172
|
+
`_extract_sigma_stream` (the existing functions show the pattern).
|
|
173
|
+
|
|
174
|
+
### Operational notes (Sigma-specific)
|
|
175
|
+
|
|
176
|
+
- **Account ID required.** The config must set `params.account_id` to the
|
|
177
|
+
`acct_...` ID of the Stripe account being queried. Stripe rejects every
|
|
178
|
+
Sigma request with HTTP 403 otherwise.
|
|
179
|
+
- **API version pin.** `sigma_api_version` defaults to
|
|
180
|
+
`2026-04-22.preview`; the Query Run API is preview. Bump deliberately.
|
|
181
|
+
- **Polling.** `sigma_poll_interval_seconds` (default `2.0`) is the gap
|
|
182
|
+
between status polls; `sigma_poll_timeout_seconds` (default `600`) is the
|
|
183
|
+
hard ceiling per query. Heavy queries on large accounts approach the
|
|
184
|
+
ceiling; bump if needed.
|
|
185
|
+
- **Download retries.** A truncated CSV mid-body retries the whole download
|
|
186
|
+
up to `max_retries` (default `5`) with exponential backoff
|
|
187
|
+
(`retry_backoff_seconds`, default `1.0`). Duplicate-free because no rows
|
|
188
|
+
are yielded until the full CSV is in hand.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- charges_daily — every Charge created after the cursor floor.
|
|
2
|
+
--
|
|
3
|
+
-- Sigma's dialect is PrestoDB-flavored. The {since} placeholder is
|
|
4
|
+
-- substituted by source.py at run time from the engine's Cursor; it
|
|
5
|
+
-- arrives here as an ISO-8601 string and is compared against `created`
|
|
6
|
+
-- (a Sigma TIMESTAMP column).
|
|
7
|
+
--
|
|
8
|
+
-- Schema is mirrored in register.yaml. If you add a column here, add
|
|
9
|
+
-- it there (and bump the Stripe Sigma API version pin if the new
|
|
10
|
+
-- column is preview-only).
|
|
11
|
+
|
|
12
|
+
SELECT
|
|
13
|
+
id,
|
|
14
|
+
amount,
|
|
15
|
+
currency,
|
|
16
|
+
created,
|
|
17
|
+
status,
|
|
18
|
+
customer_id,
|
|
19
|
+
description,
|
|
20
|
+
paid,
|
|
21
|
+
refunded
|
|
22
|
+
FROM charges
|
|
23
|
+
WHERE created > TIMESTAMP {since}
|
|
24
|
+
ORDER BY created
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- invoices_paid — paid invoices since the cursor floor.
|
|
2
|
+
--
|
|
3
|
+
-- Sigma's invoices table tracks lifecycle transitions in the
|
|
4
|
+
-- `status_transitions_*` columns. `status_transitions_paid_at` is the
|
|
5
|
+
-- timestamp the invoice transitioned to paid; that's the right
|
|
6
|
+
-- incremental cursor (`created` or `date` would re-load an invoice
|
|
7
|
+
-- whose payment processed long after it was issued).
|
|
8
|
+
|
|
9
|
+
SELECT
|
|
10
|
+
id,
|
|
11
|
+
customer_id,
|
|
12
|
+
subscription_id,
|
|
13
|
+
total,
|
|
14
|
+
amount_paid,
|
|
15
|
+
currency,
|
|
16
|
+
status_transitions_paid_at,
|
|
17
|
+
status,
|
|
18
|
+
number
|
|
19
|
+
FROM invoices
|
|
20
|
+
WHERE status_transitions_paid_at > TIMESTAMP {since}
|
|
21
|
+
ORDER BY status_transitions_paid_at
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
-- subscriptions_active — currently-active subscriptions, full snapshot.
|
|
2
|
+
--
|
|
3
|
+
-- No {since} placeholder: this stream is full-refresh (`write_disposition:
|
|
4
|
+
-- replace` in register.yaml). Sigma's `subscriptions` table is the
|
|
5
|
+
-- authoritative current state of every subscription.
|
|
6
|
+
|
|
7
|
+
SELECT
|
|
8
|
+
id,
|
|
9
|
+
customer_id,
|
|
10
|
+
status,
|
|
11
|
+
current_period_start,
|
|
12
|
+
current_period_end,
|
|
13
|
+
cancel_at_period_end,
|
|
14
|
+
created
|
|
15
|
+
FROM subscriptions
|
|
16
|
+
WHERE status = 'active'
|
|
17
|
+
ORDER BY created
|