syncforge 1.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.
- syncforge-1.0.0/LICENSE +21 -0
- syncforge-1.0.0/PKG-INFO +196 -0
- syncforge-1.0.0/README.md +169 -0
- syncforge-1.0.0/pyproject.toml +39 -0
- syncforge-1.0.0/setup.cfg +4 -0
- syncforge-1.0.0/syncforge/__init__.py +30 -0
- syncforge-1.0.0/syncforge/client.py +244 -0
- syncforge-1.0.0/syncforge/exceptions.py +21 -0
- syncforge-1.0.0/syncforge/result.py +31 -0
- syncforge-1.0.0/syncforge.egg-info/PKG-INFO +196 -0
- syncforge-1.0.0/syncforge.egg-info/SOURCES.txt +11 -0
- syncforge-1.0.0/syncforge.egg-info/dependency_links.txt +1 -0
- syncforge-1.0.0/syncforge.egg-info/top_level.txt +1 -0
syncforge-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SyncForge
|
|
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.
|
syncforge-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syncforge
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for SyncForge — control exactly when data syncs between your database and clients.
|
|
5
|
+
Author-email: SyncForge <sureshdulupolai@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sureshdulupolai/syncforge
|
|
8
|
+
Project-URL: Documentation, https://syncforge.dev/docs/
|
|
9
|
+
Project-URL: Repository, https://github.com/sureshdulupolai/syncforge
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/sureshdulupolai/syncforge/issues
|
|
11
|
+
Keywords: syncforge,sync,database,cache,invalidation,django,fastapi
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# SyncForge Python SDK
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/syncforge/)
|
|
31
|
+
[](https://pypi.org/project/syncforge/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
Official Python SDK for the [SyncForge](https://syncforge.dev) data sync platform.
|
|
35
|
+
Control exactly when data syncs between your database and client applications — no polling, no wasted DB calls.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install syncforge
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Zero external dependencies.** Uses only Python stdlib (`urllib`, `json`, `threading`).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from syncforge import SyncForge
|
|
53
|
+
|
|
54
|
+
sf = SyncForge(api_key='sf_live_YOUR_KEY')
|
|
55
|
+
|
|
56
|
+
# After any DB write — notify all connected clients
|
|
57
|
+
sf.refresh('products')
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## The `syncforge.py` Pattern (Recommended)
|
|
63
|
+
|
|
64
|
+
Place a `syncforge.py` file at your project root — same level as `manage.py` or `main.py`.
|
|
65
|
+
This mirrors the Celery pattern and gives you a single shared instance.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# syncforge.py (project root)
|
|
69
|
+
import os
|
|
70
|
+
from syncforge import SyncForge
|
|
71
|
+
|
|
72
|
+
sf = SyncForge(
|
|
73
|
+
api_key=os.environ.get('SYNCFORGE_API_KEY', 'sf_live_YOUR_KEY')
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then import `sf` anywhere:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# views.py / routes.py
|
|
81
|
+
from syncforge import sf
|
|
82
|
+
|
|
83
|
+
def create_product(request):
|
|
84
|
+
Product.objects.create(name='New Item', price=99.99)
|
|
85
|
+
sf.refresh('products') # one line — all clients updated
|
|
86
|
+
return JsonResponse({'status': 'created'})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### `SyncForge(api_key, base_url, timeout, silent, async_mode)`
|
|
94
|
+
|
|
95
|
+
| Parameter | Default | Description |
|
|
96
|
+
|--------------|-------------------------------|------------------------------------------------------|
|
|
97
|
+
| `api_key` | required | Your API key (`sf_live_...`) |
|
|
98
|
+
| `base_url` | `https://syncforge.dev/api` | Override for local dev / self-hosted |
|
|
99
|
+
| `timeout` | `10` | HTTP timeout in seconds |
|
|
100
|
+
| `silent` | `False` | Suppress errors — logs warnings instead of raising |
|
|
101
|
+
| `async_mode` | `False` | Fire-and-forget — refresh runs in a background thread|
|
|
102
|
+
|
|
103
|
+
### `sf.refresh(*tables)` → `SyncResult | list[SyncResult]`
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
sf.refresh('products') # single table
|
|
107
|
+
sf.refresh('products', 'categories', 'orders') # multiple at once
|
|
108
|
+
|
|
109
|
+
result = sf.refresh('products')
|
|
110
|
+
print(result.ok) # True
|
|
111
|
+
print(result.calls_saved) # 1854211
|
|
112
|
+
print(result.sync_mode) # 'Event — On INSERT / UPDATE / DELETE'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `sf.ping()` → `bool`
|
|
116
|
+
Health check — returns `True` if SyncForge is reachable.
|
|
117
|
+
|
|
118
|
+
### `sf.project_info()` → `dict`
|
|
119
|
+
Returns project metadata and all registered tables.
|
|
120
|
+
|
|
121
|
+
### `sf.list_tables()` → `list`
|
|
122
|
+
Lists all tables with their sync mode and stats.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Django Integration
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# syncforge.py (next to manage.py)
|
|
130
|
+
import os
|
|
131
|
+
from syncforge import SyncForge
|
|
132
|
+
sf = SyncForge(api_key=os.environ.get('SYNCFORGE_API_KEY'))
|
|
133
|
+
|
|
134
|
+
# myapp/views.py
|
|
135
|
+
from syncforge import sf
|
|
136
|
+
|
|
137
|
+
def update_products(request):
|
|
138
|
+
Product.objects.filter(on_sale=True).update(price=F('price') * 0.9)
|
|
139
|
+
sf.refresh('products')
|
|
140
|
+
return JsonResponse({'status': 'updated'})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## FastAPI Integration
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from fastapi import FastAPI
|
|
147
|
+
from syncforge import sf
|
|
148
|
+
|
|
149
|
+
app = FastAPI()
|
|
150
|
+
|
|
151
|
+
@app.post("/products/")
|
|
152
|
+
async def create_product(name: str, price: float):
|
|
153
|
+
db.execute("INSERT INTO products ...")
|
|
154
|
+
sf.refresh('products')
|
|
155
|
+
return {"status": "ok"}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Production Tips
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Silent mode — SyncForge errors never crash your app
|
|
164
|
+
sf = SyncForge(api_key='sf_live_...', silent=True)
|
|
165
|
+
|
|
166
|
+
# Async mode — fire-and-forget, returns immediately
|
|
167
|
+
sf = SyncForge(api_key='sf_live_...', async_mode=True)
|
|
168
|
+
sf.refresh('products') # returns None, syncs in background
|
|
169
|
+
|
|
170
|
+
# Override base URL for local development
|
|
171
|
+
sf = SyncForge(api_key='sf_live_...', base_url='http://localhost:8000/api')
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from syncforge import SyncForge, AuthError, TableNotFoundError, NetworkError
|
|
180
|
+
|
|
181
|
+
sf = SyncForge(api_key='sf_live_...')
|
|
182
|
+
try:
|
|
183
|
+
sf.refresh('products')
|
|
184
|
+
except AuthError:
|
|
185
|
+
print("Invalid API key")
|
|
186
|
+
except TableNotFoundError:
|
|
187
|
+
print("Register the table in your SyncForge dashboard first")
|
|
188
|
+
except NetworkError:
|
|
189
|
+
print("Could not reach SyncForge — check your internet connection")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# SyncForge Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/syncforge/)
|
|
4
|
+
[](https://pypi.org/project/syncforge/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Official Python SDK for the [SyncForge](https://syncforge.dev) data sync platform.
|
|
8
|
+
Control exactly when data syncs between your database and client applications — no polling, no wasted DB calls.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install syncforge
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Zero external dependencies.** Uses only Python stdlib (`urllib`, `json`, `threading`).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from syncforge import SyncForge
|
|
26
|
+
|
|
27
|
+
sf = SyncForge(api_key='sf_live_YOUR_KEY')
|
|
28
|
+
|
|
29
|
+
# After any DB write — notify all connected clients
|
|
30
|
+
sf.refresh('products')
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## The `syncforge.py` Pattern (Recommended)
|
|
36
|
+
|
|
37
|
+
Place a `syncforge.py` file at your project root — same level as `manage.py` or `main.py`.
|
|
38
|
+
This mirrors the Celery pattern and gives you a single shared instance.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# syncforge.py (project root)
|
|
42
|
+
import os
|
|
43
|
+
from syncforge import SyncForge
|
|
44
|
+
|
|
45
|
+
sf = SyncForge(
|
|
46
|
+
api_key=os.environ.get('SYNCFORGE_API_KEY', 'sf_live_YOUR_KEY')
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then import `sf` anywhere:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
# views.py / routes.py
|
|
54
|
+
from syncforge import sf
|
|
55
|
+
|
|
56
|
+
def create_product(request):
|
|
57
|
+
Product.objects.create(name='New Item', price=99.99)
|
|
58
|
+
sf.refresh('products') # one line — all clients updated
|
|
59
|
+
return JsonResponse({'status': 'created'})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## API Reference
|
|
65
|
+
|
|
66
|
+
### `SyncForge(api_key, base_url, timeout, silent, async_mode)`
|
|
67
|
+
|
|
68
|
+
| Parameter | Default | Description |
|
|
69
|
+
|--------------|-------------------------------|------------------------------------------------------|
|
|
70
|
+
| `api_key` | required | Your API key (`sf_live_...`) |
|
|
71
|
+
| `base_url` | `https://syncforge.dev/api` | Override for local dev / self-hosted |
|
|
72
|
+
| `timeout` | `10` | HTTP timeout in seconds |
|
|
73
|
+
| `silent` | `False` | Suppress errors — logs warnings instead of raising |
|
|
74
|
+
| `async_mode` | `False` | Fire-and-forget — refresh runs in a background thread|
|
|
75
|
+
|
|
76
|
+
### `sf.refresh(*tables)` → `SyncResult | list[SyncResult]`
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
sf.refresh('products') # single table
|
|
80
|
+
sf.refresh('products', 'categories', 'orders') # multiple at once
|
|
81
|
+
|
|
82
|
+
result = sf.refresh('products')
|
|
83
|
+
print(result.ok) # True
|
|
84
|
+
print(result.calls_saved) # 1854211
|
|
85
|
+
print(result.sync_mode) # 'Event — On INSERT / UPDATE / DELETE'
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `sf.ping()` → `bool`
|
|
89
|
+
Health check — returns `True` if SyncForge is reachable.
|
|
90
|
+
|
|
91
|
+
### `sf.project_info()` → `dict`
|
|
92
|
+
Returns project metadata and all registered tables.
|
|
93
|
+
|
|
94
|
+
### `sf.list_tables()` → `list`
|
|
95
|
+
Lists all tables with their sync mode and stats.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Django Integration
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# syncforge.py (next to manage.py)
|
|
103
|
+
import os
|
|
104
|
+
from syncforge import SyncForge
|
|
105
|
+
sf = SyncForge(api_key=os.environ.get('SYNCFORGE_API_KEY'))
|
|
106
|
+
|
|
107
|
+
# myapp/views.py
|
|
108
|
+
from syncforge import sf
|
|
109
|
+
|
|
110
|
+
def update_products(request):
|
|
111
|
+
Product.objects.filter(on_sale=True).update(price=F('price') * 0.9)
|
|
112
|
+
sf.refresh('products')
|
|
113
|
+
return JsonResponse({'status': 'updated'})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## FastAPI Integration
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from fastapi import FastAPI
|
|
120
|
+
from syncforge import sf
|
|
121
|
+
|
|
122
|
+
app = FastAPI()
|
|
123
|
+
|
|
124
|
+
@app.post("/products/")
|
|
125
|
+
async def create_product(name: str, price: float):
|
|
126
|
+
db.execute("INSERT INTO products ...")
|
|
127
|
+
sf.refresh('products')
|
|
128
|
+
return {"status": "ok"}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Production Tips
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
# Silent mode — SyncForge errors never crash your app
|
|
137
|
+
sf = SyncForge(api_key='sf_live_...', silent=True)
|
|
138
|
+
|
|
139
|
+
# Async mode — fire-and-forget, returns immediately
|
|
140
|
+
sf = SyncForge(api_key='sf_live_...', async_mode=True)
|
|
141
|
+
sf.refresh('products') # returns None, syncs in background
|
|
142
|
+
|
|
143
|
+
# Override base URL for local development
|
|
144
|
+
sf = SyncForge(api_key='sf_live_...', base_url='http://localhost:8000/api')
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Error Handling
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from syncforge import SyncForge, AuthError, TableNotFoundError, NetworkError
|
|
153
|
+
|
|
154
|
+
sf = SyncForge(api_key='sf_live_...')
|
|
155
|
+
try:
|
|
156
|
+
sf.refresh('products')
|
|
157
|
+
except AuthError:
|
|
158
|
+
print("Invalid API key")
|
|
159
|
+
except TableNotFoundError:
|
|
160
|
+
print("Register the table in your SyncForge dashboard first")
|
|
161
|
+
except NetworkError:
|
|
162
|
+
print("Could not reach SyncForge — check your internet connection")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "syncforge"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for SyncForge — control exactly when data syncs between your database and clients."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "SyncForge", email = "sureshdulupolai@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
keywords = ["syncforge", "sync", "database", "cache", "invalidation", "django", "fastapi"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 5 - Production/Stable",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
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
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
26
|
+
"Topic :: Database",
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.8"
|
|
29
|
+
dependencies = [] # zero external dependencies — only stdlib
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/sureshdulupolai/syncforge"
|
|
33
|
+
Documentation = "https://syncforge.dev/docs/"
|
|
34
|
+
Repository = "https://github.com/sureshdulupolai/syncforge"
|
|
35
|
+
"Bug Tracker" = "https://github.com/sureshdulupolai/syncforge/issues"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["."]
|
|
39
|
+
include = ["syncforge*"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SyncForge Python SDK
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Official Python client for the SyncForge data sync platform.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from syncforge import SyncForge
|
|
9
|
+
sf = SyncForge(api_key='sf_live_YOUR_KEY')
|
|
10
|
+
sf.refresh('products')
|
|
11
|
+
|
|
12
|
+
Or use a project-level syncforge.py file (like Celery pattern)::
|
|
13
|
+
|
|
14
|
+
# syncforge.py
|
|
15
|
+
import os
|
|
16
|
+
from syncforge import SyncForge
|
|
17
|
+
sf = SyncForge(api_key=os.environ.get('SYNCFORGE_API_KEY'))
|
|
18
|
+
|
|
19
|
+
# In your views / handlers:
|
|
20
|
+
from syncforge import sf
|
|
21
|
+
sf.refresh('products')
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .client import SyncForge
|
|
25
|
+
from .result import SyncResult
|
|
26
|
+
from .exceptions import SyncForgeError, AuthError, TableNotFoundError
|
|
27
|
+
|
|
28
|
+
__version__ = "1.0.0"
|
|
29
|
+
__author__ = "SyncForge"
|
|
30
|
+
__all__ = ["SyncForge", "SyncResult", "SyncForgeError", "AuthError", "TableNotFoundError"]
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SyncForge Python SDK — Client
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import threading
|
|
7
|
+
import urllib.request
|
|
8
|
+
import urllib.error
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
from typing import Optional, Union, List
|
|
12
|
+
|
|
13
|
+
from .result import SyncResult
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
SyncForgeError, AuthError, TableNotFoundError,
|
|
16
|
+
RateLimitError, NetworkError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Default production base URL — override for self-hosted or local dev
|
|
20
|
+
DEFAULT_BASE_URL = "https://syncforge.dev/api"
|
|
21
|
+
|
|
22
|
+
# Per-request timeout (seconds)
|
|
23
|
+
DEFAULT_TIMEOUT = 10
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SyncForge:
|
|
27
|
+
"""
|
|
28
|
+
Official Python client for the SyncForge data sync platform.
|
|
29
|
+
|
|
30
|
+
Create one instance per project (API key) — typically in a
|
|
31
|
+
``syncforge.py`` file at your project root, then import it
|
|
32
|
+
wherever needed::
|
|
33
|
+
|
|
34
|
+
# syncforge.py (project root)
|
|
35
|
+
import os
|
|
36
|
+
from syncforge import SyncForge
|
|
37
|
+
|
|
38
|
+
sf = SyncForge(api_key=os.environ.get('SYNCFORGE_API_KEY', 'sf_live_...'))
|
|
39
|
+
|
|
40
|
+
# views.py / routes.py / any handler
|
|
41
|
+
from syncforge import sf
|
|
42
|
+
sf.refresh('products')
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
api_key: Your SyncForge API key (starts with ``sf_live_``).
|
|
46
|
+
base_url: Override the base URL (useful for local dev / self-hosted).
|
|
47
|
+
timeout: HTTP timeout in seconds (default: 10).
|
|
48
|
+
silent: If ``True``, all errors are suppressed and logged instead
|
|
49
|
+
of raised. Use this in production so a SyncForge outage
|
|
50
|
+
never breaks your app flow. Default: ``False``.
|
|
51
|
+
async_mode: If ``True``, every ``refresh()`` call runs in a background
|
|
52
|
+
thread and returns immediately. Default: ``False``.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
api_key: str,
|
|
58
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
59
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
60
|
+
silent: bool = False,
|
|
61
|
+
async_mode: bool = False,
|
|
62
|
+
):
|
|
63
|
+
if not api_key:
|
|
64
|
+
raise ValueError("api_key is required.")
|
|
65
|
+
|
|
66
|
+
self._api_key = api_key.strip()
|
|
67
|
+
self._base_url = base_url.rstrip("/")
|
|
68
|
+
self._timeout = timeout
|
|
69
|
+
self._silent = silent
|
|
70
|
+
self._async = async_mode
|
|
71
|
+
|
|
72
|
+
# ── Public API ────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
def refresh(self, *tables: str) -> Union[SyncResult, List[SyncResult], None]:
|
|
75
|
+
"""
|
|
76
|
+
Trigger a data sync for one or more tables.
|
|
77
|
+
|
|
78
|
+
After any database write, call this to tell SyncForge to
|
|
79
|
+
broadcast the change to all connected clients.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
*tables: One or more table names registered in your SyncForge
|
|
83
|
+
dashboard (e.g. ``'products'``, ``'orders'``).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A :class:`SyncResult` for a single table, or a list of
|
|
87
|
+
:class:`SyncResult` objects when multiple tables are passed.
|
|
88
|
+
Returns ``None`` when ``async_mode=True`` (fire-and-forget).
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
:class:`AuthError`: Invalid or missing API key.
|
|
92
|
+
:class:`TableNotFoundError`: Table not registered in dashboard.
|
|
93
|
+
:class:`NetworkError`: Connection / timeout failure.
|
|
94
|
+
:class:`SyncForgeError`: Any other server-side error.
|
|
95
|
+
|
|
96
|
+
Examples::
|
|
97
|
+
|
|
98
|
+
# Single table
|
|
99
|
+
sf.refresh('products')
|
|
100
|
+
|
|
101
|
+
# Multiple tables at once
|
|
102
|
+
sf.refresh('products', 'categories', 'inventory')
|
|
103
|
+
|
|
104
|
+
# With result inspection
|
|
105
|
+
result = sf.refresh('products')
|
|
106
|
+
if result.ok:
|
|
107
|
+
print(f"{result.calls_saved} DB calls saved!")
|
|
108
|
+
"""
|
|
109
|
+
if not tables:
|
|
110
|
+
raise ValueError("At least one table name is required.")
|
|
111
|
+
|
|
112
|
+
if self._async:
|
|
113
|
+
t = threading.Thread(
|
|
114
|
+
target=self._refresh_all,
|
|
115
|
+
args=(tables,),
|
|
116
|
+
daemon=True,
|
|
117
|
+
)
|
|
118
|
+
t.start()
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
results = self._refresh_all(tables)
|
|
122
|
+
return results[0] if len(results) == 1 else results
|
|
123
|
+
|
|
124
|
+
def ping(self) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Check that your API key is valid and SyncForge is reachable.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
``True`` if the health endpoint responds, ``False`` otherwise.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
url = f"{self._base_url}/v1/health/"
|
|
133
|
+
self._request("GET", url)
|
|
134
|
+
return True
|
|
135
|
+
except Exception:
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def project_info(self) -> dict:
|
|
139
|
+
"""
|
|
140
|
+
Fetch project metadata and registered tables for this API key.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
dict with ``project``, ``slug``, ``tables``, ``active_keys``.
|
|
144
|
+
"""
|
|
145
|
+
url = f"{self._base_url}/v1/project/"
|
|
146
|
+
return self._request("GET", url)
|
|
147
|
+
|
|
148
|
+
def list_tables(self) -> list:
|
|
149
|
+
"""
|
|
150
|
+
Return all tables registered in this project.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of dicts with ``table_name``, ``sync_mode``, ``rows_count``,
|
|
154
|
+
``database_calls_saved``.
|
|
155
|
+
"""
|
|
156
|
+
url = f"{self._base_url}/v1/tables/"
|
|
157
|
+
data = self._request("GET", url)
|
|
158
|
+
return data.get("tables", [])
|
|
159
|
+
|
|
160
|
+
# ── Internal ──────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
def _refresh_all(self, tables: tuple) -> List[SyncResult]:
|
|
163
|
+
results = []
|
|
164
|
+
for table in tables:
|
|
165
|
+
try:
|
|
166
|
+
result = self._sync_one(table)
|
|
167
|
+
results.append(result)
|
|
168
|
+
except SyncForgeError as exc:
|
|
169
|
+
if self._silent:
|
|
170
|
+
import warnings
|
|
171
|
+
warnings.warn(f"[SyncForge] {exc} (table={table!r})", stacklevel=4)
|
|
172
|
+
results.append(SyncResult(
|
|
173
|
+
ok=False, table=table, message=str(exc),
|
|
174
|
+
status_code=getattr(exc, 'status_code', None) or 0,
|
|
175
|
+
))
|
|
176
|
+
else:
|
|
177
|
+
raise
|
|
178
|
+
return results
|
|
179
|
+
|
|
180
|
+
def _sync_one(self, table: str) -> SyncResult:
|
|
181
|
+
table = table.strip().lower()
|
|
182
|
+
if not table:
|
|
183
|
+
raise ValueError("Table name cannot be empty.")
|
|
184
|
+
|
|
185
|
+
url = f"{self._base_url}/v1/sync/{table}/"
|
|
186
|
+
data = self._request("POST", url)
|
|
187
|
+
|
|
188
|
+
return SyncResult(
|
|
189
|
+
ok = data.get("status") == "ok",
|
|
190
|
+
table = data.get("table", table),
|
|
191
|
+
project = data.get("project"),
|
|
192
|
+
sync_mode = data.get("sync_mode"),
|
|
193
|
+
calls_saved = data.get("database_calls_saved", 0),
|
|
194
|
+
message = data.get("message", ""),
|
|
195
|
+
raw = data,
|
|
196
|
+
status_code = 200,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def _request(self, method: str, url: str) -> dict:
|
|
200
|
+
headers = {
|
|
201
|
+
"X-API-Key": self._api_key,
|
|
202
|
+
"Content-Type": "application/json",
|
|
203
|
+
"Accept": "application/json",
|
|
204
|
+
"User-Agent": "syncforge-python/1.0.0",
|
|
205
|
+
}
|
|
206
|
+
body = b"" if method == "GET" else b"{}"
|
|
207
|
+
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
with urllib.request.urlopen(req, timeout=self._timeout) as resp:
|
|
211
|
+
raw = resp.read().decode("utf-8")
|
|
212
|
+
return json.loads(raw) if raw else {}
|
|
213
|
+
|
|
214
|
+
except urllib.error.HTTPError as exc:
|
|
215
|
+
raw = exc.read().decode("utf-8", errors="replace")
|
|
216
|
+
data = {}
|
|
217
|
+
try:
|
|
218
|
+
data = json.loads(raw)
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
221
|
+
|
|
222
|
+
error_msg = data.get("error", raw or exc.reason)
|
|
223
|
+
code = exc.code
|
|
224
|
+
|
|
225
|
+
if code == 401:
|
|
226
|
+
raise AuthError(f"Invalid API key: {error_msg}", status_code=code)
|
|
227
|
+
if code == 404:
|
|
228
|
+
raise TableNotFoundError(
|
|
229
|
+
f"Table not found — register it in your SyncForge dashboard. {error_msg}",
|
|
230
|
+
status_code=code,
|
|
231
|
+
)
|
|
232
|
+
if code == 429:
|
|
233
|
+
raise RateLimitError(f"Rate limit exceeded: {error_msg}", status_code=code)
|
|
234
|
+
raise SyncForgeError(f"Server error {code}: {error_msg}", status_code=code)
|
|
235
|
+
|
|
236
|
+
except urllib.error.URLError as exc:
|
|
237
|
+
raise NetworkError(
|
|
238
|
+
f"Could not connect to SyncForge ({url}): {exc.reason}"
|
|
239
|
+
) from exc
|
|
240
|
+
|
|
241
|
+
except TimeoutError:
|
|
242
|
+
raise NetworkError(
|
|
243
|
+
f"Request to SyncForge timed out after {self._timeout}s."
|
|
244
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
class SyncForgeError(Exception):
|
|
2
|
+
"""Base exception for all SyncForge SDK errors."""
|
|
3
|
+
def __init__(self, message: str, status_code: int = None):
|
|
4
|
+
super().__init__(message)
|
|
5
|
+
self.status_code = status_code
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthError(SyncForgeError):
|
|
9
|
+
"""Raised when the API key is missing, invalid, or revoked."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TableNotFoundError(SyncForgeError):
|
|
13
|
+
"""Raised when the table is not registered in the SyncForge dashboard."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RateLimitError(SyncForgeError):
|
|
17
|
+
"""Raised when the API rate limit is exceeded."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NetworkError(SyncForgeError):
|
|
21
|
+
"""Raised when the HTTP request fails (timeout, DNS, etc.)."""
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class SyncResult:
|
|
7
|
+
"""
|
|
8
|
+
Returned by SyncForge.refresh().
|
|
9
|
+
|
|
10
|
+
Attributes:
|
|
11
|
+
ok True if the sync was accepted by the server.
|
|
12
|
+
table The table name that was synced.
|
|
13
|
+
project The project name associated with the API key.
|
|
14
|
+
sync_mode Human-readable sync mode label.
|
|
15
|
+
calls_saved Cumulative database calls saved for this table.
|
|
16
|
+
message Server response message.
|
|
17
|
+
raw Full raw JSON response from the server.
|
|
18
|
+
status_code HTTP status code.
|
|
19
|
+
"""
|
|
20
|
+
ok: bool
|
|
21
|
+
table: str
|
|
22
|
+
project: Optional[str] = None
|
|
23
|
+
sync_mode: Optional[str] = None
|
|
24
|
+
calls_saved: int = 0
|
|
25
|
+
message: str = ""
|
|
26
|
+
raw: Dict[str, Any] = field(default_factory=dict)
|
|
27
|
+
status_code: int = 200
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
status = "✓" if self.ok else "✗"
|
|
31
|
+
return f"<SyncResult {status} table={self.table!r} calls_saved={self.calls_saved}>"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syncforge
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for SyncForge — control exactly when data syncs between your database and clients.
|
|
5
|
+
Author-email: SyncForge <sureshdulupolai@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/sureshdulupolai/syncforge
|
|
8
|
+
Project-URL: Documentation, https://syncforge.dev/docs/
|
|
9
|
+
Project-URL: Repository, https://github.com/sureshdulupolai/syncforge
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/sureshdulupolai/syncforge/issues
|
|
11
|
+
Keywords: syncforge,sync,database,cache,invalidation,django,fastapi
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# SyncForge Python SDK
|
|
29
|
+
|
|
30
|
+
[](https://pypi.org/project/syncforge/)
|
|
31
|
+
[](https://pypi.org/project/syncforge/)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
|
|
34
|
+
Official Python SDK for the [SyncForge](https://syncforge.dev) data sync platform.
|
|
35
|
+
Control exactly when data syncs between your database and client applications — no polling, no wasted DB calls.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install syncforge
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Zero external dependencies.** Uses only Python stdlib (`urllib`, `json`, `threading`).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from syncforge import SyncForge
|
|
53
|
+
|
|
54
|
+
sf = SyncForge(api_key='sf_live_YOUR_KEY')
|
|
55
|
+
|
|
56
|
+
# After any DB write — notify all connected clients
|
|
57
|
+
sf.refresh('products')
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## The `syncforge.py` Pattern (Recommended)
|
|
63
|
+
|
|
64
|
+
Place a `syncforge.py` file at your project root — same level as `manage.py` or `main.py`.
|
|
65
|
+
This mirrors the Celery pattern and gives you a single shared instance.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
# syncforge.py (project root)
|
|
69
|
+
import os
|
|
70
|
+
from syncforge import SyncForge
|
|
71
|
+
|
|
72
|
+
sf = SyncForge(
|
|
73
|
+
api_key=os.environ.get('SYNCFORGE_API_KEY', 'sf_live_YOUR_KEY')
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Then import `sf` anywhere:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
# views.py / routes.py
|
|
81
|
+
from syncforge import sf
|
|
82
|
+
|
|
83
|
+
def create_product(request):
|
|
84
|
+
Product.objects.create(name='New Item', price=99.99)
|
|
85
|
+
sf.refresh('products') # one line — all clients updated
|
|
86
|
+
return JsonResponse({'status': 'created'})
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## API Reference
|
|
92
|
+
|
|
93
|
+
### `SyncForge(api_key, base_url, timeout, silent, async_mode)`
|
|
94
|
+
|
|
95
|
+
| Parameter | Default | Description |
|
|
96
|
+
|--------------|-------------------------------|------------------------------------------------------|
|
|
97
|
+
| `api_key` | required | Your API key (`sf_live_...`) |
|
|
98
|
+
| `base_url` | `https://syncforge.dev/api` | Override for local dev / self-hosted |
|
|
99
|
+
| `timeout` | `10` | HTTP timeout in seconds |
|
|
100
|
+
| `silent` | `False` | Suppress errors — logs warnings instead of raising |
|
|
101
|
+
| `async_mode` | `False` | Fire-and-forget — refresh runs in a background thread|
|
|
102
|
+
|
|
103
|
+
### `sf.refresh(*tables)` → `SyncResult | list[SyncResult]`
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
sf.refresh('products') # single table
|
|
107
|
+
sf.refresh('products', 'categories', 'orders') # multiple at once
|
|
108
|
+
|
|
109
|
+
result = sf.refresh('products')
|
|
110
|
+
print(result.ok) # True
|
|
111
|
+
print(result.calls_saved) # 1854211
|
|
112
|
+
print(result.sync_mode) # 'Event — On INSERT / UPDATE / DELETE'
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `sf.ping()` → `bool`
|
|
116
|
+
Health check — returns `True` if SyncForge is reachable.
|
|
117
|
+
|
|
118
|
+
### `sf.project_info()` → `dict`
|
|
119
|
+
Returns project metadata and all registered tables.
|
|
120
|
+
|
|
121
|
+
### `sf.list_tables()` → `list`
|
|
122
|
+
Lists all tables with their sync mode and stats.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Django Integration
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# syncforge.py (next to manage.py)
|
|
130
|
+
import os
|
|
131
|
+
from syncforge import SyncForge
|
|
132
|
+
sf = SyncForge(api_key=os.environ.get('SYNCFORGE_API_KEY'))
|
|
133
|
+
|
|
134
|
+
# myapp/views.py
|
|
135
|
+
from syncforge import sf
|
|
136
|
+
|
|
137
|
+
def update_products(request):
|
|
138
|
+
Product.objects.filter(on_sale=True).update(price=F('price') * 0.9)
|
|
139
|
+
sf.refresh('products')
|
|
140
|
+
return JsonResponse({'status': 'updated'})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## FastAPI Integration
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from fastapi import FastAPI
|
|
147
|
+
from syncforge import sf
|
|
148
|
+
|
|
149
|
+
app = FastAPI()
|
|
150
|
+
|
|
151
|
+
@app.post("/products/")
|
|
152
|
+
async def create_product(name: str, price: float):
|
|
153
|
+
db.execute("INSERT INTO products ...")
|
|
154
|
+
sf.refresh('products')
|
|
155
|
+
return {"status": "ok"}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Production Tips
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
# Silent mode — SyncForge errors never crash your app
|
|
164
|
+
sf = SyncForge(api_key='sf_live_...', silent=True)
|
|
165
|
+
|
|
166
|
+
# Async mode — fire-and-forget, returns immediately
|
|
167
|
+
sf = SyncForge(api_key='sf_live_...', async_mode=True)
|
|
168
|
+
sf.refresh('products') # returns None, syncs in background
|
|
169
|
+
|
|
170
|
+
# Override base URL for local development
|
|
171
|
+
sf = SyncForge(api_key='sf_live_...', base_url='http://localhost:8000/api')
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Error Handling
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from syncforge import SyncForge, AuthError, TableNotFoundError, NetworkError
|
|
180
|
+
|
|
181
|
+
sf = SyncForge(api_key='sf_live_...')
|
|
182
|
+
try:
|
|
183
|
+
sf.refresh('products')
|
|
184
|
+
except AuthError:
|
|
185
|
+
print("Invalid API key")
|
|
186
|
+
except TableNotFoundError:
|
|
187
|
+
print("Register the table in your SyncForge dashboard first")
|
|
188
|
+
except NetworkError:
|
|
189
|
+
print("Could not reach SyncForge — check your internet connection")
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT — see [LICENSE](LICENSE)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
syncforge/__init__.py
|
|
5
|
+
syncforge/client.py
|
|
6
|
+
syncforge/exceptions.py
|
|
7
|
+
syncforge/result.py
|
|
8
|
+
syncforge.egg-info/PKG-INFO
|
|
9
|
+
syncforge.egg-info/SOURCES.txt
|
|
10
|
+
syncforge.egg-info/dependency_links.txt
|
|
11
|
+
syncforge.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
syncforge
|