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.
@@ -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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/syncforge)](https://pypi.org/project/syncforge/)
31
+ [![Python](https://img.shields.io/pypi/pyversions/syncforge)](https://pypi.org/project/syncforge/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/syncforge)](https://pypi.org/project/syncforge/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/syncforge)](https://pypi.org/project/syncforge/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/syncforge)](https://pypi.org/project/syncforge/)
31
+ [![Python](https://img.shields.io/pypi/pyversions/syncforge)](https://pypi.org/project/syncforge/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ syncforge