study-sync 1.0.3__tar.gz → 1.0.8__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.
- {study_sync-1.0.3 → study_sync-1.0.8}/PKG-INFO +2 -2
- {study_sync-1.0.3 → study_sync-1.0.8}/README.md +101 -101
- {study_sync-1.0.3 → study_sync-1.0.8}/pyproject.toml +4 -26
- {study_sync-1.0.3 → study_sync-1.0.8}/study_sync.egg-info/PKG-INFO +2 -2
- {study_sync-1.0.3 → study_sync-1.0.8}/studysync/__init__.py +0 -2
- study_sync-1.0.8/studysync/constants.py +3 -0
- study_sync-1.0.8/studysync/local_state.py +129 -0
- {study_sync-1.0.3 → study_sync-1.0.8}/studysync/main.py +63 -43
- study_sync-1.0.8/studysync/sync_engine.py +670 -0
- study_sync-1.0.3/studysync/constants.py +0 -18
- study_sync-1.0.3/studysync/local_state.py +0 -183
- study_sync-1.0.3/studysync/sync_engine.py +0 -595
- {study_sync-1.0.3 → study_sync-1.0.8}/setup.cfg +0 -0
- {study_sync-1.0.3 → study_sync-1.0.8}/study_sync.egg-info/SOURCES.txt +0 -0
- {study_sync-1.0.3 → study_sync-1.0.8}/study_sync.egg-info/dependency_links.txt +0 -0
- {study_sync-1.0.3 → study_sync-1.0.8}/study_sync.egg-info/entry_points.txt +0 -0
- {study_sync-1.0.3 → study_sync-1.0.8}/study_sync.egg-info/requires.txt +0 -0
- {study_sync-1.0.3 → study_sync-1.0.8}/study_sync.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: study_sync
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.8
|
|
4
4
|
Summary: Offline-first, distributed workspace synchronisation CLI for developers.
|
|
5
|
-
Author-email: Adinath <adinarayan.is23@bmsce.ac.in>
|
|
5
|
+
Author-email: Malatesh <malateshbsunkad03@gmail.com>, Adinath <adinarayan.is23@bmsce.ac.in>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/yourusername/studysync
|
|
8
8
|
Project-URL: Documentation, https://github.com/yourusername/studysync#readme
|
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
# StudySync
|
|
2
|
-
|
|
3
|
-
**Offline-first, distributed workspace synchronisation for developers.**
|
|
4
|
-
|
|
5
|
-
Share and sync files across laptops over a local network or the internet — with cryptographic integrity checking and conflict protection built in.
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## Install
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
pip install studysync
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
That's it. No environment setup, no config files, no server URL required.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## Quick Start (2 steps)
|
|
20
|
-
|
|
21
|
-
**Step 1 — Install**
|
|
22
|
-
```bash
|
|
23
|
-
pip install studysync
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
**Step 2 — Join a workspace with your token**
|
|
27
|
-
```bash
|
|
28
|
-
study join <TOKEN>
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
You'll receive a `<TOKEN>` from whoever created the workspace. After joining, pull all shared files straight to your current directory:
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
study pull
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
## Full Workflow
|
|
40
|
-
|
|
41
|
-
### Create a workspace (team lead / project owner)
|
|
42
|
-
```bash
|
|
43
|
-
study workspace create my-project
|
|
44
|
-
# Output includes a TOKEN — share it with your team
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### Join an existing workspace (everyone else)
|
|
48
|
-
```bash
|
|
49
|
-
study join <TOKEN>
|
|
50
|
-
study pull # downloads all files into your current directory
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Push a file
|
|
54
|
-
```bash
|
|
55
|
-
study push path/to/file.py
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
If someone else pushed a newer version since your last pull, you'll see:
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
⚠ CONFLICT — Remote has changes. Pull first.
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Pull, resolve, then push again.
|
|
65
|
-
|
|
66
|
-
### Check sync status
|
|
67
|
-
```bash
|
|
68
|
-
study status
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Shows `CLEAN`, `MODIFIED`, `DELETED`, or `UNTRACKED` for every file in your local workspace.
|
|
72
|
-
|
|
73
|
-
---
|
|
74
|
-
|
|
75
|
-
## Self-Hosting
|
|
76
|
-
|
|
77
|
-
Advanced users running their own StudySync backend can override the default server:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
study join <TOKEN> --server https://my-backend.example.com
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
The URL is saved locally after the first use — subsequent commands pick it up automatically.
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## How It Works
|
|
88
|
-
|
|
89
|
-
| Feature | Detail |
|
|
90
|
-
|---|---|
|
|
91
|
-
| **Integrity** | Every file is SHA-256 hashed before push and verified after pull |
|
|
92
|
-
| **Conflict protection** | Optimistic Concurrency Control (OCC) — the server rejects a push if remote has a newer version |
|
|
93
|
-
| **Offline-first** | Edits happen locally; the network is only touched on explicit `push` / `pull` |
|
|
94
|
-
| **Zero-payload server** | File bytes stream directly between client and storage; the server only manages metadata |
|
|
95
|
-
| **Silent history** | Every push is versioned server-side — no data is ever permanently overwritten |
|
|
96
|
-
|
|
97
|
-
---
|
|
98
|
-
|
|
99
|
-
## License
|
|
100
|
-
|
|
101
|
-
MIT © Adinath
|
|
1
|
+
# StudySync
|
|
2
|
+
|
|
3
|
+
**Offline-first, distributed workspace synchronisation for developers.**
|
|
4
|
+
|
|
5
|
+
Share and sync files across laptops over a local network or the internet — with cryptographic integrity checking and conflict protection built in.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install studysync
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
That's it. No environment setup, no config files, no server URL required.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start (2 steps)
|
|
20
|
+
|
|
21
|
+
**Step 1 — Install**
|
|
22
|
+
```bash
|
|
23
|
+
pip install studysync
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Step 2 — Join a workspace with your token**
|
|
27
|
+
```bash
|
|
28
|
+
study join <TOKEN>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You'll receive a `<TOKEN>` from whoever created the workspace. After joining, pull all shared files straight to your current directory:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
study pull
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Full Workflow
|
|
40
|
+
|
|
41
|
+
### Create a workspace (team lead / project owner)
|
|
42
|
+
```bash
|
|
43
|
+
study workspace create my-project
|
|
44
|
+
# Output includes a TOKEN — share it with your team
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Join an existing workspace (everyone else)
|
|
48
|
+
```bash
|
|
49
|
+
study join <TOKEN>
|
|
50
|
+
study pull # downloads all files into your current directory
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Push a file
|
|
54
|
+
```bash
|
|
55
|
+
study push path/to/file.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If someone else pushed a newer version since your last pull, you'll see:
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
⚠ CONFLICT — Remote has changes. Pull first.
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Pull, resolve, then push again.
|
|
65
|
+
|
|
66
|
+
### Check sync status
|
|
67
|
+
```bash
|
|
68
|
+
study status
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Shows `CLEAN`, `MODIFIED`, `DELETED`, or `UNTRACKED` for every file in your local workspace.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Self-Hosting
|
|
76
|
+
|
|
77
|
+
Advanced users running their own StudySync backend can override the default server:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
study join <TOKEN> --server https://my-backend.example.com
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The URL is saved locally after the first use — subsequent commands pick it up automatically.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## How It Works
|
|
88
|
+
|
|
89
|
+
| Feature | Detail |
|
|
90
|
+
|---|---|
|
|
91
|
+
| **Integrity** | Every file is SHA-256 hashed before push and verified after pull |
|
|
92
|
+
| **Conflict protection** | Optimistic Concurrency Control (OCC) — the server rejects a push if remote has a newer version |
|
|
93
|
+
| **Offline-first** | Edits happen locally; the network is only touched on explicit `push` / `pull` |
|
|
94
|
+
| **Zero-payload server** | File bytes stream directly between client and storage; the server only manages metadata |
|
|
95
|
+
| **Silent history** | Every push is versioned server-side — no data is ever permanently overwritten |
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT © Adinath
|
|
@@ -2,22 +2,17 @@
|
|
|
2
2
|
requires = ["setuptools>=68.0", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
|
-
# ---------------------------------------------------------------------------
|
|
6
|
-
# Core metadata
|
|
7
|
-
# ---------------------------------------------------------------------------
|
|
8
5
|
[project]
|
|
9
6
|
name = "study_sync"
|
|
10
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.8"
|
|
11
8
|
description = "Offline-first, distributed workspace synchronisation CLI for developers."
|
|
12
9
|
license = { text = "MIT" }
|
|
13
10
|
requires-python = ">=3.10"
|
|
14
11
|
authors = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
keywords = [
|
|
18
|
-
"sync", "cli", "workspace", "distributed",
|
|
19
|
-
"version-control", "developer-tools",
|
|
12
|
+
{ name="Malatesh", email="malateshbsunkad03@gmail.com"},
|
|
13
|
+
{ name = "Adinath", email = "adinarayan.is23@bmsce.ac.in" },
|
|
20
14
|
]
|
|
15
|
+
keywords = ["sync", "cli", "workspace", "distributed", "version-control", "developer-tools"]
|
|
21
16
|
classifiers = [
|
|
22
17
|
"Development Status :: 4 - Beta",
|
|
23
18
|
"Environment :: Console",
|
|
@@ -32,20 +27,12 @@ classifiers = [
|
|
|
32
27
|
"Topic :: System :: Filesystems",
|
|
33
28
|
"Topic :: Utilities",
|
|
34
29
|
]
|
|
35
|
-
|
|
36
|
-
# ---------------------------------------------------------------------------
|
|
37
|
-
# Runtime dependencies
|
|
38
|
-
# ---------------------------------------------------------------------------
|
|
39
30
|
dependencies = [
|
|
40
31
|
"typer[all]>=0.12.0",
|
|
41
32
|
"rich>=13.7.0",
|
|
42
33
|
"requests>=2.31.0",
|
|
43
34
|
]
|
|
44
35
|
|
|
45
|
-
# ---------------------------------------------------------------------------
|
|
46
|
-
# Optional extras
|
|
47
|
-
# pip install studysync[dev]
|
|
48
|
-
# ---------------------------------------------------------------------------
|
|
49
36
|
[project.optional-dependencies]
|
|
50
37
|
dev = [
|
|
51
38
|
"pytest>=8.0.0",
|
|
@@ -55,24 +42,15 @@ dev = [
|
|
|
55
42
|
"twine>=5.0.0",
|
|
56
43
|
]
|
|
57
44
|
|
|
58
|
-
# ---------------------------------------------------------------------------
|
|
59
|
-
# Entry point — makes `study` work as a global shell command
|
|
60
|
-
# ---------------------------------------------------------------------------
|
|
61
45
|
[project.scripts]
|
|
62
46
|
study = "studysync.main:app"
|
|
63
47
|
|
|
64
|
-
# ---------------------------------------------------------------------------
|
|
65
|
-
# Project URLs shown on the PyPI landing page
|
|
66
|
-
# ---------------------------------------------------------------------------
|
|
67
48
|
[project.urls]
|
|
68
49
|
Homepage = "https://github.com/yourusername/studysync"
|
|
69
50
|
Documentation = "https://github.com/yourusername/studysync#readme"
|
|
70
51
|
"Bug Tracker" = "https://github.com/yourusername/studysync/issues"
|
|
71
52
|
Changelog = "https://github.com/yourusername/studysync/releases"
|
|
72
53
|
|
|
73
|
-
# ---------------------------------------------------------------------------
|
|
74
|
-
# Package discovery
|
|
75
|
-
# ---------------------------------------------------------------------------
|
|
76
54
|
[tool.setuptools.packages.find]
|
|
77
55
|
where = ["."]
|
|
78
56
|
include = ["studysync*"]
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: study_sync
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.8
|
|
4
4
|
Summary: Offline-first, distributed workspace synchronisation CLI for developers.
|
|
5
|
-
Author-email: Adinath <adinarayan.is23@bmsce.ac.in>
|
|
5
|
+
Author-email: Malatesh <malateshbsunkad03@gmail.com>, Adinath <adinarayan.is23@bmsce.ac.in>
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/yourusername/studysync
|
|
8
8
|
Project-URL: Documentation, https://github.com/yourusername/studysync#readme
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
local_state.py — Manages all local CLI state.
|
|
3
|
+
|
|
4
|
+
Directory layout
|
|
5
|
+
----------------
|
|
6
|
+
~/.study/
|
|
7
|
+
config.json — active workspace credentials
|
|
8
|
+
manifest.json — per-file {version, checksum} map
|
|
9
|
+
workspaces/
|
|
10
|
+
<workspace_name>/ — vault: local copies of synced files
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import shutil
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Directory constants
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
STUDY_DIR: Path = Path.home() / ".study"
|
|
26
|
+
WORKSPACES_DIR: Path = STUDY_DIR / "workspaces"
|
|
27
|
+
CONFIG_PATH: Path = STUDY_DIR / "config.json"
|
|
28
|
+
MANIFEST_PATH: Path = STUDY_DIR / "manifest.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Bootstrap
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def ensure_dirs() -> None:
|
|
36
|
+
"""Create ~/.study and ~/.study/workspaces/ if they don't exist."""
|
|
37
|
+
STUDY_DIR.mkdir(parents=True, exist_ok=True)
|
|
38
|
+
WORKSPACES_DIR.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def workspace_root(workspace_name: str) -> Path:
|
|
42
|
+
"""Return (and create) ~/.study/workspaces/<workspace_name>/."""
|
|
43
|
+
p = WORKSPACES_DIR / workspace_name
|
|
44
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
return p
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Config
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def load_config() -> dict[str, Any]:
|
|
53
|
+
"""Load ~/.study/config.json; returns {} if missing or corrupt."""
|
|
54
|
+
if not CONFIG_PATH.exists():
|
|
55
|
+
return {}
|
|
56
|
+
try:
|
|
57
|
+
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
58
|
+
except (json.JSONDecodeError, OSError):
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save_config(data: dict[str, Any]) -> None:
|
|
63
|
+
"""Persist config to ~/.study/config.json."""
|
|
64
|
+
ensure_dirs()
|
|
65
|
+
CONFIG_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Manifest
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def load_manifest() -> dict[str, dict[str, Any]]:
|
|
73
|
+
"""
|
|
74
|
+
Load ~/.study/manifest.json.
|
|
75
|
+
|
|
76
|
+
Schema: {file_path: {version: int, checksum: str}}
|
|
77
|
+
"""
|
|
78
|
+
if not MANIFEST_PATH.exists():
|
|
79
|
+
return {}
|
|
80
|
+
try:
|
|
81
|
+
return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
|
82
|
+
except (json.JSONDecodeError, OSError):
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def save_manifest(data: dict[str, dict[str, Any]]) -> None:
|
|
87
|
+
"""Persist manifest to ~/.study/manifest.json."""
|
|
88
|
+
ensure_dirs()
|
|
89
|
+
MANIFEST_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def update_manifest_entry(file_path: str, version: int, checksum: str) -> None:
|
|
93
|
+
"""Update a single entry in the manifest without touching others."""
|
|
94
|
+
manifest = load_manifest()
|
|
95
|
+
manifest[file_path] = {"version": version, "checksum": checksum}
|
|
96
|
+
save_manifest(manifest)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Vault helpers
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def local_file_path(workspace_name: str, file_path: str) -> Path:
|
|
104
|
+
"""Return the vault path for a given workspace file."""
|
|
105
|
+
return WORKSPACES_DIR / workspace_name / file_path
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def all_local_files(workspace_name: str) -> list[Path]:
|
|
109
|
+
"""
|
|
110
|
+
Return every file currently in the vault for this workspace.
|
|
111
|
+
Paths are relative to the vault root.
|
|
112
|
+
"""
|
|
113
|
+
root = WORKSPACES_DIR / workspace_name
|
|
114
|
+
if not root.exists():
|
|
115
|
+
return []
|
|
116
|
+
return [p for p in root.rglob("*") if p.is_file()]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Hashing
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def sha256_file(path: Path) -> str:
|
|
124
|
+
"""Return the hex SHA-256 digest of *path* using 64 KiB streaming reads."""
|
|
125
|
+
h = hashlib.sha256()
|
|
126
|
+
with open(path, "rb") as fh:
|
|
127
|
+
for chunk in iter(lambda: fh.read(65_536), b""):
|
|
128
|
+
h.update(chunk)
|
|
129
|
+
return h.hexdigest()
|