smklog-sdk 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dmitrii T
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,172 @@
1
+ Metadata-Version: 2.4
2
+ Name: smklog-sdk
3
+ Version: 0.1.0
4
+ Summary: Official Python client and command line for the SMKlog Quote API: live USPS, UPS, FedEx and DHL parcel rates from a US origin, a checkout handed to a human, and where it stands.
5
+ Author-email: SMKlog <info@smklog.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://smklog.com/developers/
8
+ Project-URL: Documentation, https://smklog.com/developers/
9
+ Project-URL: Repository, https://github.com/smklog/smklog-sdk
10
+ Project-URL: Changelog, https://github.com/smklog/smklog-sdk/releases
11
+ Project-URL: OpenAPI, https://quote-api.smklog.com/openapi.json
12
+ Project-URL: Status page, https://smklog.com/developers/status
13
+ Keywords: smklog,shipping,parcel,rates,usps,ups,fedex,dhl,labels
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP
25
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Dynamic: license-file
31
+
32
+ # smklog-sdk (Python)
33
+
34
+ Official Python client and command line for the [SMKlog Quote API](https://smklog.com/developers/):
35
+ live USPS, UPS, FedEx and DHL Express parcel rates from a US origin, a checkout handed
36
+ to a human, and where that checkout stands. No API key, no account, no dependencies
37
+ beyond the standard library. Python 3.9 or newer.
38
+
39
+ ```
40
+ pip install smklog-sdk
41
+ ```
42
+
43
+ The distribution is `smklog-sdk`; the import package is `smklog`.
44
+
45
+ ## Quickstart
46
+
47
+ ```python
48
+ from smklog import SmklogClient
49
+
50
+ client = SmklogClient()
51
+ quote = client.get_parcel_quote(product="Yamaha FG800 acoustic guitar in a hard case",
52
+ from_postal_code="07102", to_postal_code="30303")
53
+ for rate in quote.rates: # best value first, as the API returns them
54
+ print(rate.carrier, rate.service, rate.amount, "counter", rate.retail_amount)
55
+ if quote.is_priced() and quote.quote_id: # the handle is good for 15 minutes
56
+ best = quote.cheapest()
57
+ session = client.create_payment_session(quote_id=quote.quote_id, service=best.service)
58
+ print(session.handoff_url) # the human confirms and pays here
59
+ if session.session_id: # None only when the record was not stored
60
+ print(client.get_checkout_status(session.session_id).status)
61
+ ```
62
+
63
+ The pair costs one carrier call: the quote answer carries `quote_id`, and a checkout
64
+ opened with it reuses those rates (`session.quote_reused` is true) instead of pricing
65
+ the shipment again.
66
+
67
+ Exact measurements skip the box estimate: pass all four of `length_cm`, `width_cm`,
68
+ `height_cm`, `weight_kg` (the client marks the package `manual`). A request can also be a
69
+ `QuoteRequest` / `CheckoutSessionRequest` model or a plain dict; fields keep the wire names.
70
+
71
+ Every `get_parcel_quote` call spends a live carrier rating on the server: one call per
72
+ shipment, cache on your side, never poll. `create_payment_session` takes one of two forms:
73
+
74
+ - `create_payment_session(quote_id=..., service=...)`: the quote is reused and no second
75
+ carrier call is spent. `quote_id` is the handle every priced quote answer carries; it is
76
+ `None` when nothing was priced or the handle could not be stored. The API ignores any
77
+ shipment fields sent next to a valid `quote_id`.
78
+ - `create_payment_session(product=..., from_zip=..., to_zip=..., to_country=..., quantity=...,
79
+ service=...)`: the API prices the shipment live and opens the checkout in one call.
80
+
81
+ A `quote_id` older than 15 minutes makes the API price the shipment fields live, so send
82
+ them along when the quote may have aged. The client never turns one form into the other.
83
+
84
+ ## Client options
85
+
86
+ | Option | Default | Meaning |
87
+ | --- | --- | --- |
88
+ | `base_url` | `https://quote-api.smklog.com` | The API host. |
89
+ | `timeout` | `30.0` | Seconds per request; a quote can take 10-20 s while carriers answer. |
90
+ | `token` | `None` | Optional OAuth bearer for a dedicated rate-limit bucket ([auth.md](https://smklog.com/auth.md)); sent as `Authorization: Bearer ...` only when set. |
91
+ | `user_agent` | `None` | Suffix appended after `smklog-sdk-python/0.1.0`. |
92
+
93
+ Operations: `get_parcel_quote(...)` -> `QuoteResponse` (with `quote_id`, `rates`,
94
+ `cheapest()`, `is_priced()`), `create_payment_session(...)` -> `CheckoutSession`,
95
+ `get_checkout_status(session_id)` -> `CheckoutStatus`, `status()` -> `Liveness`.
96
+
97
+ Every model is a dataclass whose fields are the wire names. Fields this version does
98
+ not know about land in `.extra`; `.raw` is the body exactly as received; `.to_dict()`
99
+ serializes back. Only the idempotent GETs (`get_checkout_status`, `status`) are retried,
100
+ at most twice, on network failures and 5xx. POSTs are never retried, and a 429 is never
101
+ retried anywhere: the buckets are hourly (about 80 quotes, 60 status reads, 20 token
102
+ requests per client), so a retry loop only burns them faster.
103
+
104
+ ## Errors
105
+
106
+ Every failure raises `SmklogError` or one of its subclasses, carrying `status` (HTTP
107
+ status, 0 when nothing was sent), `error` (the stable code), `message`, `missing`
108
+ (field names, often empty) and `body` (the response body as received). When the Problem
109
+ carries no `message`, the error's message is the code plus the missing fields
110
+ (`missing_required_fields: missing to_postal_code`). Errors pickle, so they cross a
111
+ `multiprocessing` boundary intact.
112
+
113
+ | Code | Exception |
114
+ | --- | --- |
115
+ | `rate_limited`, `slow_down` | `RateLimitedError` |
116
+ | `product_not_recognized`, `product_url_not_detected`, `missing_required_fields`, `invalid_request`, `invalid_json` | `InvalidRequestError` |
117
+ | `session_not_found` | `NotFoundError` |
118
+ | `session_failed` | `SessionFailedError` |
119
+ | anything else | `SmklogError` |
120
+
121
+ A network failure or timeout is `SmklogError` with `status == 0` and
122
+ `error == "network_error"`; the native exception is its `__cause__`. Requests the client
123
+ refuses before sending (a 4-digit ZIP, a country outside US/CA/GB/DE/AU, a quantity
124
+ below 1, measurements that do not come as a set of four) raise `InvalidRequestError`
125
+ with `status == 0`.
126
+
127
+ ## Command line
128
+
129
+ ```
130
+ smklog quote "Yamaha FG800 acoustic guitar in a hard case" 07102 30303 [--country US] [--qty 1] [--dims 112x48x20 --kg 6.4] [--json]
131
+ smklog session --quote-id q_... [--service "USPS Ground Advantage"] [--json]
132
+ smklog session-status as_... [--json]
133
+ smklog status [--json]
134
+ ```
135
+
136
+ Human output is a small table; `--json` prints the raw answer. A priced `smklog quote`
137
+ also prints the `quote_id` on its own line, ready to pass to `smklog session --quote-id`
138
+ within 15 minutes (`--json` carries it in the body):
139
+
140
+ ```
141
+ $ smklog quote "Yamaha FG800 acoustic guitar in a hard case" 07102 30303
142
+ Yamaha FG800 acoustic guitar in a hard case
143
+ Box: 112 x 48 x 20 cm, 6.4 kg (estimate, medium confidence)
144
+ Carrier Service Checkout Counter Delivery
145
+ USPS Ground Advantage $38.42 $46.95 2-5 business days
146
+ UPS Ground $41.10 $67.35 3 days, by 2026-10-16
147
+ quote_id q_8f3c2e1a4b7d
148
+ $ smklog session --quote-id q_8f3c2e1a4b7d --service "USPS Ground Advantage"
149
+ ```
150
+
151
+ Exit code 0 on a priced answer or a successful read, 2 when the API refuses (the Problem
152
+ is printed) or answers a quote without a price, 3 on a rate limit, 1 when the host could
153
+ not be reached.
154
+ `--base-url` and `--token` (or `SMKLOG_BASE_URL` / `SMKLOG_TOKEN`) are accepted by every
155
+ command. `python -m smklog` runs the same program.
156
+
157
+ ## Development
158
+
159
+ ```
160
+ cd python
161
+ python3 -m unittest discover -s tests
162
+ ```
163
+
164
+ The tests replay the vendored examples in `../testdata/` over a local HTTP server and
165
+ never reach the network. `SMKLOG_LIVE=1` adds one live test that calls `status()` on
166
+ production; nothing in the suite ever requests a quote.
167
+
168
+ Docs, schemas, playground and status page: [smklog.com/developers](https://smklog.com/developers/).
169
+ The contract is the OpenAPI 3.1 document at
170
+ [quote-api.smklog.com/openapi.json](https://quote-api.smklog.com/openapi.json).
171
+
172
+ MIT license; see [LICENSE](LICENSE).
@@ -0,0 +1,141 @@
1
+ # smklog-sdk (Python)
2
+
3
+ Official Python client and command line for the [SMKlog Quote API](https://smklog.com/developers/):
4
+ live USPS, UPS, FedEx and DHL Express parcel rates from a US origin, a checkout handed
5
+ to a human, and where that checkout stands. No API key, no account, no dependencies
6
+ beyond the standard library. Python 3.9 or newer.
7
+
8
+ ```
9
+ pip install smklog-sdk
10
+ ```
11
+
12
+ The distribution is `smklog-sdk`; the import package is `smklog`.
13
+
14
+ ## Quickstart
15
+
16
+ ```python
17
+ from smklog import SmklogClient
18
+
19
+ client = SmklogClient()
20
+ quote = client.get_parcel_quote(product="Yamaha FG800 acoustic guitar in a hard case",
21
+ from_postal_code="07102", to_postal_code="30303")
22
+ for rate in quote.rates: # best value first, as the API returns them
23
+ print(rate.carrier, rate.service, rate.amount, "counter", rate.retail_amount)
24
+ if quote.is_priced() and quote.quote_id: # the handle is good for 15 minutes
25
+ best = quote.cheapest()
26
+ session = client.create_payment_session(quote_id=quote.quote_id, service=best.service)
27
+ print(session.handoff_url) # the human confirms and pays here
28
+ if session.session_id: # None only when the record was not stored
29
+ print(client.get_checkout_status(session.session_id).status)
30
+ ```
31
+
32
+ The pair costs one carrier call: the quote answer carries `quote_id`, and a checkout
33
+ opened with it reuses those rates (`session.quote_reused` is true) instead of pricing
34
+ the shipment again.
35
+
36
+ Exact measurements skip the box estimate: pass all four of `length_cm`, `width_cm`,
37
+ `height_cm`, `weight_kg` (the client marks the package `manual`). A request can also be a
38
+ `QuoteRequest` / `CheckoutSessionRequest` model or a plain dict; fields keep the wire names.
39
+
40
+ Every `get_parcel_quote` call spends a live carrier rating on the server: one call per
41
+ shipment, cache on your side, never poll. `create_payment_session` takes one of two forms:
42
+
43
+ - `create_payment_session(quote_id=..., service=...)`: the quote is reused and no second
44
+ carrier call is spent. `quote_id` is the handle every priced quote answer carries; it is
45
+ `None` when nothing was priced or the handle could not be stored. The API ignores any
46
+ shipment fields sent next to a valid `quote_id`.
47
+ - `create_payment_session(product=..., from_zip=..., to_zip=..., to_country=..., quantity=...,
48
+ service=...)`: the API prices the shipment live and opens the checkout in one call.
49
+
50
+ A `quote_id` older than 15 minutes makes the API price the shipment fields live, so send
51
+ them along when the quote may have aged. The client never turns one form into the other.
52
+
53
+ ## Client options
54
+
55
+ | Option | Default | Meaning |
56
+ | --- | --- | --- |
57
+ | `base_url` | `https://quote-api.smklog.com` | The API host. |
58
+ | `timeout` | `30.0` | Seconds per request; a quote can take 10-20 s while carriers answer. |
59
+ | `token` | `None` | Optional OAuth bearer for a dedicated rate-limit bucket ([auth.md](https://smklog.com/auth.md)); sent as `Authorization: Bearer ...` only when set. |
60
+ | `user_agent` | `None` | Suffix appended after `smklog-sdk-python/0.1.0`. |
61
+
62
+ Operations: `get_parcel_quote(...)` -> `QuoteResponse` (with `quote_id`, `rates`,
63
+ `cheapest()`, `is_priced()`), `create_payment_session(...)` -> `CheckoutSession`,
64
+ `get_checkout_status(session_id)` -> `CheckoutStatus`, `status()` -> `Liveness`.
65
+
66
+ Every model is a dataclass whose fields are the wire names. Fields this version does
67
+ not know about land in `.extra`; `.raw` is the body exactly as received; `.to_dict()`
68
+ serializes back. Only the idempotent GETs (`get_checkout_status`, `status`) are retried,
69
+ at most twice, on network failures and 5xx. POSTs are never retried, and a 429 is never
70
+ retried anywhere: the buckets are hourly (about 80 quotes, 60 status reads, 20 token
71
+ requests per client), so a retry loop only burns them faster.
72
+
73
+ ## Errors
74
+
75
+ Every failure raises `SmklogError` or one of its subclasses, carrying `status` (HTTP
76
+ status, 0 when nothing was sent), `error` (the stable code), `message`, `missing`
77
+ (field names, often empty) and `body` (the response body as received). When the Problem
78
+ carries no `message`, the error's message is the code plus the missing fields
79
+ (`missing_required_fields: missing to_postal_code`). Errors pickle, so they cross a
80
+ `multiprocessing` boundary intact.
81
+
82
+ | Code | Exception |
83
+ | --- | --- |
84
+ | `rate_limited`, `slow_down` | `RateLimitedError` |
85
+ | `product_not_recognized`, `product_url_not_detected`, `missing_required_fields`, `invalid_request`, `invalid_json` | `InvalidRequestError` |
86
+ | `session_not_found` | `NotFoundError` |
87
+ | `session_failed` | `SessionFailedError` |
88
+ | anything else | `SmklogError` |
89
+
90
+ A network failure or timeout is `SmklogError` with `status == 0` and
91
+ `error == "network_error"`; the native exception is its `__cause__`. Requests the client
92
+ refuses before sending (a 4-digit ZIP, a country outside US/CA/GB/DE/AU, a quantity
93
+ below 1, measurements that do not come as a set of four) raise `InvalidRequestError`
94
+ with `status == 0`.
95
+
96
+ ## Command line
97
+
98
+ ```
99
+ smklog quote "Yamaha FG800 acoustic guitar in a hard case" 07102 30303 [--country US] [--qty 1] [--dims 112x48x20 --kg 6.4] [--json]
100
+ smklog session --quote-id q_... [--service "USPS Ground Advantage"] [--json]
101
+ smklog session-status as_... [--json]
102
+ smklog status [--json]
103
+ ```
104
+
105
+ Human output is a small table; `--json` prints the raw answer. A priced `smklog quote`
106
+ also prints the `quote_id` on its own line, ready to pass to `smklog session --quote-id`
107
+ within 15 minutes (`--json` carries it in the body):
108
+
109
+ ```
110
+ $ smklog quote "Yamaha FG800 acoustic guitar in a hard case" 07102 30303
111
+ Yamaha FG800 acoustic guitar in a hard case
112
+ Box: 112 x 48 x 20 cm, 6.4 kg (estimate, medium confidence)
113
+ Carrier Service Checkout Counter Delivery
114
+ USPS Ground Advantage $38.42 $46.95 2-5 business days
115
+ UPS Ground $41.10 $67.35 3 days, by 2026-10-16
116
+ quote_id q_8f3c2e1a4b7d
117
+ $ smklog session --quote-id q_8f3c2e1a4b7d --service "USPS Ground Advantage"
118
+ ```
119
+
120
+ Exit code 0 on a priced answer or a successful read, 2 when the API refuses (the Problem
121
+ is printed) or answers a quote without a price, 3 on a rate limit, 1 when the host could
122
+ not be reached.
123
+ `--base-url` and `--token` (or `SMKLOG_BASE_URL` / `SMKLOG_TOKEN`) are accepted by every
124
+ command. `python -m smklog` runs the same program.
125
+
126
+ ## Development
127
+
128
+ ```
129
+ cd python
130
+ python3 -m unittest discover -s tests
131
+ ```
132
+
133
+ The tests replay the vendored examples in `../testdata/` over a local HTTP server and
134
+ never reach the network. `SMKLOG_LIVE=1` adds one live test that calls `status()` on
135
+ production; nothing in the suite ever requests a quote.
136
+
137
+ Docs, schemas, playground and status page: [smklog.com/developers](https://smklog.com/developers/).
138
+ The contract is the OpenAPI 3.1 document at
139
+ [quote-api.smklog.com/openapi.json](https://quote-api.smklog.com/openapi.json).
140
+
141
+ MIT license; see [LICENSE](LICENSE).
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "smklog-sdk"
7
+ version = "0.1.0"
8
+ description = "Official Python client and command line for the SMKlog Quote API: live USPS, UPS, FedEx and DHL parcel rates from a US origin, a checkout handed to a human, and where it stands."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.9"
13
+ authors = [{ name = "SMKlog", email = "info@smklog.com" }]
14
+ keywords = ["smklog", "shipping", "parcel", "rates", "usps", "ups", "fedex", "dhl", "labels"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3 :: Only",
21
+ "Programming Language :: Python :: 3.9",
22
+ "Programming Language :: Python :: 3.10",
23
+ "Programming Language :: Python :: 3.11",
24
+ "Programming Language :: Python :: 3.12",
25
+ "Programming Language :: Python :: 3.13",
26
+ "Topic :: Internet :: WWW/HTTP",
27
+ "Topic :: Software Development :: Libraries :: Python Modules",
28
+ "Typing :: Typed",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.urls]
33
+ Homepage = "https://smklog.com/developers/"
34
+ Documentation = "https://smklog.com/developers/"
35
+ Repository = "https://github.com/smklog/smklog-sdk"
36
+ Changelog = "https://github.com/smklog/smklog-sdk/releases"
37
+ "OpenAPI" = "https://quote-api.smklog.com/openapi.json"
38
+ "Status page" = "https://smklog.com/developers/status"
39
+
40
+ [project.scripts]
41
+ smklog = "smklog.cli:main"
42
+
43
+ [tool.setuptools.package-data]
44
+ smklog = ["py.typed"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,50 @@
1
+ """Official Python client for the SMKlog Quote API: https://smklog.com/developers/
2
+
3
+ from smklog import SmklogClient
4
+ quote = SmklogClient().get_parcel_quote(product="paperback books",
5
+ from_postal_code="07030", to_postal_code="33101")
6
+ """
7
+
8
+ from ._version import __version__
9
+ from .client import DEFAULT_BASE_URL, DEFAULT_TIMEOUT, SmklogClient
10
+ from .errors import (
11
+ InvalidRequestError,
12
+ NotFoundError,
13
+ RateLimitedError,
14
+ SessionFailedError,
15
+ SmklogError,
16
+ )
17
+ from .models import (
18
+ Checkout,
19
+ CheckoutSession,
20
+ CheckoutSessionRequest,
21
+ CheckoutStatus,
22
+ Liveness,
23
+ Package,
24
+ Problem,
25
+ QuoteRequest,
26
+ QuoteResponse,
27
+ Rate,
28
+ )
29
+
30
+ __all__ = [
31
+ "__version__",
32
+ "DEFAULT_BASE_URL",
33
+ "DEFAULT_TIMEOUT",
34
+ "SmklogClient",
35
+ "SmklogError",
36
+ "RateLimitedError",
37
+ "InvalidRequestError",
38
+ "NotFoundError",
39
+ "SessionFailedError",
40
+ "QuoteRequest",
41
+ "QuoteResponse",
42
+ "Rate",
43
+ "Package",
44
+ "CheckoutSessionRequest",
45
+ "CheckoutSession",
46
+ "Checkout",
47
+ "CheckoutStatus",
48
+ "Liveness",
49
+ "Problem",
50
+ ]
@@ -0,0 +1,7 @@
1
+ """Lets ``python -m smklog`` behave like the ``smklog`` console script."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ sys.exit(main())
@@ -0,0 +1,3 @@
1
+ # Kept in its own module so both the package root and the client's User-Agent
2
+ # can read it without importing each other.
3
+ __version__ = "0.1.0"