syncforge 1.0.1__tar.gz → 1.0.3__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.1/syncforge.egg-info → syncforge-1.0.3}/PKG-INFO +1 -1
- {syncforge-1.0.1 → syncforge-1.0.3}/pyproject.toml +1 -1
- {syncforge-1.0.1 → syncforge-1.0.3}/syncforge/client.py +26 -0
- syncforge-1.0.3/syncforge/django.py +78 -0
- syncforge-1.0.3/syncforge/middleware.py +86 -0
- {syncforge-1.0.1 → syncforge-1.0.3/syncforge.egg-info}/PKG-INFO +1 -1
- {syncforge-1.0.1 → syncforge-1.0.3}/syncforge.egg-info/SOURCES.txt +2 -0
- {syncforge-1.0.1 → syncforge-1.0.3}/LICENSE +0 -0
- {syncforge-1.0.1 → syncforge-1.0.3}/README.md +0 -0
- {syncforge-1.0.1 → syncforge-1.0.3}/setup.cfg +0 -0
- {syncforge-1.0.1 → syncforge-1.0.3}/syncforge/__init__.py +0 -0
- {syncforge-1.0.1 → syncforge-1.0.3}/syncforge/exceptions.py +0 -0
- {syncforge-1.0.1 → syncforge-1.0.3}/syncforge/result.py +0 -0
- {syncforge-1.0.1 → syncforge-1.0.3}/syncforge.egg-info/dependency_links.txt +0 -0
- {syncforge-1.0.1 → syncforge-1.0.3}/syncforge.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "syncforge"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.3"
|
|
8
8
|
description = "Official Python SDK for SyncForge — control exactly when data syncs between your database and clients."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -183,6 +183,32 @@ class SyncForge:
|
|
|
183
183
|
return False
|
|
184
184
|
raise
|
|
185
185
|
|
|
186
|
+
def delete_table(self, table_name: str) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Delete a registered table from the SyncForge dashboard programmatically.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
table_name: The name of the table to delete.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
bool: True if it was deleted, False otherwise.
|
|
195
|
+
"""
|
|
196
|
+
table_name = table_name.strip().lower()
|
|
197
|
+
if not table_name:
|
|
198
|
+
raise ValueError("Table name cannot be empty.")
|
|
199
|
+
|
|
200
|
+
import urllib.parse
|
|
201
|
+
url = f"{self._base_url}/v1/tables/?table_name={urllib.parse.quote(table_name)}"
|
|
202
|
+
try:
|
|
203
|
+
res = self._request("DELETE", url)
|
|
204
|
+
return res.get("deleted", False)
|
|
205
|
+
except SyncForgeError as exc:
|
|
206
|
+
if self._silent:
|
|
207
|
+
import warnings
|
|
208
|
+
warnings.warn(f"[SyncForge] delete_table failed: {exc}", stacklevel=2)
|
|
209
|
+
return False
|
|
210
|
+
raise
|
|
211
|
+
|
|
186
212
|
# ── Internal ──────────────────────────────────────────────────────────────
|
|
187
213
|
|
|
188
214
|
def _refresh_all(self, tables: tuple) -> List[SyncResult]:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django integration for SyncForge.
|
|
3
|
+
Provides the @sync_model decorator to auto-sync Django models.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from django.db.models.signals import post_save, post_delete
|
|
9
|
+
from django.apps import apps
|
|
10
|
+
HAS_DJANGO = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
HAS_DJANGO = False
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("syncforge")
|
|
15
|
+
|
|
16
|
+
_registered_tables = set()
|
|
17
|
+
|
|
18
|
+
def sync_model(sf_client, sync_mode='event'):
|
|
19
|
+
"""
|
|
20
|
+
Class decorator for Django models to automatically sync with SyncForge.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
from syncforge import sf
|
|
24
|
+
from syncforge.django import sync_model
|
|
25
|
+
|
|
26
|
+
@sync_model(sf)
|
|
27
|
+
class Product(models.Model):
|
|
28
|
+
name = models.CharField(max_length=100)
|
|
29
|
+
"""
|
|
30
|
+
def decorator(cls):
|
|
31
|
+
if not HAS_DJANGO:
|
|
32
|
+
raise ImportError("Django is not installed. Cannot use @sync_model.")
|
|
33
|
+
|
|
34
|
+
table_name = cls._meta.db_table
|
|
35
|
+
|
|
36
|
+
# 1. Register table on SyncForge dashboard
|
|
37
|
+
try:
|
|
38
|
+
sf_client.create_table(table_name, sync_mode=sync_mode)
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.warning(f"[SyncForge] Failed to register table {table_name}: {e}")
|
|
41
|
+
|
|
42
|
+
_registered_tables.add(table_name)
|
|
43
|
+
|
|
44
|
+
# 2. Hook into ORM signals to trigger syncs automatically
|
|
45
|
+
def _trigger_sync(sender, **kwargs):
|
|
46
|
+
try:
|
|
47
|
+
sf_client.refresh(table_name)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.error(f"[SyncForge] Failed to trigger sync for {table_name}: {e}")
|
|
50
|
+
|
|
51
|
+
# Connect signals
|
|
52
|
+
post_save.connect(_trigger_sync, sender=cls, weak=False, dispatch_uid=f"sf_save_{table_name}")
|
|
53
|
+
post_delete.connect(_trigger_sync, sender=cls, weak=False, dispatch_uid=f"sf_delete_{table_name}")
|
|
54
|
+
|
|
55
|
+
return cls
|
|
56
|
+
return decorator
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def sync_migrations(sf_client):
|
|
60
|
+
"""
|
|
61
|
+
Removes tables from the SyncForge dashboard that no longer exist in your Django project.
|
|
62
|
+
Call this inside an AppConfig.ready() or after your migrations run.
|
|
63
|
+
"""
|
|
64
|
+
if not HAS_DJANGO:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
active_tables = {model._meta.db_table for model in apps.get_models()}
|
|
69
|
+
|
|
70
|
+
# Fetch current registered tables from SyncForge
|
|
71
|
+
sf_tables = sf_client.list_tables()
|
|
72
|
+
for t in sf_tables:
|
|
73
|
+
t_name = t.get('table_name')
|
|
74
|
+
if t_name and t_name not in active_tables:
|
|
75
|
+
sf_client.delete_table(t_name)
|
|
76
|
+
logger.info(f"[SyncForge] Cleaned up deleted table: {t_name}")
|
|
77
|
+
except Exception as e:
|
|
78
|
+
logger.warning(f"[SyncForge] sync_migrations cleanup failed: {e}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SyncForge Security Middleware
|
|
3
|
+
Professional-grade request/response logging and basic WAF protection for Django apps.
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from django.http import HttpResponseForbidden
|
|
10
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
11
|
+
HAS_DJANGO = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
HAS_DJANGO = False
|
|
14
|
+
class MiddlewareMixin:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger('syncforge.security')
|
|
18
|
+
|
|
19
|
+
class SyncForgeSecurityMiddleware(MiddlewareMixin):
|
|
20
|
+
"""
|
|
21
|
+
Drop-in security and logging middleware.
|
|
22
|
+
Add 'syncforge.middleware.SyncForgeSecurityMiddleware' to your MIDDLEWARE setting.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Common malicious patterns to block automatically
|
|
26
|
+
MALICIOUS_PATTERNS = [
|
|
27
|
+
'../', # Path Traversal
|
|
28
|
+
'<script', # XSS
|
|
29
|
+
'javascript:', # XSS
|
|
30
|
+
'UNION SELECT', # SQLi
|
|
31
|
+
'OR 1=1', # SQLi
|
|
32
|
+
'-- ', # SQL comment injection
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
def process_request(self, request):
|
|
36
|
+
if not HAS_DJANGO:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
request._syncforge_start_time = time.time()
|
|
40
|
+
|
|
41
|
+
# Basic WAF (Web Application Firewall) checks
|
|
42
|
+
if self._is_malicious(request):
|
|
43
|
+
ip = request.META.get('HTTP_X_FORWARDED_FOR') or request.META.get('REMOTE_ADDR')
|
|
44
|
+
logger.warning(f"[SyncForge Security] Blocked malicious request from {ip} on {request.path}")
|
|
45
|
+
return HttpResponseForbidden("Blocked by SyncForge Security Firewall.")
|
|
46
|
+
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def process_response(self, request, response):
|
|
50
|
+
if not HAS_DJANGO:
|
|
51
|
+
return response
|
|
52
|
+
|
|
53
|
+
# Logging
|
|
54
|
+
if hasattr(request, '_syncforge_start_time'):
|
|
55
|
+
duration = (time.time() - request._syncforge_start_time) * 1000
|
|
56
|
+
method = request.method
|
|
57
|
+
path = request.path
|
|
58
|
+
status = response.status_code
|
|
59
|
+
|
|
60
|
+
# Format: [POST] /api/users/ - 200 OK (45.2ms)
|
|
61
|
+
if status >= 500:
|
|
62
|
+
level = logger.error
|
|
63
|
+
elif status >= 400:
|
|
64
|
+
level = logger.warning
|
|
65
|
+
else:
|
|
66
|
+
level = logger.info
|
|
67
|
+
|
|
68
|
+
level(f"[SyncForge] [{method}] {path} - {status} ({duration:.1f}ms)")
|
|
69
|
+
|
|
70
|
+
# Inject Security Headers
|
|
71
|
+
response['X-Powered-By'] = 'SyncForge'
|
|
72
|
+
response['X-Content-Type-Options'] = 'nosniff'
|
|
73
|
+
response['X-XSS-Protection'] = '1; mode=block'
|
|
74
|
+
|
|
75
|
+
return response
|
|
76
|
+
|
|
77
|
+
def _is_malicious(self, request):
|
|
78
|
+
path = request.path.upper()
|
|
79
|
+
query = request.META.get('QUERY_STRING', '').upper()
|
|
80
|
+
|
|
81
|
+
for pattern in self.MALICIOUS_PATTERNS:
|
|
82
|
+
p = pattern.upper()
|
|
83
|
+
if p in path or p in query:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
return False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|