spreadspace 0.1.0__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.
@@ -0,0 +1,13 @@
1
+ # Generated OpenAPI core — produced by scripts/generate.sh in CI. DO NOT EDIT,
2
+ # never commit (regenerated every run from the spec).
3
+ src/spreadspace/_generated/
4
+
5
+ # Python build / test artifacts
6
+ __pycache__/
7
+ *.py[cod]
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ *.egg-info/
11
+ build/
12
+ dist/
13
+ .venv/
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: spreadspace
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for the SpreadSpace API.
5
+ Project-URL: Homepage, https://spreadspace.ai
6
+ Project-URL: Documentation, https://docs.spreadspace.ai
7
+ Project-URL: Source, https://github.com/spreadspace
8
+ Author: SpreadSpace
9
+ License: MIT
10
+ Keywords: api,document,extraction,lending,sdk,spreadspace
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: httpx<1,>=0.27
16
+ Provides-Extra: dev
17
+ Requires-Dist: mypy>=1.10; extra == 'dev'
18
+ Requires-Dist: pytest>=8; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # SpreadSpace Python SDK
22
+
23
+ Official Python client for the [SpreadSpace API](https://docs.spreadspace.ai) —
24
+ document extraction and financial spreading for lending.
25
+
26
+ > Some examples below require the generated core (built in CI) or a live sandbox
27
+ > key. They are marked **[needs sandbox key]** / **[needs generated core]**.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install spreadspace
33
+ ```
34
+
35
+ Requires Python 3.9+.
36
+
37
+ ## Authentication
38
+
39
+ ```python
40
+ from spreadspace import SpreadSpace
41
+
42
+ client = SpreadSpace(api_key="ss_test_...") # or omit and set SPREADSPACE_API_KEY
43
+ ```
44
+
45
+ The key prefix selects the environment:
46
+
47
+ - `ss_test_...` — routes to your **sandbox tenant** (seed data, safe to experiment).
48
+ - `ss_live_...` — routes to your live tenant (real data).
49
+
50
+ If `api_key` is omitted, the client reads `SPREADSPACE_API_KEY` from the
51
+ environment. Never hard-code a live key; never commit any key.
52
+
53
+ ## Client options
54
+
55
+ ```python
56
+ client = SpreadSpace(
57
+ api_key="ss_test_...",
58
+ base_url="https://api.spreadspace.ai", # override for a private deployment
59
+ api_version="2026-05-03", # pins the SpreadSpace-Version header
60
+ timeout=60.0, # seconds, per request
61
+ max_retries=2, # 429 + 5xx + transport errors
62
+ )
63
+ ```
64
+
65
+ ### API version pinning
66
+
67
+ Every request sends a dated `SpreadSpace-Version` header. The SDK pins a default
68
+ version per release (decoupled from the SDK's own semver). Pin it explicitly to
69
+ insulate your integration from server-side changes, and override per call when
70
+ you need a newer surface:
71
+
72
+ ```python
73
+ client.borrowers.list(api_version="2026-06-01") # one call on a newer version
74
+ ```
75
+
76
+ ## Pagination (lazy, auto cursor)
77
+
78
+ List endpoints return a lazy iterator that walks cursors for you — it fetches the
79
+ next page only as you consume it.
80
+
81
+ ```python
82
+ for borrower in client.borrowers.list():
83
+ print(borrower["id"])
84
+
85
+ # Filters pass straight through and persist across pages:
86
+ for job in client.jobs.list(status="completed", limit=50):
87
+ print(job["id"])
88
+ ```
89
+
90
+ ## Async export + wait
91
+
92
+ Exports are asynchronous operations. `create` returns a handle; `wait` polls to
93
+ completion (or raises on timeout).
94
+
95
+ ```python
96
+ op = client.exports.create(borrower_id="...", format="xlsx")
97
+ result = op.wait(timeout=300) # seconds
98
+ print(result["download_url"])
99
+ ```
100
+
101
+ ## Upload a document + wait for processing
102
+
103
+ ```python
104
+ job = client.documents.upload("statement.pdf", borrower_id="...")
105
+ final = job.wait(timeout=600) # seconds
106
+ print(final["status"])
107
+ ```
108
+
109
+ The upload helper requests a presigned URL, PUTs the file bytes directly to
110
+ storage with the matching `Content-Type` (part of the V4 signature), then returns
111
+ a job handle you can `wait` on.
112
+
113
+ ## Error handling
114
+
115
+ All errors derive from `SpreadSpaceError`. Match on the typed subclass, never on
116
+ the message string:
117
+
118
+ ```python
119
+ from spreadspace import (
120
+ SpreadSpaceError, # base
121
+ NetworkError, # transport failure, no HTTP response
122
+ BadRequestError, # 400
123
+ AuthenticationError, # 401
124
+ PermissionDeniedError, # 403
125
+ NotFoundError, # 404
126
+ ConflictError, # 409
127
+ RateLimitError, # 429
128
+ InternalServerError, # 5xx
129
+ )
130
+
131
+ try:
132
+ client.borrowers.get("missing-id")
133
+ except RateLimitError as e:
134
+ print("retry after", e.retry_after, "seconds")
135
+ except NotFoundError as e:
136
+ print("not found")
137
+ except SpreadSpaceError as e:
138
+ # Every error carries request_id — quote it in support tickets.
139
+ print(e.message, e.status_code, e.request_id)
140
+ ```
141
+
142
+ `request_id` comes from the `X-Request-ID` response header (falling back to the
143
+ error body). Transient failures (429, 5xx, transport errors) are retried
144
+ automatically up to `max_retries` with exponential backoff + full jitter,
145
+ honoring `Retry-After`.
146
+
147
+ ## Money is exact
148
+
149
+ Monetary values decode as `decimal.Decimal`, not `float` — the SDK reads the
150
+ literal digits off the wire, so amounts are exact with no float rounding:
151
+
152
+ ```python
153
+ b = client.borrowers.get("...") # [needs sandbox key]
154
+ assert b["total_revenue"] == Decimal("1234.56") # never 1234.5600000000001
155
+ ```
156
+
157
+ ## Development
158
+
159
+ The generated OpenAPI core lives in `src/spreadspace/_generated/` and is built in
160
+ CI (`scripts/generate.sh`, Java-based — not run locally). It is **gitignored and
161
+ must never be hand-edited**. The hand-written ergonomic layer (transport,
162
+ helpers, typed errors) is the only code committed by hand.
163
+
164
+ ```bash
165
+ pip install -e '.[dev]'
166
+ pytest
167
+ ```
168
+
169
+ ## License
170
+
171
+ MIT
@@ -0,0 +1,151 @@
1
+ # SpreadSpace Python SDK
2
+
3
+ Official Python client for the [SpreadSpace API](https://docs.spreadspace.ai) —
4
+ document extraction and financial spreading for lending.
5
+
6
+ > Some examples below require the generated core (built in CI) or a live sandbox
7
+ > key. They are marked **[needs sandbox key]** / **[needs generated core]**.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install spreadspace
13
+ ```
14
+
15
+ Requires Python 3.9+.
16
+
17
+ ## Authentication
18
+
19
+ ```python
20
+ from spreadspace import SpreadSpace
21
+
22
+ client = SpreadSpace(api_key="ss_test_...") # or omit and set SPREADSPACE_API_KEY
23
+ ```
24
+
25
+ The key prefix selects the environment:
26
+
27
+ - `ss_test_...` — routes to your **sandbox tenant** (seed data, safe to experiment).
28
+ - `ss_live_...` — routes to your live tenant (real data).
29
+
30
+ If `api_key` is omitted, the client reads `SPREADSPACE_API_KEY` from the
31
+ environment. Never hard-code a live key; never commit any key.
32
+
33
+ ## Client options
34
+
35
+ ```python
36
+ client = SpreadSpace(
37
+ api_key="ss_test_...",
38
+ base_url="https://api.spreadspace.ai", # override for a private deployment
39
+ api_version="2026-05-03", # pins the SpreadSpace-Version header
40
+ timeout=60.0, # seconds, per request
41
+ max_retries=2, # 429 + 5xx + transport errors
42
+ )
43
+ ```
44
+
45
+ ### API version pinning
46
+
47
+ Every request sends a dated `SpreadSpace-Version` header. The SDK pins a default
48
+ version per release (decoupled from the SDK's own semver). Pin it explicitly to
49
+ insulate your integration from server-side changes, and override per call when
50
+ you need a newer surface:
51
+
52
+ ```python
53
+ client.borrowers.list(api_version="2026-06-01") # one call on a newer version
54
+ ```
55
+
56
+ ## Pagination (lazy, auto cursor)
57
+
58
+ List endpoints return a lazy iterator that walks cursors for you — it fetches the
59
+ next page only as you consume it.
60
+
61
+ ```python
62
+ for borrower in client.borrowers.list():
63
+ print(borrower["id"])
64
+
65
+ # Filters pass straight through and persist across pages:
66
+ for job in client.jobs.list(status="completed", limit=50):
67
+ print(job["id"])
68
+ ```
69
+
70
+ ## Async export + wait
71
+
72
+ Exports are asynchronous operations. `create` returns a handle; `wait` polls to
73
+ completion (or raises on timeout).
74
+
75
+ ```python
76
+ op = client.exports.create(borrower_id="...", format="xlsx")
77
+ result = op.wait(timeout=300) # seconds
78
+ print(result["download_url"])
79
+ ```
80
+
81
+ ## Upload a document + wait for processing
82
+
83
+ ```python
84
+ job = client.documents.upload("statement.pdf", borrower_id="...")
85
+ final = job.wait(timeout=600) # seconds
86
+ print(final["status"])
87
+ ```
88
+
89
+ The upload helper requests a presigned URL, PUTs the file bytes directly to
90
+ storage with the matching `Content-Type` (part of the V4 signature), then returns
91
+ a job handle you can `wait` on.
92
+
93
+ ## Error handling
94
+
95
+ All errors derive from `SpreadSpaceError`. Match on the typed subclass, never on
96
+ the message string:
97
+
98
+ ```python
99
+ from spreadspace import (
100
+ SpreadSpaceError, # base
101
+ NetworkError, # transport failure, no HTTP response
102
+ BadRequestError, # 400
103
+ AuthenticationError, # 401
104
+ PermissionDeniedError, # 403
105
+ NotFoundError, # 404
106
+ ConflictError, # 409
107
+ RateLimitError, # 429
108
+ InternalServerError, # 5xx
109
+ )
110
+
111
+ try:
112
+ client.borrowers.get("missing-id")
113
+ except RateLimitError as e:
114
+ print("retry after", e.retry_after, "seconds")
115
+ except NotFoundError as e:
116
+ print("not found")
117
+ except SpreadSpaceError as e:
118
+ # Every error carries request_id — quote it in support tickets.
119
+ print(e.message, e.status_code, e.request_id)
120
+ ```
121
+
122
+ `request_id` comes from the `X-Request-ID` response header (falling back to the
123
+ error body). Transient failures (429, 5xx, transport errors) are retried
124
+ automatically up to `max_retries` with exponential backoff + full jitter,
125
+ honoring `Retry-After`.
126
+
127
+ ## Money is exact
128
+
129
+ Monetary values decode as `decimal.Decimal`, not `float` — the SDK reads the
130
+ literal digits off the wire, so amounts are exact with no float rounding:
131
+
132
+ ```python
133
+ b = client.borrowers.get("...") # [needs sandbox key]
134
+ assert b["total_revenue"] == Decimal("1234.56") # never 1234.5600000000001
135
+ ```
136
+
137
+ ## Development
138
+
139
+ The generated OpenAPI core lives in `src/spreadspace/_generated/` and is built in
140
+ CI (`scripts/generate.sh`, Java-based — not run locally). It is **gitignored and
141
+ must never be hand-edited**. The hand-written ergonomic layer (transport,
142
+ helpers, typed errors) is the only code committed by hand.
143
+
144
+ ```bash
145
+ pip install -e '.[dev]'
146
+ pytest
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "spreadspace"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for the SpreadSpace API."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "SpreadSpace" }]
13
+ keywords = ["spreadspace", "api", "sdk", "document", "extraction", "lending"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = ["httpx>=0.27,<1"]
20
+
21
+ [project.optional-dependencies]
22
+ dev = ["pytest>=8", "mypy>=1.10"]
23
+
24
+ [project.urls]
25
+ Homepage = "https://spreadspace.ai"
26
+ Documentation = "https://docs.spreadspace.ai"
27
+ Source = "https://github.com/spreadspace"
28
+
29
+ # The generated OpenAPI core lands in src/spreadspace/_generated/ at build time
30
+ # (see scripts/generate.sh). The hand-written ergonomic layer lives alongside it
31
+ # and is the only code committed by hand.
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/spreadspace"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ addopts = "-q"
38
+
39
+ [tool.mypy]
40
+ python_version = "3.9"
41
+ strict = true
42
+ files = ["src/spreadspace"]
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ # Regenerate the Python SDK's generated OpenAPI core.
3
+ #
4
+ # Self-hosted pipeline (no paid SDK vendor): we drive OpenAPI Generator directly.
5
+ # The hand-written ergonomic layer (transport, helpers, typed errors) lives in
6
+ # src/spreadspace/ and is committed; the machine-generated models + low-level
7
+ # api client land in src/spreadspace/_generated/ and are GITIGNORED — they are
8
+ # regenerated from the spec every run and must NEVER be hand-edited.
9
+ #
10
+ # Java is required (OpenAPI Generator is a JVM tool). Local dev has no JRE, so
11
+ # this is CI-only — see .github/workflows/sdk.yml. Author/verify here, run there.
12
+ #
13
+ # Idempotent + safe to re-run: it rebuilds the public spec, re-validates, and
14
+ # overwrites _generated/ from scratch each time.
15
+ set -euo pipefail
16
+
17
+ # -- pinned versions --------------------------------------------------------
18
+ # CLI wrapper (npx) and the generator JAR are pinned to EXACT versions so the
19
+ # generated output is reproducible across CI runs and machines.
20
+ OPENAPI_GENERATOR_CLI_VERSION="2.20.0" # @openapitools/openapi-generator-cli (npm wrapper)
21
+ OPENAPI_GENERATOR_VERSION="${OPENAPI_GENERATOR_VERSION:-7.13.0}" # the generator JAR (recent 7.x)
22
+ export OPENAPI_GENERATOR_VERSION
23
+
24
+ # Generator target. NOTE: the exact `-g` name MUST be confirmed against the
25
+ # pinned JAR with `npx @openapitools/openapi-generator-cli@${OPENAPI_GENERATOR_CLI_VERSION} list`
26
+ # before trusting it — do NOT assume. In 7.x the maintained Python generator is
27
+ # `python` (the legacy `python-legacy` is separate). We pin `python`.
28
+ GENERATOR_NAME="python"
29
+
30
+ # -- paths ------------------------------------------------------------------
31
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
+ SDK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" # sdk/python
33
+ REPO_ROOT="$(cd "${SDK_DIR}/../.." && pwd)" # repo root
34
+ PUBLIC_SPEC="${REPO_ROOT}/fern/api-public.json"
35
+ OUT_DIR="${SDK_DIR}/src/spreadspace/_generated"
36
+
37
+ OPENAPI_CLI="npx --yes @openapitools/openapi-generator-cli@${OPENAPI_GENERATOR_CLI_VERSION}"
38
+
39
+ echo "==> Refreshing public spec via fern/build-public-spec.sh"
40
+ # Reuse the ONE shared spec producer: strips /api/admin, /api/internal,
41
+ # /api/portfolios + localhost servers, writes fern/api-public.json. Needs jq + python3.
42
+ bash "${REPO_ROOT}/fern/build-public-spec.sh"
43
+
44
+ echo "==> Validating spec"
45
+ ${OPENAPI_CLI} validate -i "${PUBLIC_SPEC}"
46
+
47
+ echo "==> Generating Python core into ${OUT_DIR}"
48
+ # --package-name spreadspace._generated keeps the generated package nested under
49
+ # the hand-written `spreadspace` package, so it can never collide with it. But
50
+ # the generator writes the package as a PATH under -o (i.e. <out>/spreadspace/
51
+ # _generated/), so we generate into a throwaway stage dir and move just the
52
+ # generated subtree into place — never touching the hand-written src/spreadspace.
53
+ # library=urllib3: the generated low-level client uses urllib3 (the default,
54
+ # zero extra deps in the generated tree). Our hand-written transport is httpx;
55
+ # the generated client is the typed-model + raw-call substrate the ergonomic
56
+ # layer sits on top of — we don't ship its HTTP path on the hot calls.
57
+ STAGE="$(mktemp -d)"
58
+ trap 'rm -rf "${STAGE}"' EXIT
59
+ ${OPENAPI_CLI} generate \
60
+ -i "${PUBLIC_SPEC}" \
61
+ -g "${GENERATOR_NAME}" \
62
+ -o "${STAGE}" \
63
+ --package-name spreadspace._generated \
64
+ --additional-properties=library=urllib3,packageVersion=0.1.0,generateSourceCodeOnly=true
65
+
66
+ # Wipe first so removed models/operations don't linger (idempotent regen).
67
+ rm -rf "${OUT_DIR}"
68
+ mkdir -p "$(dirname "${OUT_DIR}")"
69
+ mv "${STAGE}/spreadspace/_generated" "${OUT_DIR}"
70
+
71
+ echo "==> Done."
72
+ echo "NOTE: ${OUT_DIR} is gitignored (see sdk/python/.gitignore) and is"
73
+ echo " regenerated from the spec on every run — NEVER hand-edit it."
@@ -0,0 +1,75 @@
1
+ """SpreadSpace Python SDK.
2
+
3
+ from spreadspace import SpreadSpace
4
+ client = SpreadSpace(api_key="ss_test_...")
5
+ for borrower in client.borrowers.list():
6
+ ...
7
+ """
8
+
9
+ from ._version import DEFAULT_API_VERSION, SDK_VERSION
10
+ from .client import SpreadSpace
11
+ from .errors import (
12
+ APIStatusError,
13
+ AuthenticationError,
14
+ BadRequestError,
15
+ ConflictError,
16
+ InternalServerError,
17
+ NetworkError,
18
+ NotFoundError,
19
+ PermissionDeniedError,
20
+ RateLimitError,
21
+ SpreadSpaceError,
22
+ )
23
+ from .helpers.operations import (
24
+ AsyncOperation,
25
+ AsyncOperationError,
26
+ AsyncOperationHandle,
27
+ AsyncOperationTimeout,
28
+ )
29
+ from .helpers.pagination import PageIterator
30
+ from .helpers.upload import JobHandle, PresignedUrl, UploadError, UploadTimeout
31
+ from .webhooks import (
32
+ DEFAULT_FRESHNESS_TOLERANCE_SECONDS,
33
+ SIGNATURE_HEADER_NAME,
34
+ WEBHOOK_EVENT_TYPES,
35
+ WebhookEvent,
36
+ WebhookSignatureError,
37
+ verify_and_parse_webhook,
38
+ verify_webhook_signature,
39
+ )
40
+
41
+ __all__ = [
42
+ # client + version
43
+ "SpreadSpace",
44
+ "SDK_VERSION",
45
+ "DEFAULT_API_VERSION",
46
+ # errors
47
+ "SpreadSpaceError",
48
+ "NetworkError",
49
+ "APIStatusError",
50
+ "BadRequestError",
51
+ "AuthenticationError",
52
+ "PermissionDeniedError",
53
+ "NotFoundError",
54
+ "ConflictError",
55
+ "RateLimitError",
56
+ "InternalServerError",
57
+ # helper types
58
+ "PageIterator",
59
+ "AsyncOperation",
60
+ "AsyncOperationHandle",
61
+ "AsyncOperationError",
62
+ "AsyncOperationTimeout",
63
+ "JobHandle",
64
+ "PresignedUrl",
65
+ "UploadError",
66
+ "UploadTimeout",
67
+ # webhooks
68
+ "verify_webhook_signature",
69
+ "verify_and_parse_webhook",
70
+ "WebhookSignatureError",
71
+ "WebhookEvent",
72
+ "WEBHOOK_EVENT_TYPES",
73
+ "SIGNATURE_HEADER_NAME",
74
+ "DEFAULT_FRESHNESS_TOLERANCE_SECONDS",
75
+ ]