nia-sync 0.1.6__py3-none-any.whl → 0.1.8__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.
- api_client.py +104 -0
- auth.py +3 -3
- config.py +99 -116
- extractor.py +30 -4
- main.py +776 -74
- {nia_sync-0.1.6.dist-info → nia_sync-0.1.8.dist-info}/METADATA +1 -1
- nia_sync-0.1.8.dist-info/RECORD +13 -0
- {nia_sync-0.1.6.dist-info → nia_sync-0.1.8.dist-info}/top_level.txt +2 -0
- sync.py +62 -6
- ui.py +119 -0
- nia_sync-0.1.6.dist-info/RECORD +0 -11
- {nia_sync-0.1.6.dist-info → nia_sync-0.1.8.dist-info}/WHEEL +0 -0
- {nia_sync-0.1.6.dist-info → nia_sync-0.1.8.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
api_client.py,sha256=wg3oRixlyzR_GmJDrmpVemJb0GI1ixd7UceSyHBR52s,2712
|
|
2
|
+
auth.py,sha256=-DeD3azCHlqqI7zySKEhsxeeT8kJYsfzpuqAsGKyA1s,5720
|
|
3
|
+
config.py,sha256=_zeDLBggazFGlR1LUitMNTiMpqHMxZBfcUJ-C6tXgdY,7233
|
|
4
|
+
extractor.py,sha256=KhLTRJQAgbsZ_qqXzBjU5Qh2R83r2vmNMF-bB4Pzbqw,30279
|
|
5
|
+
main.py,sha256=owUa1k_f3BWlARv7erfxhhvl-aIHWbjTPhD0Ln3T9sc,52672
|
|
6
|
+
sync.py,sha256=gHxA0pn2_I7habRwWJ33tSit0aRDxEZHA2Gdyt6Fe20,12859
|
|
7
|
+
ui.py,sha256=yxlRz2VIkiaMTzV95QhsV3Uojt3Ns6_QXLCSlUWJPr0,3456
|
|
8
|
+
watcher.py,sha256=JmsN9uR7Ss1mDC-kApXL6Hg_wQZWTsO7rRIFkQu8GbM,9978
|
|
9
|
+
nia_sync-0.1.8.dist-info/METADATA,sha256=Phtagx-S9wDy-ARK6C3ZkN-NPlgjoSe3V_R5qKk5hMo,246
|
|
10
|
+
nia_sync-0.1.8.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
nia_sync-0.1.8.dist-info/entry_points.txt,sha256=Fx8TIOgXqWdZzZEkEateDtcNfgnwuPW4jZTqlEUrHVs,33
|
|
12
|
+
nia_sync-0.1.8.dist-info/top_level.txt,sha256=nHlEpqudkMdi7d4wpt_qUI-vjVay1X5CaB6foOQ85Dg,54
|
|
13
|
+
nia_sync-0.1.8.dist-info/RECORD,,
|
sync.py
CHANGED
|
@@ -14,8 +14,8 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
import httpx
|
|
16
16
|
|
|
17
|
-
from config import
|
|
18
|
-
from extractor import extract_incremental, detect_source_type
|
|
17
|
+
from config import get_api_base_url, get_api_key
|
|
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 ~
|
|
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,
|
|
@@ -199,7 +255,7 @@ def upload_sync_data(
|
|
|
199
255
|
client = get_http_client()
|
|
200
256
|
response = _post_with_retries(
|
|
201
257
|
client=client,
|
|
202
|
-
url=f"{
|
|
258
|
+
url=f"{get_api_base_url()}/v2/daemon/sync",
|
|
203
259
|
headers={"Authorization": f"Bearer {api_key}"},
|
|
204
260
|
payload={
|
|
205
261
|
"local_folder_id": local_folder_id,
|
|
@@ -278,7 +334,7 @@ def report_sync_error(local_folder_id: str | None, error: str, path: str | None
|
|
|
278
334
|
client = get_http_client()
|
|
279
335
|
_post_with_retries(
|
|
280
336
|
client=client,
|
|
281
|
-
url=f"{
|
|
337
|
+
url=f"{get_api_base_url()}/v2/daemon/sources/{local_folder_id}/error",
|
|
282
338
|
headers={"Authorization": f"Bearer {api_key}"},
|
|
283
339
|
payload={"error": error, "path": path},
|
|
284
340
|
)
|
ui.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UI utilities for Nia CLI - ASCII art and spinners.
|
|
3
|
+
"""
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
import random
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
NIA_LOGO_MINI = "[bold cyan]NIA[/bold cyan]"
|
|
13
|
+
|
|
14
|
+
# Spinner styles that look good
|
|
15
|
+
SPINNER_STYLES = [
|
|
16
|
+
"dots",
|
|
17
|
+
"dots2",
|
|
18
|
+
"dots3",
|
|
19
|
+
"dots12",
|
|
20
|
+
"line",
|
|
21
|
+
"arc",
|
|
22
|
+
"bouncingBar",
|
|
23
|
+
"bouncingBall",
|
|
24
|
+
"moon",
|
|
25
|
+
"runner",
|
|
26
|
+
"aesthetic",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Action-specific spinners and messages
|
|
30
|
+
SPINNER_MESSAGES = {
|
|
31
|
+
"login": ["Authenticating...", "Connecting to Nia...", "Verifying..."],
|
|
32
|
+
"sync": ["Syncing...", "Uploading changes...", "Processing files..."],
|
|
33
|
+
"search": ["Searching...", "Finding matches...", "Querying knowledge..."],
|
|
34
|
+
"upgrade": ["Checking for updates...", "Upgrading...", "Installing..."],
|
|
35
|
+
"fetch": ["Fetching...", "Loading data...", "Retrieving..."],
|
|
36
|
+
"connect": ["Connecting...", "Establishing connection...", "Reaching server..."],
|
|
37
|
+
"process": ["Processing...", "Working...", "Computing..."],
|
|
38
|
+
"default": ["Working...", "Please wait...", "Processing..."],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def print_logo(subtitle: str | None = None, compact: bool = False):
|
|
43
|
+
"""Print the Nia header (no logo)."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def print_header(title: str, subtitle: str | None = None):
|
|
48
|
+
"""Print a styled header with optional subtitle."""
|
|
49
|
+
console.print(f"\n{NIA_LOGO_MINI} [bold cyan]{title}[/bold cyan]")
|
|
50
|
+
if subtitle:
|
|
51
|
+
console.print(f" [dim]{subtitle}[/dim]")
|
|
52
|
+
console.print()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@contextmanager
|
|
56
|
+
def spinner(action: str = "default", message: str | None = None, min_time: float = 0.4):
|
|
57
|
+
"""
|
|
58
|
+
Context manager for showing a spinner during operations.
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
with spinner("sync", "Syncing files..."):
|
|
62
|
+
do_sync()
|
|
63
|
+
"""
|
|
64
|
+
import time
|
|
65
|
+
from rich.status import Status
|
|
66
|
+
|
|
67
|
+
# Pick message
|
|
68
|
+
if message is None:
|
|
69
|
+
messages = SPINNER_MESSAGES.get(action, SPINNER_MESSAGES["default"])
|
|
70
|
+
message = messages[0]
|
|
71
|
+
|
|
72
|
+
# Pick a good spinner
|
|
73
|
+
spinner_name = "dots"
|
|
74
|
+
start_time = time.time()
|
|
75
|
+
|
|
76
|
+
with Status(f"[bold cyan]{message}[/bold cyan]", spinner=spinner_name, console=console) as status:
|
|
77
|
+
# Expose update method through a simple interface
|
|
78
|
+
status._message = message
|
|
79
|
+
yield status
|
|
80
|
+
|
|
81
|
+
# Ensure spinner is visible for at least min_time
|
|
82
|
+
elapsed = time.time() - start_time
|
|
83
|
+
if elapsed < min_time:
|
|
84
|
+
time.sleep(min_time - elapsed)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def update_spinner(status, message: str):
|
|
88
|
+
"""Update the spinner message."""
|
|
89
|
+
status.update(f"[bold cyan]{message}[/bold cyan]")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def success(message: str, prefix: str = "✓"):
|
|
93
|
+
"""Print a success message."""
|
|
94
|
+
console.print(f"[green]{prefix}[/green] {message}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def error(message: str, prefix: str = "✗"):
|
|
98
|
+
"""Print an error message."""
|
|
99
|
+
console.print(f"[red]{prefix}[/red] {message}")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def warning(message: str, prefix: str = "⚠"):
|
|
103
|
+
"""Print a warning message."""
|
|
104
|
+
console.print(f"[yellow]{prefix}[/yellow] {message}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def info(message: str, prefix: str = "•"):
|
|
108
|
+
"""Print an info message."""
|
|
109
|
+
console.print(f"[cyan]{prefix}[/cyan] {message}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def dim(message: str):
|
|
113
|
+
"""Print a dimmed message."""
|
|
114
|
+
console.print(f"[dim]{message}[/dim]")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def print_welcome():
|
|
118
|
+
"""Print the welcome message."""
|
|
119
|
+
console.print("[bold cyan]Nia Sync Engine[/bold cyan] — Keep local folders in sync with Nia cloud\n")
|
nia_sync-0.1.6.dist-info/RECORD
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
auth.py,sha256=n0ezRqIbz3kZYcyIjH47_MDKgwNgAaVr0Y2NEfRxa50,5704
|
|
2
|
-
config.py,sha256=dbnkDihOiJL0-WBzTyxDXM2AIduwsvZycgQuhAoYglU,7637
|
|
3
|
-
extractor.py,sha256=IxpFGOlklusmPs2Jz398DH1hVVYpciwQN_HDyxpmm_Q,29193
|
|
4
|
-
main.py,sha256=f1e2q99QfI_U0YyYgsC75P1wHebKuweWa0pYEAQPMO8,26582
|
|
5
|
-
sync.py,sha256=jdVPceViN5uxoRCr5ukCVumBhvl3t53xkJnvZwvKmdw,10710
|
|
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,,
|
|
File without changes
|
|
File without changes
|