gitstore 0.1.0__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.
- gitstore/__init__.py +8 -0
- gitstore/client.py +249 -0
- gitstore/config.py +13 -0
- gitstore/crypto_ops.py +93 -0
- gitstore/github_ops.py +61 -0
- gitstore-0.1.0.dist-info/METADATA +105 -0
- gitstore-0.1.0.dist-info/RECORD +9 -0
- gitstore-0.1.0.dist-info/WHEEL +5 -0
- gitstore-0.1.0.dist-info/top_level.txt +1 -0
gitstore/__init__.py
ADDED
gitstore/client.py
ADDED
|
@@ -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
|
+
|
gitstore/config.py
ADDED
|
@@ -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.")
|
gitstore/crypto_ops.py
ADDED
|
@@ -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
|
+
)
|
gitstore/github_ops.py
ADDED
|
@@ -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,9 @@
|
|
|
1
|
+
gitstore/__init__.py,sha256=pHAAzHZGmY_-qOAGM67oyqCT1OJPAxFQLtF-EkDHTGc,226
|
|
2
|
+
gitstore/client.py,sha256=p1psMDHtvO7CqI5r0IilyD2roQfKkPWmGoILCxI1eh4,9771
|
|
3
|
+
gitstore/config.py,sha256=FTeDpvPnK0aEFNnAPbazeK5Bw2e8dkPiUtF0FwmY700,464
|
|
4
|
+
gitstore/crypto_ops.py,sha256=Q1w0ivFSpBFbSjw0Q1OTX7cBKrtyQpz1riHVf-71pvQ,2785
|
|
5
|
+
gitstore/github_ops.py,sha256=mtOaAP_3Yt9XCley3XfemR9aRkxgs_BphIAPZ2tGWpk,2061
|
|
6
|
+
gitstore-0.1.0.dist-info/METADATA,sha256=qsDFuVakYf-wyVaq2BSBmOA9Sp7_yRb6q4cmKQY_NL0,2318
|
|
7
|
+
gitstore-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
8
|
+
gitstore-0.1.0.dist-info/top_level.txt,sha256=p8mWIVzn1WpffuAYcTDg2lP8xyNqKXjK0PoNQrPOvmA,9
|
|
9
|
+
gitstore-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
gitstore
|