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.
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: study_sync
3
- Version: 1.0.3
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.3"
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
- { name = "Adinath", email = "adinarayan.is23@bmsce.ac.in" },
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
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,3 +1 @@
1
1
  """StudySync CLI package."""
2
-
3
- __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ """Shared constants for the StudySync CLI."""
2
+
3
+ PRODUCTION_SERVER_URL: str = "https://studysync-backend-pfft.onrender.com"
@@ -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()