authsec-sdk 4.0.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.
- authsec_sdk-4.0.0/MANIFEST.in +3 -0
- authsec_sdk-4.0.0/PKG-INFO +415 -0
- authsec_sdk-4.0.0/README.md +400 -0
- authsec_sdk-4.0.0/pyproject.toml +33 -0
- authsec_sdk-4.0.0/setup.cfg +4 -0
- authsec_sdk-4.0.0/setup.py +3 -0
- authsec_sdk-4.0.0/src/authsec_sdk/__init__.py +44 -0
- authsec_sdk-4.0.0/src/authsec_sdk/ciba_sdk.py +196 -0
- authsec_sdk-4.0.0/src/authsec_sdk/core.py +1049 -0
- authsec_sdk-4.0.0/src/authsec_sdk/spiffe_workload_api/__init__.py +31 -0
- authsec_sdk-4.0.0/src/authsec_sdk/spiffe_workload_api/api/__init__.py +9 -0
- authsec_sdk-4.0.0/src/authsec_sdk/spiffe_workload_api/api/workload.proto +126 -0
- authsec_sdk-4.0.0/src/authsec_sdk/spiffe_workload_api/api/workload_pb2.py +81 -0
- authsec_sdk-4.0.0/src/authsec_sdk/spiffe_workload_api/api/workload_pb2_grpc.py +278 -0
- authsec_sdk-4.0.0/src/authsec_sdk/spiffe_workload_api/client.py +437 -0
- authsec_sdk-4.0.0/src/authsec_sdk/spiffe_workload_api/simple.py +248 -0
- authsec_sdk-4.0.0/src/authsec_sdk/spire_sdk.py +368 -0
- authsec_sdk-4.0.0/src/authsec_sdk.egg-info/PKG-INFO +415 -0
- authsec_sdk-4.0.0/src/authsec_sdk.egg-info/SOURCES.txt +20 -0
- authsec_sdk-4.0.0/src/authsec_sdk.egg-info/dependency_links.txt +1 -0
- authsec_sdk-4.0.0/src/authsec_sdk.egg-info/requires.txt +6 -0
- authsec_sdk-4.0.0/src/authsec_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authsec-sdk
|
|
3
|
+
Version: 4.0.0
|
|
4
|
+
Summary: AuthSec SDK for MCP auth, services, CIBA, and SPIFFE integration
|
|
5
|
+
Author-email: AuthSec Team <a@authnull.com>
|
|
6
|
+
Keywords: authsec,mcp,oauth,rbac,spiffe,ciba,authentication,authorization
|
|
7
|
+
Requires-Python: >=3.10.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
10
|
+
Requires-Dist: fastapi>=0.110.0
|
|
11
|
+
Requires-Dist: grpcio>=1.60.0
|
|
12
|
+
Requires-Dist: protobuf<6.0.0,>=5.29.0
|
|
13
|
+
Requires-Dist: requests>=2.31.0
|
|
14
|
+
Requires-Dist: uvicorn>=0.27.0
|
|
15
|
+
|
|
16
|
+
# AuthSec Python SDK (`authsec_sdk`)
|
|
17
|
+
|
|
18
|
+
Add OAuth + RBAC protection to MCP tools with minimal code changes.
|
|
19
|
+
|
|
20
|
+
This SDK lets you:
|
|
21
|
+
- Protect MCP tools with `@protected_by_AuthSec(...)`
|
|
22
|
+
- Register normal unprotected MCP tools with `@mcp_tool(...)`
|
|
23
|
+
- Run an MCP server that delegates auth/tool listing to AuthSec SDK Manager
|
|
24
|
+
- Access downstream service credentials safely through `ServiceAccessSDK`
|
|
25
|
+
|
|
26
|
+
## What This SDK Does (and Why)
|
|
27
|
+
|
|
28
|
+
AuthSec is the **control plane**: users authenticate through AuthSec, show up in the AuthSec web app, and you assign them roles/permissions and conditional access policies.
|
|
29
|
+
|
|
30
|
+
This Python package is the **enforcement layer for MCP**:
|
|
31
|
+
- It provides a minimal HTTP MCP server (`run_mcp_server_with_oauth(...)`) so you do not need to implement MCP transport + OAuth bootstrap tools yourself.
|
|
32
|
+
- It delegates OAuth flows and authorization decisions to the hosted AuthSec SDK Manager.
|
|
33
|
+
- It enforces allow/deny for protected tools at **call time** (the security boundary), and can also hide tools in `tools/list` as a UX improvement (policy controlled by AuthSec).
|
|
34
|
+
|
|
35
|
+
Why this approach:
|
|
36
|
+
- B2B teams often have many internal tools. You configure identity + RBAC once in AuthSec, then every MCP tool can inherit the same enterprise policy without re-implementing Okta/SAML/SCIM in each server.
|
|
37
|
+
- The SDK stays thin: your business logic stays in your tool handlers; AuthSec decides who can call them.
|
|
38
|
+
|
|
39
|
+
## Package
|
|
40
|
+
|
|
41
|
+
- Name: `authsec-sdk`
|
|
42
|
+
- Import path: `authsec_sdk`
|
|
43
|
+
- Python: `>=3.10.11`
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
### One command on macOS
|
|
48
|
+
|
|
49
|
+
From the repo:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bash scripts/bootstrap-python-sdk-macos.sh --client-id YOUR_CLIENT_ID
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Using GitHub raw:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
curl -fsSL https://raw.githubusercontent.com/authsec-ai/sdk-authsec/main/scripts/bootstrap-python-sdk-macos.sh | bash -s -- --client-id YOUR_CLIENT_ID
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Notes:
|
|
62
|
+
- Default install source is `pypi`.
|
|
63
|
+
- Use `--source https` or `--source ssh` if you want to install directly from GitHub.
|
|
64
|
+
- Add `--run` to start the sample server immediately after setup.
|
|
65
|
+
|
|
66
|
+
### Local development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cd packages/python-sdk
|
|
70
|
+
python3 -m pip install -e .
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### From GitHub (advanced)
|
|
74
|
+
|
|
75
|
+
If you already cloned this repo, the shortest path is an editable install:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python3 -m pip install -e packages/python-sdk
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If you need a one-command setup on macOS, use the bootstrap script above.
|
|
82
|
+
|
|
83
|
+
<details>
|
|
84
|
+
<summary>Direct pip install from GitHub monorepo (long command)</summary>
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
python3 -m venv .venv
|
|
88
|
+
source .venv/bin/activate
|
|
89
|
+
python3 -m pip install --upgrade pip
|
|
90
|
+
python3 -m pip install "git+ssh://git@github.com/authsec-ai/sdk-authsec.git@main#subdirectory=packages/python-sdk"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
If you prefer HTTPS:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python3 -m pip install "git+https://github.com/authsec-ai/sdk-authsec.git@main#subdirectory=packages/python-sdk"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Windows PowerShell
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
py -3 -m venv .venv
|
|
103
|
+
.venv\Scripts\Activate.ps1
|
|
104
|
+
py -3 -m pip install --upgrade pip
|
|
105
|
+
py -3 -m pip install "git+https://github.com/authsec-ai/sdk-authsec.git@main#subdirectory=packages/python-sdk"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
If your org uses GitHub SSH on Windows:
|
|
109
|
+
|
|
110
|
+
```powershell
|
|
111
|
+
py -3 -m pip install "git+ssh://git@github.com/authsec-ai/sdk-authsec.git@main#subdirectory=packages/python-sdk"
|
|
112
|
+
```
|
|
113
|
+
</details>
|
|
114
|
+
|
|
115
|
+
### From package index
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
python3 -m pip install authsec-sdk
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### Windows PowerShell
|
|
122
|
+
|
|
123
|
+
```powershell
|
|
124
|
+
py -3 -m pip install authsec-sdk
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Verify install
|
|
128
|
+
|
|
129
|
+
#### macOS / Linux
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
python3 -c "import authsec_sdk; print(authsec_sdk.__version__)"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Windows PowerShell
|
|
136
|
+
|
|
137
|
+
```powershell
|
|
138
|
+
py -3 -c "import authsec_sdk; print(authsec_sdk.__version__)"
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Quick Start
|
|
142
|
+
|
|
143
|
+
Create `server.py`:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from authsec_sdk import mcp_tool, protected_by_AuthSec, run_mcp_server_with_oauth
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@mcp_tool(
|
|
150
|
+
name="ping",
|
|
151
|
+
description="Health check tool",
|
|
152
|
+
inputSchema={
|
|
153
|
+
"type": "object",
|
|
154
|
+
"properties": {},
|
|
155
|
+
"required": []
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
async def ping(arguments: dict) -> list:
|
|
159
|
+
return [{"type": "text", "text": "pong"}]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@protected_by_AuthSec(
|
|
163
|
+
tool_name="delete_invoice",
|
|
164
|
+
roles=["admin"],
|
|
165
|
+
scopes=["write"],
|
|
166
|
+
require_all=True,
|
|
167
|
+
description="Delete invoice by id",
|
|
168
|
+
inputSchema={
|
|
169
|
+
"type": "object",
|
|
170
|
+
"properties": {
|
|
171
|
+
"invoice_id": {"type": "string"},
|
|
172
|
+
"session_id": {"type": "string"}
|
|
173
|
+
},
|
|
174
|
+
"required": ["invoice_id"]
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
async def delete_invoice(arguments: dict) -> list:
|
|
178
|
+
invoice_id = arguments.get("invoice_id")
|
|
179
|
+
user = (arguments.get("_user_info") or {}).get("email_id", "unknown-user")
|
|
180
|
+
return [{"type": "text", "text": f"Deleted invoice {invoice_id} by {user}"}]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
import __main__
|
|
185
|
+
|
|
186
|
+
run_mcp_server_with_oauth(
|
|
187
|
+
user_module=__main__,
|
|
188
|
+
client_id="YOUR_CLIENT_ID",
|
|
189
|
+
app_name="my-python-mcp",
|
|
190
|
+
host="127.0.0.1",
|
|
191
|
+
port=3005,
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Run on macOS / Linux:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
AUTHSEC_AUTH_SERVICE_URL="https://dev.api.authsec.dev/sdkmgr/mcp-auth" \
|
|
199
|
+
AUTHSEC_SERVICES_URL="https://dev.api.authsec.dev/sdkmgr/services" \
|
|
200
|
+
python3 server.py
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Run on Windows PowerShell:
|
|
204
|
+
|
|
205
|
+
```powershell
|
|
206
|
+
$env:AUTHSEC_AUTH_SERVICE_URL = "https://dev.api.authsec.dev/sdkmgr/mcp-auth"
|
|
207
|
+
$env:AUTHSEC_SERVICES_URL = "https://dev.api.authsec.dev/sdkmgr/services"
|
|
208
|
+
py -3 .\server.py
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Connect MCP Inspector:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npx @modelcontextprotocol/inspector http://127.0.0.1:3005
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Full setup by OS
|
|
218
|
+
|
|
219
|
+
#### macOS / Linux
|
|
220
|
+
|
|
221
|
+
1. Create and activate a virtual environment.
|
|
222
|
+
2. Install the SDK from GitHub or from a package index.
|
|
223
|
+
3. Create `server.py` with `from authsec_sdk import ...`.
|
|
224
|
+
4. Export `AUTHSEC_AUTH_SERVICE_URL` and `AUTHSEC_SERVICES_URL` if you want to override the defaults.
|
|
225
|
+
5. Run `python3 server.py`.
|
|
226
|
+
6. Open MCP Inspector against `http://127.0.0.1:3005`.
|
|
227
|
+
|
|
228
|
+
#### Windows PowerShell
|
|
229
|
+
|
|
230
|
+
1. Create and activate a virtual environment with `py -3 -m venv .venv` and `.venv\Scripts\Activate.ps1`.
|
|
231
|
+
2. Install the SDK from GitHub or from a package index.
|
|
232
|
+
3. Create `server.py` with `from authsec_sdk import ...`.
|
|
233
|
+
4. Set `$env:AUTHSEC_AUTH_SERVICE_URL` and `$env:AUTHSEC_SERVICES_URL` if you want to override the defaults.
|
|
234
|
+
5. Run `py -3 .\server.py`.
|
|
235
|
+
6. Open MCP Inspector against `http://127.0.0.1:3005`.
|
|
236
|
+
|
|
237
|
+
## How Protection Works
|
|
238
|
+
|
|
239
|
+
1. Server exposes OAuth tools from SDK Manager (`oauth_start`, `oauth_authenticate`, `oauth_status`, etc.).
|
|
240
|
+
2. User authenticates and gets/uses a session.
|
|
241
|
+
3. When a protected tool is called, SDK hits `protect-tool` upstream.
|
|
242
|
+
4. RBAC is evaluated from your tool declaration (`roles`, `groups`, `resources`, `scopes`, `permissions`).
|
|
243
|
+
5. On success, your handler receives:
|
|
244
|
+
- `arguments["session_id"]` (resolved session)
|
|
245
|
+
- `arguments["_user_info"]` (JWT/user claims)
|
|
246
|
+
|
|
247
|
+
Auth behavior note:
|
|
248
|
+
- Auth decisions are delegated upstream to SDK Manager.
|
|
249
|
+
- SDK may still expose OAuth tool schemas when upstream `tools/list` is unavailable so clients can continue auth bootstrap.
|
|
250
|
+
|
|
251
|
+
## Decorators
|
|
252
|
+
|
|
253
|
+
### `@protected_by_AuthSec(...)`
|
|
254
|
+
|
|
255
|
+
Use for tools that require auth/RBAC.
|
|
256
|
+
|
|
257
|
+
Key parameters:
|
|
258
|
+
- `tool_name`
|
|
259
|
+
- `roles`, `groups`, `resources`, `scopes`, `permissions`
|
|
260
|
+
- `require_all` (default `False`; if `True`, all specified categories must match)
|
|
261
|
+
- `description`
|
|
262
|
+
- `inputSchema`
|
|
263
|
+
|
|
264
|
+
### `@mcp_tool(...)`
|
|
265
|
+
|
|
266
|
+
Use for tools that should stay unprotected.
|
|
267
|
+
|
|
268
|
+
Key parameters:
|
|
269
|
+
- `name`
|
|
270
|
+
- `description`
|
|
271
|
+
- `inputSchema`
|
|
272
|
+
|
|
273
|
+
## Session-Aware Handler Pattern
|
|
274
|
+
|
|
275
|
+
If your protected function accepts a second `session` argument, SDK passes a simple session object:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
@protected_by_AuthSec("service_call", scopes=["read"])
|
|
279
|
+
async def service_call(arguments: dict, session) -> list:
|
|
280
|
+
# session.session_id, session.user_id, session.tenant_id, session.access_token
|
|
281
|
+
return [{"type": "text", "text": session.session_id}]
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Service Access SDK
|
|
285
|
+
|
|
286
|
+
Use `ServiceAccessSDK(session)` to fetch service credentials/tokens from SDK Manager services API:
|
|
287
|
+
|
|
288
|
+
```python
|
|
289
|
+
from authsec_sdk import ServiceAccessSDK
|
|
290
|
+
|
|
291
|
+
@protected_by_AuthSec("fetch_github_token", scopes=["read"])
|
|
292
|
+
async def fetch_github_token(arguments: dict, session) -> list:
|
|
293
|
+
services = ServiceAccessSDK(session)
|
|
294
|
+
token = await services.get_service_token("github")
|
|
295
|
+
return [{"type": "text", "text": f"Token length: {len(token)}"}]
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## Environment Variables
|
|
299
|
+
|
|
300
|
+
SDK runtime:
|
|
301
|
+
- `AUTHSEC_AUTH_SERVICE_URL` (optional, default points to dev)
|
|
302
|
+
- `AUTHSEC_SERVICES_URL` (optional, default points to dev)
|
|
303
|
+
- `AUTHSEC_TIMEOUT_SECONDS` (default `15`)
|
|
304
|
+
- `AUTHSEC_RETRIES` (default `2`)
|
|
305
|
+
- `AUTHSEC_TOOLS_LIST_TIMEOUT_SECONDS` (default `8`)
|
|
306
|
+
|
|
307
|
+
Note:
|
|
308
|
+
- Tool visibility policies like `AUTHSEC_ALWAYS_EXPOSE_PROTECTED_TOOLS` / `AUTHSEC_HIDE_UNAUTHORIZED_TOOLS` are controlled in SDK Manager service config, not this package.
|
|
309
|
+
|
|
310
|
+
## Troubleshooting
|
|
311
|
+
|
|
312
|
+
- `ModuleNotFoundError: No module named 'AuthSec_SDK'`
|
|
313
|
+
- The correct import is `from authsec_sdk import ...`. The package name is `authsec-sdk`, but the Python import path is `authsec_sdk`.
|
|
314
|
+
- Local folder named `authsec_sdk` shadows the installed package
|
|
315
|
+
- Rename or remove the local folder before running your app. `python path/to/script.py` puts that script directory first on `sys.path`.
|
|
316
|
+
- Apple Silicon Mac imports fail with `incompatible architecture`
|
|
317
|
+
- Do not mix `arm64` and `x86_64` Python environments. Recreate the virtualenv using the same architecture you will run.
|
|
318
|
+
- `OAuth tool failed: ... released back to the pool`
|
|
319
|
+
- This is an SDK Manager backend deployment/version issue, not client SDK usage.
|
|
320
|
+
- `verifyToken failed: 404`
|
|
321
|
+
- Check SDK Manager `AUTH_MANAGER_URL` and ensure it targets your correct environment (`dev` vs `prod`).
|
|
322
|
+
- Protected tools denied unexpectedly
|
|
323
|
+
- Confirm JWT claims contain expected roles/scopes/resources and match your decorator RBAC.
|
|
324
|
+
|
|
325
|
+
## Prompt Template (Copy/Paste)
|
|
326
|
+
|
|
327
|
+
Use this prompt with any coding LLM to wrap a Python MCP server with AuthSec:
|
|
328
|
+
|
|
329
|
+
```text
|
|
330
|
+
You are editing a Python MCP server codebase.
|
|
331
|
+
|
|
332
|
+
Goal:
|
|
333
|
+
1) Integrate authsec_sdk so selected tools are protected with OAuth + RBAC.
|
|
334
|
+
2) Keep public tools unprotected.
|
|
335
|
+
3) Start server via run_mcp_server_with_oauth.
|
|
336
|
+
|
|
337
|
+
Requirements:
|
|
338
|
+
- Use @protected_by_AuthSec for sensitive tools.
|
|
339
|
+
- Use @mcp_tool for unprotected tools.
|
|
340
|
+
- Pass a clear inputSchema for each tool.
|
|
341
|
+
- Ensure protected handlers read user info from arguments["_user_info"].
|
|
342
|
+
- Keep all OAuth/auth checks delegated upstream (no local bypass/fallback logic).
|
|
343
|
+
- Add startup instructions using AUTHSEC_AUTH_SERVICE_URL and AUTHSEC_SERVICES_URL.
|
|
344
|
+
|
|
345
|
+
Inputs:
|
|
346
|
+
- client_id: <YOUR_CLIENT_ID>
|
|
347
|
+
- app_name: <YOUR_APP_NAME>
|
|
348
|
+
- protected tools + RBAC:
|
|
349
|
+
- <tool_name_1>: roles=[...], scopes=[...], require_all=<true/false>
|
|
350
|
+
- <tool_name_2>: ...
|
|
351
|
+
- unprotected tools:
|
|
352
|
+
- <tool_name_3>
|
|
353
|
+
|
|
354
|
+
Deliverables:
|
|
355
|
+
- Updated Python source files.
|
|
356
|
+
- A runnable command section.
|
|
357
|
+
- A short verification checklist using MCP Inspector.
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Publishing (Python)
|
|
361
|
+
|
|
362
|
+
### One-command publish (recommended)
|
|
363
|
+
|
|
364
|
+
From the repo root:
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# TestPyPI first
|
|
368
|
+
bash scripts/publish-python-sdk.sh --test
|
|
369
|
+
|
|
370
|
+
# Then PyPI
|
|
371
|
+
bash scripts/publish-python-sdk.sh
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Auth for `twine` (do not commit tokens):
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
export TWINE_USERNAME="__token__"
|
|
378
|
+
export TWINE_PASSWORD="pypi-REDACTED"
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 1) Bump version
|
|
382
|
+
|
|
383
|
+
Update version consistently:
|
|
384
|
+
- `packages/python-sdk/pyproject.toml` -> `[project].version`
|
|
385
|
+
- `packages/python-sdk/src/authsec_sdk/__init__.py` -> `__version__`
|
|
386
|
+
|
|
387
|
+
### 2) Build artifacts
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
cd packages/python-sdk
|
|
391
|
+
python3 -m pip install --upgrade build twine
|
|
392
|
+
python3 -m build
|
|
393
|
+
python3 -m twine check dist/*
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 3) Test publish (recommended)
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
python3 -m twine upload --repository testpypi dist/*
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Install from TestPyPI in a clean venv and run smoke test.
|
|
403
|
+
|
|
404
|
+
### 4) Publish to PyPI
|
|
405
|
+
|
|
406
|
+
```bash
|
|
407
|
+
python3 -m twine upload dist/*
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### 5) Post-publish check
|
|
411
|
+
|
|
412
|
+
```bash
|
|
413
|
+
python3 -m pip install --upgrade authsec-sdk
|
|
414
|
+
python3 -c "import authsec_sdk; print(authsec_sdk.__version__)"
|
|
415
|
+
```
|