nia-sync 0.1.6__py3-none-any.whl → 0.1.7__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nia-sync
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Keep your local files in sync with Nia Cloud
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: typer>=0.9.0
@@ -2,10 +2,10 @@ auth.py,sha256=n0ezRqIbz3kZYcyIjH47_MDKgwNgAaVr0Y2NEfRxa50,5704
2
2
  config.py,sha256=dbnkDihOiJL0-WBzTyxDXM2AIduwsvZycgQuhAoYglU,7637
3
3
  extractor.py,sha256=IxpFGOlklusmPs2Jz398DH1hVVYpciwQN_HDyxpmm_Q,29193
4
4
  main.py,sha256=f1e2q99QfI_U0YyYgsC75P1wHebKuweWa0pYEAQPMO8,26582
5
- sync.py,sha256=jdVPceViN5uxoRCr5ukCVumBhvl3t53xkJnvZwvKmdw,10710
5
+ sync.py,sha256=82w71raeH66DlwmoClWfNS4-NiCfZ-TA8ozK9O21HZk,12843
6
6
  watcher.py,sha256=JmsN9uR7Ss1mDC-kApXL6Hg_wQZWTsO7rRIFkQu8GbM,9978
7
- nia_sync-0.1.6.dist-info/METADATA,sha256=BpVvABRhWNRG4dcuFh06zVr7e7Zba2jh95GcR7lZOts,246
8
- nia_sync-0.1.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
- nia_sync-0.1.6.dist-info/entry_points.txt,sha256=Fx8TIOgXqWdZzZEkEateDtcNfgnwuPW4jZTqlEUrHVs,33
10
- nia_sync-0.1.6.dist-info/top_level.txt,sha256=_ZWBugSHWwSpLXYJAcF6TlWmzECu18k0y1-EX27jtBw,40
11
- nia_sync-0.1.6.dist-info/RECORD,,
7
+ nia_sync-0.1.7.dist-info/METADATA,sha256=YqDiD8s4_7I1uSWIpWm_ABGqqsypjxWF2U8FT08LuB0,246
8
+ nia_sync-0.1.7.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
9
+ nia_sync-0.1.7.dist-info/entry_points.txt,sha256=Fx8TIOgXqWdZzZEkEateDtcNfgnwuPW4jZTqlEUrHVs,33
10
+ nia_sync-0.1.7.dist-info/top_level.txt,sha256=_ZWBugSHWwSpLXYJAcF6TlWmzECu18k0y1-EX27jtBw,40
11
+ nia_sync-0.1.7.dist-info/RECORD,,
sync.py CHANGED
@@ -15,7 +15,7 @@ from typing import Any
15
15
  import httpx
16
16
 
17
17
  from config import API_BASE_URL, get_api_key
18
- from extractor import extract_incremental, detect_source_type
18
+ from extractor import extract_incremental, detect_source_type, TYPE_FOLDER
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -26,10 +26,31 @@ MAX_BATCH_SIZE_BYTES = 4 * 1024 * 1024 # 4MB max payload per batch
26
26
  MAX_RETRIES = 4
27
27
  RETRY_BASE_DELAY = 1.5
28
28
  RETRY_MAX_DELAY = 15.0
29
+ FOLDER_CURSOR_VERSION = 1
29
30
 
30
31
  # Reusable client for connection pooling
31
32
  _http_client: httpx.Client | None = None
32
33
 
34
+ def _normalize_folder_cursor(
35
+ path: str,
36
+ cursor: dict[str, Any] | None,
37
+ ) -> tuple[dict[str, Any], str | None]:
38
+ if not cursor:
39
+ return {}, "missing"
40
+
41
+ cursor_version = cursor.get("cursor_version")
42
+ root_path = cursor.get("root_path")
43
+ normalized_root = os.path.abspath(os.path.expanduser(root_path)) if root_path else None
44
+
45
+ if cursor_version != FOLDER_CURSOR_VERSION:
46
+ return {}, "version_mismatch"
47
+ if not normalized_root:
48
+ return {}, "missing_root_path"
49
+ if normalized_root != path:
50
+ return {}, "root_path_changed"
51
+
52
+ return cursor, None
53
+
33
54
  def get_http_client() -> httpx.Client:
34
55
  """Get or create HTTP client with connection pooling."""
35
56
  global _http_client
@@ -88,8 +109,9 @@ def sync_source(source: dict[str, Any]) -> dict[str, Any]:
88
109
  "message": "No local path configured",
89
110
  }
90
111
 
91
- # Expand ~ in path
112
+ # Expand ~ and normalize path
92
113
  path = os.path.expanduser(path)
114
+ path = os.path.abspath(path)
93
115
 
94
116
  # Validate path exists
95
117
  if not os.path.exists(path):
@@ -105,6 +127,12 @@ def sync_source(source: dict[str, Any]) -> dict[str, Any]:
105
127
  if not detected_type:
106
128
  detected_type = detect_source_type(path)
107
129
 
130
+ cursor_reset_reason = None
131
+ if detected_type == TYPE_FOLDER:
132
+ cursor, cursor_reset_reason = _normalize_folder_cursor(path, cursor)
133
+ if cursor_reset_reason:
134
+ logger.info(f"Resetting folder sync cursor for {path} ({cursor_reset_reason})")
135
+
108
136
  logger.info(f"Syncing {path} (type={detected_type})")
109
137
 
110
138
  try:
@@ -116,10 +144,38 @@ def sync_source(source: dict[str, Any]) -> dict[str, Any]:
116
144
  )
117
145
 
118
146
  files = extraction_result.get("files", [])
119
- new_cursor = extraction_result.get("cursor", {})
147
+ new_cursor = dict(extraction_result.get("cursor", {}))
120
148
  stats = extraction_result.get("stats", {})
121
149
 
150
+ if detected_type == TYPE_FOLDER:
151
+ new_cursor["cursor_version"] = FOLDER_CURSOR_VERSION
152
+ new_cursor["root_path"] = path
153
+
122
154
  if not files:
155
+ if detected_type == TYPE_FOLDER and cursor_reset_reason:
156
+ cursor_update = new_cursor
157
+ upload_result = upload_sync_data(
158
+ local_folder_id=local_folder_id,
159
+ files=[],
160
+ cursor=cursor_update,
161
+ stats=stats,
162
+ is_final_batch=True,
163
+ )
164
+ if upload_result.get("status") == "ok":
165
+ source["cursor"] = cursor_update
166
+ return {
167
+ "path": path,
168
+ "status": "success",
169
+ "added": 0,
170
+ "message": "No new data (cursor updated)",
171
+ }
172
+ report_sync_error(local_folder_id, upload_result.get("message", "Upload failed"), path)
173
+ return {
174
+ "path": path,
175
+ "status": "error",
176
+ "error": upload_result.get("message", "Upload failed"),
177
+ }
178
+
123
179
  logger.info(f"No new data to sync for {path}")
124
180
  return {
125
181
  "path": path,