gitstore 0.1.0__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.
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitstore
3
+ Version: 0.1.0
4
+ Summary: Utilities to encrypt files/directories with utilitz and exchange them through GitHub repositories.
5
+ Author: artitzco
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests>=2.31.0
10
+ Requires-Dist: utilitz[crypto]
11
+
12
+ # gitstore
13
+
14
+ `gitstore` is a focused Python package for one goal:
15
+
16
+ - upload encrypted files/folders to a GitHub-backed repo
17
+ - restore them later by logical name from GitHub raw URLs
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install gitstore
23
+ ```
24
+
25
+ ## Dependency on `utilitz`
26
+
27
+ This project depends on:
28
+
29
+ - `utilitz[crypto]`
30
+ - `requests`
31
+
32
+ ## Project Structure
33
+
34
+ ```text
35
+ gitstore/
36
+ src/gitstore/
37
+ __init__.py
38
+ client.py
39
+ config.py
40
+ crypto_ops.py
41
+ github_ops.py
42
+ pyproject.toml
43
+ README.md
44
+ ```
45
+
46
+ ## Core API
47
+
48
+ ```python
49
+ from gitstore import GitStoreUploader, GitStoreDownloader
50
+ ```
51
+
52
+ ## Upload
53
+
54
+ ```python
55
+ from gitstore import GitStoreUploader
56
+
57
+ uploader = GitStoreUploader(
58
+ repo_path="C:/repos/my-publish-repo", # optional, defaults to current working directory
59
+ security_level="high", # default
60
+ )
61
+
62
+ record = uploader.store(
63
+ source_path="C:/data/documento.pdf", # file or directory
64
+ name="documento_ventas_q2", # logical name only
65
+ replace_existing=True, # default: replace and purge previous history
66
+ commit_message=None, # default: automatic message
67
+ )
68
+ print(record)
69
+ ```
70
+
71
+ ## Download
72
+
73
+ ```python
74
+ from gitstore import GitStoreDownloader
75
+
76
+ downloader = GitStoreDownloader(
77
+ raw_base_url="https://raw.githubusercontent.com/USER/REPO/main",
78
+ )
79
+
80
+ output_path = downloader.restore(
81
+ name="documento_ventas_q2",
82
+ output_path="C:/restore/documento.pdf",
83
+ overwrite=True,
84
+ )
85
+ print(output_path)
86
+ ```
87
+
88
+ ## Destroy
89
+
90
+ ```python
91
+ from gitstore import GitStoreUploader
92
+
93
+ uploader = GitStoreUploader(repo_path="C:/repos/my-publish-repo")
94
+ uploader.destroy(name="documento_ventas_q2")
95
+ ```
96
+
97
+ `destroy(...)` removes the current artifact and rewrites Git history for that artifact path.
98
+
99
+ ## Password Source
100
+
101
+ Both classes auto-detect password from:
102
+
103
+ - `GITSTORE_PASSWORD`
104
+
105
+ If `password` is not passed, the environment variable is used.
@@ -0,0 +1,94 @@
1
+ # gitstore
2
+
3
+ `gitstore` is a focused Python package for one goal:
4
+
5
+ - upload encrypted files/folders to a GitHub-backed repo
6
+ - restore them later by logical name from GitHub raw URLs
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install gitstore
12
+ ```
13
+
14
+ ## Dependency on `utilitz`
15
+
16
+ This project depends on:
17
+
18
+ - `utilitz[crypto]`
19
+ - `requests`
20
+
21
+ ## Project Structure
22
+
23
+ ```text
24
+ gitstore/
25
+ src/gitstore/
26
+ __init__.py
27
+ client.py
28
+ config.py
29
+ crypto_ops.py
30
+ github_ops.py
31
+ pyproject.toml
32
+ README.md
33
+ ```
34
+
35
+ ## Core API
36
+
37
+ ```python
38
+ from gitstore import GitStoreUploader, GitStoreDownloader
39
+ ```
40
+
41
+ ## Upload
42
+
43
+ ```python
44
+ from gitstore import GitStoreUploader
45
+
46
+ uploader = GitStoreUploader(
47
+ repo_path="C:/repos/my-publish-repo", # optional, defaults to current working directory
48
+ security_level="high", # default
49
+ )
50
+
51
+ record = uploader.store(
52
+ source_path="C:/data/documento.pdf", # file or directory
53
+ name="documento_ventas_q2", # logical name only
54
+ replace_existing=True, # default: replace and purge previous history
55
+ commit_message=None, # default: automatic message
56
+ )
57
+ print(record)
58
+ ```
59
+
60
+ ## Download
61
+
62
+ ```python
63
+ from gitstore import GitStoreDownloader
64
+
65
+ downloader = GitStoreDownloader(
66
+ raw_base_url="https://raw.githubusercontent.com/USER/REPO/main",
67
+ )
68
+
69
+ output_path = downloader.restore(
70
+ name="documento_ventas_q2",
71
+ output_path="C:/restore/documento.pdf",
72
+ overwrite=True,
73
+ )
74
+ print(output_path)
75
+ ```
76
+
77
+ ## Destroy
78
+
79
+ ```python
80
+ from gitstore import GitStoreUploader
81
+
82
+ uploader = GitStoreUploader(repo_path="C:/repos/my-publish-repo")
83
+ uploader.destroy(name="documento_ventas_q2")
84
+ ```
85
+
86
+ `destroy(...)` removes the current artifact and rewrites Git history for that artifact path.
87
+
88
+ ## Password Source
89
+
90
+ Both classes auto-detect password from:
91
+
92
+ - `GITSTORE_PASSWORD`
93
+
94
+ If `password` is not passed, the environment variable is used.
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gitstore"
7
+ version = "0.1.0"
8
+ description = "Utilities to encrypt files/directories with utilitz and exchange them through GitHub repositories."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "artitzco" }
14
+ ]
15
+ dependencies = [
16
+ "requests>=2.31.0",
17
+ "utilitz[crypto]"
18
+ ]
19
+
20
+ [tool.setuptools]
21
+ package-dir = {"" = "src"}
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ from .client import DEFAULT_PASSWORD_ENV_VAR, GitStoreDownloader, GitStoreUploader, StoredArtifact
2
+
3
+ __all__ = [
4
+ "GitStoreUploader",
5
+ "GitStoreDownloader",
6
+ "StoredArtifact",
7
+ "DEFAULT_PASSWORD_ENV_VAR",
8
+ ]
@@ -0,0 +1,249 @@
1
+ import json
2
+ import os
3
+ import shutil
4
+ import tempfile
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+
9
+ from .config import GitStoreConfig
10
+ from .github_ops import download_raw_file, git_add_commit_push, git_purge_path_from_history
11
+ from .crypto_ops import decrypt_directory, decrypt_file, encrypt_directory, encrypt_file
12
+
13
+ DEFAULT_PASSWORD_ENV_VAR = "GITSTORE_PASSWORD"
14
+
15
+
16
+ @dataclass(frozen=True)
17
+ class StoredArtifact:
18
+ name: str
19
+ artifact: str
20
+ is_directory: bool
21
+ created_at_utc: str
22
+
23
+
24
+ def _resolve_password(password: str | None, password_env_var: str) -> str:
25
+ resolved_password = password or os.getenv(password_env_var)
26
+ if not resolved_password:
27
+ raise ValueError(
28
+ "Password was not provided and no environment variable was found. "
29
+ f"Set '{password_env_var}' or pass password explicitly."
30
+ )
31
+ return resolved_password
32
+
33
+
34
+ class GitStoreUploader:
35
+ def __init__(
36
+ self,
37
+ repo_path: str | None = None,
38
+ password: str | None = None,
39
+ vault_dir: str = "vault",
40
+ request_timeout: int = 60,
41
+ password_env_var: str = DEFAULT_PASSWORD_ENV_VAR,
42
+ security_level: str = "high",
43
+ ) -> None:
44
+ resolved_password = _resolve_password(password, password_env_var)
45
+ self.config = GitStoreConfig(password=resolved_password, request_timeout=request_timeout)
46
+ self.repo_path = Path(repo_path).expanduser().resolve() if repo_path else Path.cwd().resolve()
47
+ self.vault_dir = vault_dir.strip().strip("/\\") or "vault"
48
+ self._manifest_rel = f"{self.vault_dir}/index.json"
49
+ self.password_env_var = password_env_var
50
+ self.security_level = security_level
51
+
52
+ def store(
53
+ self,
54
+ source_path: str,
55
+ name: str,
56
+ commit_message: str | None = None,
57
+ replace_existing: bool = True,
58
+ security_level: str | None = None,
59
+ ) -> StoredArtifact:
60
+ source = Path(source_path).expanduser().resolve()
61
+ if not source.exists():
62
+ raise FileNotFoundError(f"Input path not found: {source}")
63
+ if not name or any(sep in name for sep in ("/", "\\")):
64
+ raise ValueError("name must be a simple identifier without path separators.")
65
+ self._ensure_repo()
66
+
67
+ is_directory = source.is_dir()
68
+ level = security_level or self.security_level
69
+ if is_directory:
70
+ encrypted_path = Path(
71
+ encrypt_directory(
72
+ source_directory=str(source),
73
+ config=self.config,
74
+ security_level=level,
75
+ )
76
+ )
77
+ else:
78
+ encrypted_path = Path(
79
+ encrypt_file(
80
+ source_path=str(source),
81
+ config=self.config,
82
+ security_level=level,
83
+ )
84
+ )
85
+ extension = "".join(encrypted_path.suffixes) or ".asc"
86
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
87
+ artifact_filename = f"{name}__{stamp}{extension}"
88
+ artifact_rel = f"{self.vault_dir}/{artifact_filename}"
89
+ artifact_abs = self.repo_path / artifact_rel
90
+ artifact_abs.parent.mkdir(parents=True, exist_ok=True)
91
+ shutil.copy2(encrypted_path, artifact_abs)
92
+
93
+ manifest = self._load_manifest_local()
94
+ old_artifact_rel = None
95
+ if name in manifest:
96
+ if not replace_existing:
97
+ raise ValueError(
98
+ f"Name '{name}' already exists. Use replace_existing=True to replace it."
99
+ )
100
+ old_artifact = manifest[name].get("artifact")
101
+ if old_artifact:
102
+ old_artifact_rel = f"{self.vault_dir}/{old_artifact}"
103
+ old_artifact_abs = self.repo_path / old_artifact_rel
104
+ if old_artifact_abs.exists():
105
+ old_artifact_abs.unlink()
106
+
107
+ record = StoredArtifact(
108
+ name=name,
109
+ artifact=artifact_filename,
110
+ is_directory=is_directory,
111
+ created_at_utc=datetime.now(timezone.utc).isoformat(),
112
+ )
113
+ manifest[name] = record.__dict__
114
+ self._save_manifest_local(manifest)
115
+
116
+ message = commit_message or f"gitstore: store '{name}'"
117
+ paths_to_commit = [artifact_rel, self._manifest_rel]
118
+ if old_artifact_rel:
119
+ paths_to_commit.insert(0, old_artifact_rel)
120
+ git_add_commit_push(
121
+ repo_path=str(self.repo_path),
122
+ paths_in_repo=paths_to_commit,
123
+ commit_message=message,
124
+ )
125
+ if old_artifact_rel:
126
+ git_purge_path_from_history(
127
+ repo_path=str(self.repo_path),
128
+ path_in_repo=old_artifact_rel,
129
+ )
130
+ return record
131
+
132
+ def destroy(self, name: str, commit_message: str | None = None) -> None:
133
+ self._ensure_repo()
134
+ manifest = self._load_manifest_local()
135
+ if name not in manifest:
136
+ raise KeyError(f"Name not found in manifest: {name}")
137
+ record = manifest[name]
138
+ artifact = record.get("artifact")
139
+ if not artifact:
140
+ raise ValueError(f"Invalid manifest record for name: {name}")
141
+
142
+ artifact_rel = f"{self.vault_dir}/{artifact}"
143
+ artifact_abs = self.repo_path / artifact_rel
144
+ if artifact_abs.exists():
145
+ artifact_abs.unlink()
146
+
147
+ del manifest[name]
148
+ self._save_manifest_local(manifest)
149
+
150
+ message = commit_message or f"gitstore: destroy '{name}'"
151
+ git_add_commit_push(
152
+ repo_path=str(self.repo_path),
153
+ paths_in_repo=[artifact_rel, self._manifest_rel],
154
+ commit_message=message,
155
+ )
156
+ git_purge_path_from_history(repo_path=str(self.repo_path), path_in_repo=artifact_rel)
157
+
158
+ def _ensure_repo(self) -> None:
159
+ if not (self.repo_path / ".git").exists():
160
+ raise FileNotFoundError(f"Not a git repository: {self.repo_path}")
161
+
162
+ def _manifest_path(self) -> Path:
163
+ return self.repo_path / self._manifest_rel
164
+
165
+ def _load_manifest_local(self) -> dict:
166
+ manifest_path = self._manifest_path()
167
+ if not manifest_path.exists():
168
+ return {}
169
+ with open(manifest_path, "r", encoding="utf-8") as f:
170
+ data = json.load(f)
171
+ if not isinstance(data, dict):
172
+ raise ValueError("Manifest format is invalid.")
173
+ return data
174
+
175
+ def _save_manifest_local(self, manifest: dict) -> None:
176
+ manifest_path = self._manifest_path()
177
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
178
+ with open(manifest_path, "w", encoding="utf-8", newline="\n") as f:
179
+ json.dump(manifest, f, indent=2, sort_keys=True)
180
+ f.write("\n")
181
+
182
+
183
+ class GitStoreDownloader:
184
+ def __init__(
185
+ self,
186
+ raw_base_url: str,
187
+ password: str | None = None,
188
+ vault_dir: str = "vault",
189
+ request_timeout: int = 60,
190
+ password_env_var: str = DEFAULT_PASSWORD_ENV_VAR,
191
+ ) -> None:
192
+ if not raw_base_url:
193
+ raise ValueError("raw_base_url is required for downloader.")
194
+ resolved_password = _resolve_password(password, password_env_var)
195
+ self.config = GitStoreConfig(password=resolved_password, request_timeout=request_timeout)
196
+ self.raw_base_url = raw_base_url.rstrip("/")
197
+ self.vault_dir = vault_dir.strip().strip("/\\") or "vault"
198
+ self._manifest_rel = f"{self.vault_dir}/index.json"
199
+ self.password_env_var = password_env_var
200
+
201
+ def restore(
202
+ self,
203
+ name: str,
204
+ output_path: str | None = None,
205
+ overwrite: bool = False,
206
+ ) -> str:
207
+ record = self._get_manifest_record(name)
208
+ artifact = record["artifact"]
209
+ is_directory = bool(record["is_directory"])
210
+
211
+ temp_dir = Path(tempfile.mkdtemp(prefix="gitstore_restore_"))
212
+ temp_file = temp_dir / "artifact.enc"
213
+ try:
214
+ raw_url = f"{self.raw_base_url}/{self.vault_dir}/{artifact}"
215
+ download_raw_file(raw_url, str(temp_file), timeout=self.config.request_timeout)
216
+ if is_directory:
217
+ return decrypt_directory(
218
+ encrypted_path=str(temp_file),
219
+ config=self.config,
220
+ output_path=output_path,
221
+ overwrite=overwrite,
222
+ )
223
+ return decrypt_file(
224
+ encrypted_path=str(temp_file),
225
+ config=self.config,
226
+ output_path=output_path,
227
+ overwrite=overwrite,
228
+ )
229
+ finally:
230
+ shutil.rmtree(temp_dir, ignore_errors=True)
231
+
232
+ def _get_manifest_record(self, name: str) -> dict:
233
+ manifest_url = f"{self.raw_base_url}/{self._manifest_rel}"
234
+ temp_dir = Path(tempfile.mkdtemp(prefix="gitstore_manifest_"))
235
+ temp_manifest = temp_dir / "index.json"
236
+ try:
237
+ download_raw_file(manifest_url, str(temp_manifest), timeout=self.config.request_timeout)
238
+ with open(temp_manifest, "r", encoding="utf-8") as f:
239
+ manifest = json.load(f)
240
+ finally:
241
+ shutil.rmtree(temp_dir, ignore_errors=True)
242
+
243
+ if name not in manifest:
244
+ raise KeyError(f"Name not found in manifest: {name}")
245
+ record = manifest[name]
246
+ if not isinstance(record, dict) or "artifact" not in record or "is_directory" not in record:
247
+ raise ValueError(f"Invalid manifest record for name: {name}")
248
+ return record
249
+
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class GitStoreConfig:
6
+ password: str
7
+ request_timeout: int = 60
8
+
9
+ def __post_init__(self) -> None:
10
+ if not isinstance(self.password, str) or not self.password:
11
+ raise ValueError("password must be a non-empty string.")
12
+ if not isinstance(self.request_timeout, int) or self.request_timeout <= 0:
13
+ raise ValueError("request_timeout must be a positive integer.")
@@ -0,0 +1,93 @@
1
+ from pathlib import Path
2
+
3
+ from .config import GitStoreConfig
4
+
5
+
6
+ def _crypto():
7
+ try:
8
+ from utilitz import crypto as utilitz_crypto
9
+ except ImportError as exc:
10
+ raise ImportError(
11
+ "utilitz with crypto extras is required. Install with: pip install 'utilitz[crypto]'"
12
+ ) from exc
13
+ return utilitz_crypto
14
+
15
+
16
+ def _resolve_security(security_level: str):
17
+ crypto = _crypto()
18
+ level = (security_level or "standard").strip().lower()
19
+ if level == "standard":
20
+ return crypto.SECURITY_STANDARD
21
+ if level == "high":
22
+ return crypto.SECURITY_HIGH
23
+ if level == "paranoid":
24
+ return crypto.SECURITY_PARANOID
25
+ raise ValueError("security_level must be 'standard', 'high', or 'paranoid'.")
26
+
27
+
28
+ def encrypt_file(
29
+ source_path: str,
30
+ config: GitStoreConfig,
31
+ output_path: str | None = None,
32
+ security_level: str = "high",
33
+ ) -> str:
34
+ path = Path(source_path).expanduser().resolve()
35
+ if not path.is_file():
36
+ raise FileNotFoundError(f"Input file not found: {path}")
37
+ return _crypto().encrypt_file(
38
+ file_path=str(path),
39
+ password=config.password,
40
+ output_path=output_path,
41
+ security=_resolve_security(security_level),
42
+ )
43
+
44
+
45
+ def encrypt_directory(
46
+ source_directory: str,
47
+ config: GitStoreConfig,
48
+ output_path: str | None = None,
49
+ security_level: str = "high",
50
+ ) -> str:
51
+ path = Path(source_directory).expanduser().resolve()
52
+ if not path.is_dir():
53
+ raise FileNotFoundError(f"Input directory not found: {path}")
54
+ return _crypto().encrypt_directory(
55
+ directory_path=str(path),
56
+ password=config.password,
57
+ output_path=output_path,
58
+ security=_resolve_security(security_level),
59
+ )
60
+
61
+
62
+ def decrypt_file(
63
+ encrypted_path: str,
64
+ config: GitStoreConfig,
65
+ output_path: str | None = None,
66
+ overwrite: bool = False,
67
+ ) -> str:
68
+ path = Path(encrypted_path).expanduser().resolve()
69
+ if not path.is_file():
70
+ raise FileNotFoundError(f"Encrypted file not found: {path}")
71
+ return _crypto().decrypt_file(
72
+ encrypted_file=str(path),
73
+ password=config.password,
74
+ output_path=output_path,
75
+ overwrite=overwrite,
76
+ )
77
+
78
+
79
+ def decrypt_directory(
80
+ encrypted_path: str,
81
+ config: GitStoreConfig,
82
+ output_path: str | None = None,
83
+ overwrite: bool = False,
84
+ ) -> str:
85
+ path = Path(encrypted_path).expanduser().resolve()
86
+ if not path.is_file():
87
+ raise FileNotFoundError(f"Encrypted file not found: {path}")
88
+ return _crypto().decrypt_directory(
89
+ encrypted_file=str(path),
90
+ password=config.password,
91
+ output_path=output_path,
92
+ overwrite=overwrite,
93
+ )
@@ -0,0 +1,61 @@
1
+ import subprocess
2
+ from pathlib import Path
3
+
4
+ import requests
5
+
6
+
7
+ def download_raw_file(raw_url: str, output_path: str, timeout: int = 60) -> str:
8
+ destination = Path(output_path).expanduser().resolve()
9
+ destination.parent.mkdir(parents=True, exist_ok=True)
10
+
11
+ with requests.get(raw_url, timeout=timeout, stream=True) as response:
12
+ response.raise_for_status()
13
+ with open(destination, "wb") as f:
14
+ for chunk in response.iter_content(chunk_size=65536):
15
+ if chunk:
16
+ f.write(chunk)
17
+ return str(destination)
18
+
19
+
20
+
21
+ def git_add_commit_push(
22
+ repo_path: str,
23
+ paths_in_repo: list[str],
24
+ commit_message: str,
25
+ ) -> None:
26
+ repo = Path(repo_path).expanduser().resolve()
27
+ if not (repo / ".git").exists():
28
+ raise FileNotFoundError(f"Not a git repository: {repo}")
29
+ if not paths_in_repo:
30
+ raise ValueError("paths_in_repo must not be empty.")
31
+
32
+ subprocess.run(["git", "add", *paths_in_repo], cwd=repo, check=True)
33
+ subprocess.run(["git", "commit", "-m", commit_message], cwd=repo, check=True)
34
+ subprocess.run(["git", "push"], cwd=repo, check=True)
35
+
36
+
37
+ def git_purge_path_from_history(repo_path: str, path_in_repo: str) -> None:
38
+ repo = Path(repo_path).expanduser().resolve()
39
+ if not (repo / ".git").exists():
40
+ raise FileNotFoundError(f"Not a git repository: {repo}")
41
+
42
+ # Rewrites history to remove the file from all commits, then force-pushes.
43
+ subprocess.run(
44
+ [
45
+ "git",
46
+ "filter-branch",
47
+ "--force",
48
+ "--index-filter",
49
+ f"git rm -r --cached --ignore-unmatch -- {path_in_repo}",
50
+ "--prune-empty",
51
+ "--tag-name-filter",
52
+ "cat",
53
+ "--",
54
+ "--all",
55
+ ],
56
+ cwd=repo,
57
+ check=True,
58
+ )
59
+ subprocess.run(["git", "push", "origin", "--force", "--all"], cwd=repo, check=True)
60
+ subprocess.run(["git", "push", "origin", "--force", "--tags"], cwd=repo, check=True)
61
+
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitstore
3
+ Version: 0.1.0
4
+ Summary: Utilities to encrypt files/directories with utilitz and exchange them through GitHub repositories.
5
+ Author: artitzco
6
+ License: MIT
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: requests>=2.31.0
10
+ Requires-Dist: utilitz[crypto]
11
+
12
+ # gitstore
13
+
14
+ `gitstore` is a focused Python package for one goal:
15
+
16
+ - upload encrypted files/folders to a GitHub-backed repo
17
+ - restore them later by logical name from GitHub raw URLs
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install gitstore
23
+ ```
24
+
25
+ ## Dependency on `utilitz`
26
+
27
+ This project depends on:
28
+
29
+ - `utilitz[crypto]`
30
+ - `requests`
31
+
32
+ ## Project Structure
33
+
34
+ ```text
35
+ gitstore/
36
+ src/gitstore/
37
+ __init__.py
38
+ client.py
39
+ config.py
40
+ crypto_ops.py
41
+ github_ops.py
42
+ pyproject.toml
43
+ README.md
44
+ ```
45
+
46
+ ## Core API
47
+
48
+ ```python
49
+ from gitstore import GitStoreUploader, GitStoreDownloader
50
+ ```
51
+
52
+ ## Upload
53
+
54
+ ```python
55
+ from gitstore import GitStoreUploader
56
+
57
+ uploader = GitStoreUploader(
58
+ repo_path="C:/repos/my-publish-repo", # optional, defaults to current working directory
59
+ security_level="high", # default
60
+ )
61
+
62
+ record = uploader.store(
63
+ source_path="C:/data/documento.pdf", # file or directory
64
+ name="documento_ventas_q2", # logical name only
65
+ replace_existing=True, # default: replace and purge previous history
66
+ commit_message=None, # default: automatic message
67
+ )
68
+ print(record)
69
+ ```
70
+
71
+ ## Download
72
+
73
+ ```python
74
+ from gitstore import GitStoreDownloader
75
+
76
+ downloader = GitStoreDownloader(
77
+ raw_base_url="https://raw.githubusercontent.com/USER/REPO/main",
78
+ )
79
+
80
+ output_path = downloader.restore(
81
+ name="documento_ventas_q2",
82
+ output_path="C:/restore/documento.pdf",
83
+ overwrite=True,
84
+ )
85
+ print(output_path)
86
+ ```
87
+
88
+ ## Destroy
89
+
90
+ ```python
91
+ from gitstore import GitStoreUploader
92
+
93
+ uploader = GitStoreUploader(repo_path="C:/repos/my-publish-repo")
94
+ uploader.destroy(name="documento_ventas_q2")
95
+ ```
96
+
97
+ `destroy(...)` removes the current artifact and rewrites Git history for that artifact path.
98
+
99
+ ## Password Source
100
+
101
+ Both classes auto-detect password from:
102
+
103
+ - `GITSTORE_PASSWORD`
104
+
105
+ If `password` is not passed, the environment variable is used.
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/gitstore/__init__.py
4
+ src/gitstore/client.py
5
+ src/gitstore/config.py
6
+ src/gitstore/crypto_ops.py
7
+ src/gitstore/github_ops.py
8
+ src/gitstore.egg-info/PKG-INFO
9
+ src/gitstore.egg-info/SOURCES.txt
10
+ src/gitstore.egg-info/dependency_links.txt
11
+ src/gitstore.egg-info/requires.txt
12
+ src/gitstore.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ requests>=2.31.0
2
+ utilitz[crypto]
@@ -0,0 +1 @@
1
+ gitstore