gcshttpx 0.1.4__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.
- gcshttpx-0.1.4/.gitignore +73 -0
- gcshttpx-0.1.4/LICENSE +21 -0
- gcshttpx-0.1.4/PKG-INFO +421 -0
- gcshttpx-0.1.4/README.md +389 -0
- gcshttpx-0.1.4/gcshttpx/__init__.py +21 -0
- gcshttpx-0.1.4/gcshttpx/auth.py +562 -0
- gcshttpx-0.1.4/gcshttpx/py.typed +2 -0
- gcshttpx-0.1.4/gcshttpx/storage.py +944 -0
- gcshttpx-0.1.4/pyproject.toml +64 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# dependencies
|
|
4
|
+
node_modules
|
|
5
|
+
.pnp
|
|
6
|
+
.pnp.js
|
|
7
|
+
|
|
8
|
+
# testing
|
|
9
|
+
coverage
|
|
10
|
+
.trivyignore
|
|
11
|
+
.pytest_cache
|
|
12
|
+
|
|
13
|
+
# temporary
|
|
14
|
+
temp/
|
|
15
|
+
logs/
|
|
16
|
+
|
|
17
|
+
# next.js
|
|
18
|
+
.next/
|
|
19
|
+
out/
|
|
20
|
+
build
|
|
21
|
+
|
|
22
|
+
# misc
|
|
23
|
+
.DS_Store
|
|
24
|
+
*.pem
|
|
25
|
+
|
|
26
|
+
# debug
|
|
27
|
+
npm-debug.log*
|
|
28
|
+
yarn-debug.log*
|
|
29
|
+
yarn-error.log*
|
|
30
|
+
.pnpm-debug.log*
|
|
31
|
+
|
|
32
|
+
# local env files
|
|
33
|
+
.env.local
|
|
34
|
+
.env.development.local
|
|
35
|
+
.env.test.local
|
|
36
|
+
.env.production.local
|
|
37
|
+
|
|
38
|
+
# turbo
|
|
39
|
+
.turbo
|
|
40
|
+
|
|
41
|
+
.env
|
|
42
|
+
.vercel
|
|
43
|
+
.vscode
|
|
44
|
+
.env*.local
|
|
45
|
+
|
|
46
|
+
venv
|
|
47
|
+
|
|
48
|
+
.vercel
|
|
49
|
+
.env*.local
|
|
50
|
+
.idea
|
|
51
|
+
|
|
52
|
+
__pycache__
|
|
53
|
+
|
|
54
|
+
# Python virtual environments
|
|
55
|
+
.venv
|
|
56
|
+
venv/
|
|
57
|
+
*.exe
|
|
58
|
+
|
|
59
|
+
# uv
|
|
60
|
+
.uv/
|
|
61
|
+
|
|
62
|
+
# Build artifacts
|
|
63
|
+
dist/
|
|
64
|
+
*.egg-info/
|
|
65
|
+
|
|
66
|
+
.env*.local
|
|
67
|
+
*.log
|
|
68
|
+
secret
|
|
69
|
+
*.code-workspace
|
|
70
|
+
.claude/settings.local.json
|
|
71
|
+
profile.html
|
|
72
|
+
docker-compose.override.yml
|
|
73
|
+
.coverage
|
gcshttpx-0.1.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Piotr Penar
|
|
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.
|
gcshttpx-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gcshttpx
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Minimal async Google Cloud Storage + Auth client built on httpx (HTTP/2).
|
|
5
|
+
Project-URL: Homepage, https://github.com/piotrpenar/gcshttpx
|
|
6
|
+
Project-URL: Source, https://github.com/piotrpenar/gcshttpx
|
|
7
|
+
Project-URL: Issues, https://github.com/piotrpenar/gcshttpx/issues
|
|
8
|
+
Author: Piotr Penar
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: asyncio,gcs,google-cloud-storage,http2,httpx
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: cryptography>=43.0.0
|
|
24
|
+
Requires-Dist: httpx[http2]>=0.27.0
|
|
25
|
+
Requires-Dist: orjson>=3.10.0
|
|
26
|
+
Requires-Dist: pyjwt>=2.9.0
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: coverage>=7.6.0; extra == 'test'
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
30
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# gcshttpx
|
|
34
|
+
|
|
35
|
+
**Minimal, secure async Google Cloud Storage client built on httpx with native HTTP/2 support.**
|
|
36
|
+
|
|
37
|
+
[](https://github.com/piotrpenar/gcshttpx/actions)
|
|
38
|
+
[](https://pypi.org/project/gcshttpx/)
|
|
39
|
+
[](https://pypi.org/project/gcshttpx/)
|
|
40
|
+
[](LICENSE)
|
|
41
|
+
|
|
42
|
+
## Why gcshttpx?
|
|
43
|
+
|
|
44
|
+
- **🚀 Fast**: Built on httpx with HTTP/2 support for connection multiplexing
|
|
45
|
+
- **🔒 Secure**: Explicit credential handling, no automatic filesystem searches
|
|
46
|
+
- **🪶 Lightweight**: Minimal dependencies (httpx, orjson, PyJWT, cryptography)
|
|
47
|
+
- **⚡ Async-first**: Full async/await support with modern Python
|
|
48
|
+
- **🎯 Simple**: Clean, intuitive API for common operations
|
|
49
|
+
- **📦 Type-safe**: Fully typed with py.typed marker
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Using uv (recommended)
|
|
55
|
+
uv add gcshttpx
|
|
56
|
+
|
|
57
|
+
# Using pip
|
|
58
|
+
pip install gcshttpx
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Quick Start
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import asyncio
|
|
65
|
+
from gcshttpx import Storage, Token
|
|
66
|
+
|
|
67
|
+
async def main():
|
|
68
|
+
# Create authenticated token
|
|
69
|
+
token = Token(
|
|
70
|
+
service_file="path/to/service-account.json",
|
|
71
|
+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Create storage client
|
|
75
|
+
storage = Storage(token=token)
|
|
76
|
+
|
|
77
|
+
# Upload a file
|
|
78
|
+
await storage.upload("my-bucket", "hello.txt", b"Hello, World!")
|
|
79
|
+
|
|
80
|
+
# Download a file
|
|
81
|
+
data = await storage.download("my-bucket", "hello.txt")
|
|
82
|
+
print(data) # b"Hello, World!"
|
|
83
|
+
|
|
84
|
+
# List objects
|
|
85
|
+
items = await storage.list_objects("my-bucket", params={"prefix": "logs/"})
|
|
86
|
+
for item in items["items"]:
|
|
87
|
+
print(item["name"])
|
|
88
|
+
|
|
89
|
+
# Clean up
|
|
90
|
+
await storage.close()
|
|
91
|
+
|
|
92
|
+
asyncio.run(main())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Authentication
|
|
96
|
+
|
|
97
|
+
### Service Account (Recommended for Production)
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from gcshttpx import Token
|
|
101
|
+
|
|
102
|
+
# From file path
|
|
103
|
+
token = Token(
|
|
104
|
+
service_file="/path/to/service-account.json",
|
|
105
|
+
scopes=["https://www.googleapis.com/auth/devstorage.read_write"]
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# From environment variable
|
|
109
|
+
# Set GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
|
|
110
|
+
token = Token(scopes=["https://www.googleapis.com/auth/cloud-platform"])
|
|
111
|
+
|
|
112
|
+
# From file-like object
|
|
113
|
+
import io
|
|
114
|
+
import json
|
|
115
|
+
|
|
116
|
+
# Do not use this pattern in production
|
|
117
|
+
credentials = {
|
|
118
|
+
"type": "service_account",
|
|
119
|
+
"project_id": "my-project",
|
|
120
|
+
"private_key_id": "key-id",
|
|
121
|
+
"private_key": "-----BEGIN PRIVATE KEY-----\n...",
|
|
122
|
+
"client_email": "service@my-project.iam.gserviceaccount.com",
|
|
123
|
+
"client_id": "123456789",
|
|
124
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
125
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
token = Token(
|
|
129
|
+
service_file=io.StringIO(json.dumps(credentials)),
|
|
130
|
+
scopes=["https://www.googleapis.com/auth/cloud-platform"]
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### GCE Metadata Server (For GCP Environments)
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from gcshttpx import Token
|
|
138
|
+
|
|
139
|
+
# Automatically uses GCE metadata service if no credentials provided
|
|
140
|
+
token = Token(scopes=["https://www.googleapis.com/auth/cloud-platform"])
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Security Notes
|
|
144
|
+
|
|
145
|
+
⚠️ **gcshttpx takes security seriously:**
|
|
146
|
+
|
|
147
|
+
- **HTTPS-only token endpoints**: Prevents credential leakage
|
|
148
|
+
- **Input validation**: All credentials are validated before use
|
|
149
|
+
- **No sensitive data in logs**: Errors don't expose credentials
|
|
150
|
+
|
|
151
|
+
## Storage Operations
|
|
152
|
+
|
|
153
|
+
### Upload
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# Simple upload (bytes or str)
|
|
157
|
+
await storage.upload("bucket", "file.txt", b"content")
|
|
158
|
+
|
|
159
|
+
# With metadata
|
|
160
|
+
await storage.upload(
|
|
161
|
+
"bucket",
|
|
162
|
+
"file.json",
|
|
163
|
+
b'{"key": "value"}',
|
|
164
|
+
content_type="application/json",
|
|
165
|
+
metadata={"cacheControl": "no-cache", "contentLanguage": "en"}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# From file
|
|
169
|
+
await storage.upload_from_filename("bucket", "remote.txt", "local.txt")
|
|
170
|
+
|
|
171
|
+
# Gzip compression
|
|
172
|
+
await storage.upload("bucket", "file.txt", b"content", zipped=True)
|
|
173
|
+
|
|
174
|
+
# Force resumable upload (for large files)
|
|
175
|
+
await storage.upload(
|
|
176
|
+
"bucket",
|
|
177
|
+
"large-file.bin",
|
|
178
|
+
large_data,
|
|
179
|
+
force_resumable_upload=True
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Download
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
# Download to bytes
|
|
187
|
+
data = await storage.download("bucket", "file.txt")
|
|
188
|
+
|
|
189
|
+
# Download to file
|
|
190
|
+
await storage.download_to_filename("bucket", "remote.txt", "local.txt")
|
|
191
|
+
|
|
192
|
+
# Download metadata only
|
|
193
|
+
metadata = await storage.download_metadata("bucket", "file.txt")
|
|
194
|
+
print(metadata["size"], metadata["contentType"])
|
|
195
|
+
|
|
196
|
+
# Stream large files
|
|
197
|
+
stream = await storage.download_stream("bucket", "large-file.bin")
|
|
198
|
+
while chunk := await stream.read(8192):
|
|
199
|
+
process(chunk)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### List & Search
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
# List all objects
|
|
206
|
+
result = await storage.list_objects("bucket")
|
|
207
|
+
for item in result["items"]:
|
|
208
|
+
print(item["name"], item["size"])
|
|
209
|
+
|
|
210
|
+
# With prefix filter
|
|
211
|
+
result = await storage.list_objects("bucket", params={"prefix": "logs/2024/"})
|
|
212
|
+
|
|
213
|
+
# List with delimiter (folder-like)
|
|
214
|
+
result = await storage.list_objects("bucket", params={"delimiter": "/"})
|
|
215
|
+
print("Prefixes:", result.get("prefixes")) # 'folders'
|
|
216
|
+
print("Items:", result.get("items")) # files in current 'folder'
|
|
217
|
+
|
|
218
|
+
# List buckets
|
|
219
|
+
buckets = await storage.list_buckets("my-project-id")
|
|
220
|
+
for bucket in buckets:
|
|
221
|
+
print(bucket.name)
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Delete
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
# Delete object
|
|
228
|
+
await storage.delete("bucket", "file.txt")
|
|
229
|
+
|
|
230
|
+
# Delete returns status text
|
|
231
|
+
status = await storage.delete("bucket", "file.txt")
|
|
232
|
+
print(status) # "OK" or error message
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Object Operations
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
# Check if object exists
|
|
239
|
+
exists = await storage.blob_exists("bucket", "file.txt")
|
|
240
|
+
|
|
241
|
+
# Compose multiple objects into one
|
|
242
|
+
await storage.compose(
|
|
243
|
+
"bucket",
|
|
244
|
+
"merged.txt",
|
|
245
|
+
["part1.txt", "part2.txt", "part3.txt"],
|
|
246
|
+
content_type="text/plain"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Update metadata
|
|
250
|
+
await storage.patch_metadata(
|
|
251
|
+
"bucket",
|
|
252
|
+
"file.txt",
|
|
253
|
+
{"cacheControl": "public, max-age=3600"}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Get bucket metadata
|
|
257
|
+
metadata = await storage.get_bucket_metadata("bucket")
|
|
258
|
+
print(metadata["location"], metadata["storageClass"])
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Signed URLs
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from gcshttpx import IamClient
|
|
265
|
+
|
|
266
|
+
# Create IAM client for signing
|
|
267
|
+
iam = IamClient(token=token)
|
|
268
|
+
|
|
269
|
+
# Generate signed URL (valid for 1 hour)
|
|
270
|
+
bucket = storage.get_bucket("my-bucket")
|
|
271
|
+
blob = await bucket.get_blob("file.txt")
|
|
272
|
+
signed_url = await blob.get_signed_url(
|
|
273
|
+
expiration=3600,
|
|
274
|
+
iam_client=iam
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Share the URL
|
|
278
|
+
print(f"Download link: {signed_url}")
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Bucket and Blob Objects
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
# Get bucket object
|
|
285
|
+
bucket = storage.get_bucket("my-bucket")
|
|
286
|
+
metadata = await bucket.get_metadata()
|
|
287
|
+
exists = await bucket.blob_exists("file.txt")
|
|
288
|
+
|
|
289
|
+
# Get blob object
|
|
290
|
+
blob = await bucket.get_blob("file.txt")
|
|
291
|
+
await blob.upload(b"new content", content_type="text/plain")
|
|
292
|
+
data = await blob.download()
|
|
293
|
+
await blob.delete()
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Advanced Usage
|
|
297
|
+
|
|
298
|
+
### Custom HTTP Client
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
import httpx
|
|
302
|
+
from gcshttpx import Storage, Token
|
|
303
|
+
|
|
304
|
+
# Use custom httpx client with specific settings
|
|
305
|
+
async with httpx.AsyncClient(
|
|
306
|
+
http2=True,
|
|
307
|
+
timeout=httpx.Timeout(30.0),
|
|
308
|
+
limits=httpx.Limits(max_keepalive_connections=5)
|
|
309
|
+
) as client:
|
|
310
|
+
token = Token(service_file="credentials.json")
|
|
311
|
+
storage = Storage(session=client, token=token)
|
|
312
|
+
|
|
313
|
+
await storage.upload("bucket", "file.txt", b"content")
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Token Management
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from gcshttpx import Token
|
|
320
|
+
|
|
321
|
+
token = Token(
|
|
322
|
+
service_file="credentials.json",
|
|
323
|
+
scopes=["https://www.googleapis.com/auth/cloud-platform"],
|
|
324
|
+
# Refresh token when 50% of lifetime has passed
|
|
325
|
+
background_refresh_after=0.5,
|
|
326
|
+
# Force refresh when 95% of lifetime has passed
|
|
327
|
+
force_refresh_after=0.95
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Manually get current token
|
|
331
|
+
access_token = await token.get()
|
|
332
|
+
|
|
333
|
+
# Get project ID
|
|
334
|
+
project_id = await token.get_project()
|
|
335
|
+
|
|
336
|
+
# Close token session
|
|
337
|
+
await token.close()
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Error Handling
|
|
341
|
+
|
|
342
|
+
```python
|
|
343
|
+
import httpx
|
|
344
|
+
from gcshttpx import Storage
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
data = await storage.download("bucket", "nonexistent.txt")
|
|
348
|
+
except httpx.HTTPStatusError as e:
|
|
349
|
+
print(f"HTTP {e.response.status_code}: {e.response.text}")
|
|
350
|
+
except httpx.TimeoutException:
|
|
351
|
+
print("Request timed out")
|
|
352
|
+
except ValueError as e:
|
|
353
|
+
print(f"Validation error: {e}")
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Common OAuth Scopes
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
# Full control
|
|
360
|
+
"https://www.googleapis.com/auth/cloud-platform"
|
|
361
|
+
|
|
362
|
+
# Read/write access to storage
|
|
363
|
+
"https://www.googleapis.com/auth/devstorage.read_write"
|
|
364
|
+
|
|
365
|
+
# Read-only access
|
|
366
|
+
"https://www.googleapis.com/auth/devstorage.read_only"
|
|
367
|
+
|
|
368
|
+
# Write-only access
|
|
369
|
+
"https://www.googleapis.com/auth/devstorage.write_only"
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
## Development
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# Clone repository
|
|
376
|
+
git clone https://github.com/piotrpenar/gcshttpx.git
|
|
377
|
+
cd gcshttpx
|
|
378
|
+
|
|
379
|
+
# Install with uv
|
|
380
|
+
uv sync
|
|
381
|
+
|
|
382
|
+
# Run tests
|
|
383
|
+
uv run pytest
|
|
384
|
+
|
|
385
|
+
# Run tests with coverage
|
|
386
|
+
uv run pytest --cov=gcshttpx
|
|
387
|
+
|
|
388
|
+
# Lint code
|
|
389
|
+
uv run ruff check .
|
|
390
|
+
|
|
391
|
+
# Format code
|
|
392
|
+
uv run ruff format .
|
|
393
|
+
|
|
394
|
+
# Build package
|
|
395
|
+
uv build
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## Requirements
|
|
399
|
+
|
|
400
|
+
- Python 3.10+
|
|
401
|
+
- httpx[http2] >= 0.27.0
|
|
402
|
+
- orjson >= 3.10.0
|
|
403
|
+
- PyJWT >= 2.9.0
|
|
404
|
+
- cryptography >= 43.0.0
|
|
405
|
+
|
|
406
|
+
## License
|
|
407
|
+
|
|
408
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
409
|
+
|
|
410
|
+
## Contributing
|
|
411
|
+
|
|
412
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
413
|
+
|
|
414
|
+
## Links
|
|
415
|
+
|
|
416
|
+
- **Documentation**: [README.md](https://github.com/piotrpenar/gcshttpx)
|
|
417
|
+
- **Source Code**: [GitHub](https://github.com/piotrpenar/gcshttpx)
|
|
418
|
+
- **Issue Tracker**: [GitHub Issues](https://github.com/piotrpenar/gcshttpx/issues)
|
|
419
|
+
- **PyPI**: [gcshttpx](https://pypi.org/project/gcshttpx/)
|
|
420
|
+
|
|
421
|
+
|