sprawdzai-cli 0.2.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.
sai/main.py ADDED
@@ -0,0 +1,30 @@
1
+ import typer
2
+ from .commands.backend import backend
3
+ from .commands.login import login
4
+ from .commands.logout import logout
5
+ from .commands.auth_token import auth_token
6
+ from .commands.me import me
7
+ from .commands.pull import pull
8
+ from .commands.sync import sync
9
+ from .commands.sub import sub
10
+ from .commands.ds import ds_app
11
+
12
+ app = typer.Typer(
13
+ no_args_is_help=True,
14
+ help="""
15
+ SprawdzAI CLI tool.
16
+ """
17
+ )
18
+
19
+ app.command()(backend)
20
+ app.command()(login)
21
+ app.command()(logout)
22
+ app.command()(auth_token)
23
+ app.command()(me)
24
+ app.command()(pull)
25
+ app.command()(sync)
26
+ app.command()(sub)
27
+ app.add_typer(ds_app, name="ds")
28
+
29
+ def main():
30
+ app()
sai/state.py ADDED
@@ -0,0 +1,92 @@
1
+ import json
2
+ import time
3
+ from pathlib import Path
4
+ import re
5
+ from urllib.parse import urlparse, urlunparse
6
+
7
+ STATE_FILE = Path.home() / ".sprawdzai"
8
+
9
+ DEFAULT_STATE = {
10
+ "current_backend": "remote",
11
+ "tokens": {}
12
+ }
13
+
14
+ def load_state():
15
+ if not STATE_FILE.exists():
16
+ return DEFAULT_STATE.copy()
17
+ with open(STATE_FILE, "r") as f:
18
+ return json.load(f)
19
+
20
+ def save_state(state):
21
+ with open(STATE_FILE, "w") as f:
22
+ json.dump(state, f, indent=4)
23
+
24
+ def get_current_backend():
25
+ return load_state()["current_backend"]
26
+
27
+ def set_current_backend(backend: str):
28
+ state = load_state()
29
+ state["current_backend"] = backend
30
+ save_state(state)
31
+
32
+ def get_token(backend):
33
+ state = load_state()
34
+ return state["tokens"].get(backend)
35
+
36
+ def set_token(backend, token, expires_in):
37
+ state = load_state()
38
+ state["tokens"][backend] = {
39
+ "token": token,
40
+ "refresh_at": int(time.time() + expires_in // 2),
41
+ }
42
+ save_state(state)
43
+
44
+ def clear_token(backend):
45
+ state = load_state()
46
+ if backend in state["tokens"]:
47
+ del state["tokens"][backend]
48
+ save_state(state)
49
+
50
+ def clear_all_tokens():
51
+ state = load_state()
52
+ state["tokens"] = {}
53
+ save_state(state)
54
+
55
+ def normalize_url(url: str) -> str:
56
+ parsed = urlparse(url)
57
+ scheme = parsed.scheme.lower()
58
+ netloc = parsed.netloc.lower()
59
+ path = parsed.path.rstrip("/")
60
+ if (scheme == "http" and netloc.endswith(":80")) or (scheme == "https" and netloc.endswith(":443")):
61
+ netloc = netloc.rsplit(":", 1)[0]
62
+ normalized = urlunparse((scheme, netloc, path, "", "", ""))
63
+ return normalized
64
+
65
+ def get_backend_url(name: str) -> str:
66
+ name = name.lower()
67
+
68
+ if name in ["l", "local"] or re.fullmatch(r"l\d+", name):
69
+ return "http://localhost:8080"
70
+ if name in ["r", "remote"] or re.fullmatch(r"r\d+", name):
71
+ return "https://sprawdzai.org"
72
+
73
+ if name.startswith("http://") or name.startswith("https://"):
74
+ return normalize_url(name)
75
+
76
+ raise ValueError(f"Unknown backend type: {name}")
77
+
78
+ def get_frontend_url(name: str) -> str:
79
+ name = name.lower()
80
+
81
+ if name in ["l", "local"] or re.fullmatch(r"l\d+", name):
82
+ return "https://localhost"
83
+ if name in ["r", "remote"] or re.fullmatch(r"r\d+", name):
84
+ return "https://sprawdzai.org"
85
+
86
+ if name.startswith("http://") or name.startswith("https://"):
87
+ return normalize_url(name)
88
+
89
+ raise ValueError(f"Unknown backend type: {name}")
90
+
91
+ def resolve_backend(name: str | None) -> str:
92
+ return get_current_backend() if name is None else name
sai/utils/client.py ADDED
@@ -0,0 +1,49 @@
1
+ import time
2
+ from ..state import get_backend_url, get_token, set_token
3
+ import requests
4
+
5
+ class APIClient:
6
+ def __init__(self, backend_name: str):
7
+ self.backend_name = backend_name
8
+ self.base_url = get_backend_url(backend_name) + '/api'
9
+
10
+ def _auth_header(self):
11
+ data = get_token(self.backend_name)
12
+ if not data:
13
+ return {}
14
+
15
+ if data["refresh_at"] > time.time():
16
+ self.refresh(data["token"])
17
+ data = get_token(self.backend_name)
18
+
19
+ return {"Authorization": f"Bearer {data['token']}"}
20
+
21
+ def refresh(self, token: str):
22
+ try:
23
+ url = f"{self.base_url}/auth/refresh"
24
+ hdr = {"Authorization": f"Bearer {token}"}
25
+ resp = requests.post(url, headers=hdr)
26
+ if resp.status_code == 200:
27
+ data = resp.json()
28
+ set_token(
29
+ self.backend_name,
30
+ data["access_token"],
31
+ data["expires_in"]
32
+ )
33
+ except Exception as e:
34
+ pass
35
+
36
+ def get(self, path, **kwargs):
37
+ return requests.get(self.base_url + path, headers=self._auth_header(), **kwargs)
38
+
39
+ def post(self, path, json=None, **kwargs):
40
+ return requests.post(self.base_url + path, json=json, headers=self._auth_header(), **kwargs)
41
+
42
+ def delete(self, path, json=None, **kwargs):
43
+ return requests.delete(self.base_url + path, json=json, headers=self._auth_header(), **kwargs)
44
+
45
+ def put(self, path, json=None, **kwargs):
46
+ return requests.put(self.base_url + path, json=json, headers=self._auth_header(), **kwargs)
47
+
48
+ def patch(self, path, json=None, **kwargs):
49
+ return requests.patch(self.base_url + path, json=json, headers=self._auth_header(), **kwargs)
@@ -0,0 +1,45 @@
1
+ import typer
2
+ from requests import Response
3
+ import json
4
+
5
+ def get_response_error_text(resp: Response) -> str:
6
+ error_map = {
7
+ 404: 'Not Found (404)',
8
+ 500: 'Internal Server Error (500)',
9
+ 502: 'Bad Gateway (502)'
10
+ }
11
+ detail_map = {
12
+ 'user_not_authenticated': 'You are not authenticated',
13
+ 'invalid_credentials': 'Invalid login and/or password',
14
+ 'task_not_found': 'Task not found - it doesn\'t exist or you don\'t have permissions to see it (are you logged in to correct account?)',
15
+ 'slug_not_valid': 'Slug can only contain lowercase Latin latters, digits or "-" character and must be non-empty',
16
+ 'slug_not_unique': 'There already exists another task with the same slug'
17
+ }
18
+ err_text_begin = error_map[resp.status_code] if resp.status_code in error_map.keys() else f'Error ({resp.status_code})'
19
+
20
+ if len(resp.text) == 0:
21
+ return f'{err_text_begin}: [Server did not return any response]'
22
+
23
+ try:
24
+ res = json.loads(resp.text)
25
+ except:
26
+ return f'{err_text_begin}: {resp.text}'
27
+
28
+ resp_keys = list(res.keys())
29
+ if len(resp_keys) == 1 and resp_keys[0] == "detail":
30
+ detail = res["detail"]
31
+ if type(detail) == str and detail in detail_map.keys():
32
+ return f'{err_text_begin}: {detail_map[detail]}'
33
+ return f'{err_text_begin}: {detail}'
34
+ return f'{err_text_begin}:\n{json.dumps(res, indent=4)}'
35
+
36
+ def write_error(txt: str):
37
+ typer.secho(txt, err=True, fg=typer.colors.RED)
38
+ raise typer.Exit(code=1)
39
+
40
+ def warn(txt: str):
41
+ typer.secho(txt, err=True, fg=typer.colors.YELLOW)
42
+
43
+ def handle_response_error(resp: Response):
44
+ write_error(get_response_error_text(resp))
45
+
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.4
2
+ Name: sprawdzai-cli
3
+ Version: 0.2.0
4
+ Summary: SprawdzAI command line tool
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: typer>=0.20.0
8
+ Requires-Dist: requests>=2.32.5
9
+ Requires-Dist: aiofiles>=25.1.0
10
+ Requires-Dist: tqdm>=4.67.1
11
+
12
+ ```bash
13
+ cd src/cli
14
+ ```
15
+
16
+ ```bash
17
+ pip install build
18
+ ```
19
+
20
+ To build:
21
+
22
+ ```bash
23
+ bash build.sh
24
+ ```
25
+
26
+ To install and run locally:
27
+
28
+ ```bash
29
+ pip install -e .
30
+ ```
31
+
32
+ Then run:
33
+
34
+ ```bash
35
+ sai
36
+ ```
37
+
38
+ All additional information and production installation link available on [SprawdzAI](https://sprawdzai.org/tworzenie-zadan)
@@ -0,0 +1,31 @@
1
+ sai/__init__.py,sha256=FRdQHcp_Pm9K5EzLsngbrqqbzbPOFotF_uWstGKywkA,278
2
+ sai/__main__.py,sha256=Vdhw8YA1K3wPMlbJQYL5WqvRzAKVeZ16mZQFO9VRmCo,62
3
+ sai/main.py,sha256=2X-4wm3tuWC_DNutZ_idA3W6DOLE5Av2MoaqQR2xWxM,643
4
+ sai/state.py,sha256=Ckn_U-ZpzCacQH9VS25a4LqRSpl057W511tkRvSYsII,2573
5
+ sai/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ sai/commands/auth_token.py,sha256=DbZJuPW9XUWwybHBp0X5hCu-c-O1S5RDkZNG7k1ubbk,834
7
+ sai/commands/backend.py,sha256=zl0ZRfhN2aR79-vizrP0dQVf6pCcBsCbMM06fsYlE8Q,644
8
+ sai/commands/checklib.py,sha256=HpwXWmu7TalsY0FRN9Zu0PLnhi6y_fLSS2MHd_DcF8U,4699
9
+ sai/commands/login.py,sha256=krD6BDZE4xEHaay2O_0UwFJU7vNkI7dfM2mkDmSDEBA,1371
10
+ sai/commands/logout.py,sha256=MyJgbB0CDb_Dm0QaXqRoB9PcoJXHvToE5lcRgJEviPI,906
11
+ sai/commands/me.py,sha256=KW7YVavcIxRKifUDcmACoKnuGB4w4f5j4ladSLoJvEo,1486
12
+ sai/commands/open.py,sha256=1l27DR1jQRyvNFwR9eRSDxxH2yjYXXft1SBSAoFA3F0,47
13
+ sai/commands/pull.py,sha256=dV_F0dS6SxFSuY3aG7SP7wN9rAAk3MRTCKs5vzsBYqE,2003
14
+ sai/commands/reload.py,sha256=V5Iuc0z0PFg2mrycIOIBF-rPf0kYOQiN74lzjOFz9bI,1544
15
+ sai/commands/sub.py,sha256=dqZQ0CAzEmAG3lazyhynk5qU02b7MHG4lg4TJn2ji-A,7808
16
+ sai/commands/sync.py,sha256=NkgegXuAViOorfbMHUtunaI469sRu-T7cfk3Ip8WZYQ,26496
17
+ sai/commands/ds/__init__.py,sha256=tvFgY19iTy4mEj3EXh96B_cPwYREEUDkKuZ3g1nsBiY,2055
18
+ sai/commands/ds/create.py,sha256=0K4H-DtqrxDLL5DhCH3YR7a6yU2ZOYeJsQUGfqkAPs4,3919
19
+ sai/commands/ds/link.py,sha256=3cSiJMj36cl8UVm3EF7e1Q3coQ2eM40O8CKnzNmpxR8,4003
20
+ sai/commands/ds/pull.py,sha256=79UnpUFBfiUPTq-xMbapbkGJYGk30jIJLBBD-GLPhXM,5002
21
+ sai/commands/ds/rename.py,sha256=c1xi7aa9KIPMIqQbmbXsIypZVbW5ebtcyCGKaS81tm0,1319
22
+ sai/commands/ds/send.py,sha256=1Pev-9ESo2SpDAG6eIxIxjcUIW-WTMSbaqW0xgyC3b0,5674
23
+ sai/commands/ds/unlink.py,sha256=nmfQpG8UYxZp_lkpRty5Wvq3uNCZ3ixwj0wH_qBPnQI,4137
24
+ sai/commands/ds/utils.py,sha256=p3iFOgzzU3ounZasR--5-CAPDqrZbvbg_fHUDYv7Ko8,5737
25
+ sai/utils/client.py,sha256=L0jveIUbakUoQgaL29vm9mPaHZrf-qxRMW8iv2w9flI,1780
26
+ sai/utils/error_handler.py,sha256=78-bXqVX8FSazhk6jwhBnFsxB52KZSunFkMr3L0QdSY,1707
27
+ sprawdzai_cli-0.2.0.dist-info/METADATA,sha256=ThwqfGLAdYmDYQ4p2-5MkoNEIYR6ptowQH3f1D_AXNI,584
28
+ sprawdzai_cli-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
29
+ sprawdzai_cli-0.2.0.dist-info/entry_points.txt,sha256=l0yg1xfFdhPxHAFRJcQOX0dVhbQ1O-Hr6j4JDvIWohU,38
30
+ sprawdzai_cli-0.2.0.dist-info/top_level.txt,sha256=Kvf-0z0sZYQbLuM6ta183EOzD37k3VZbRH-h7HXntR0,4
31
+ sprawdzai_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sai = sai.main:main
@@ -0,0 +1 @@
1
+ sai