pypas-cli 0.0.2__tar.gz → 0.0.4__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.
- {pypas_cli-0.0.2 → pypas_cli-0.0.4}/.gitignore +3 -0
- {pypas_cli-0.0.2 → pypas_cli-0.0.4}/PKG-INFO +5 -1
- {pypas_cli-0.0.2 → pypas_cli-0.0.4}/pyproject.toml +9 -2
- pypas_cli-0.0.4/src/pypas/__init__.py +3 -0
- pypas_cli-0.0.4/src/pypas/lib/console.py +14 -0
- pypas_cli-0.0.4/src/pypas/lib/decorators.py +15 -0
- pypas_cli-0.0.4/src/pypas/lib/exercise.py +70 -0
- pypas_cli-0.0.4/src/pypas/lib/utils.py +76 -0
- pypas_cli-0.0.4/src/pypas/main.py +43 -0
- pypas_cli-0.0.4/src/pypas/settings.py +7 -0
- pypas_cli-0.0.2/requirements.txt +0 -0
- pypas_cli-0.0.2/src/pypas/core.py +0 -1
- pypas_cli-0.0.2/src/pypas/main.py +0 -16
- {pypas_cli-0.0.2 → pypas_cli-0.0.4}/CHANGELOG.md +0 -0
- {pypas_cli-0.0.2 → pypas_cli-0.0.4}/LICENSE +0 -0
- {pypas_cli-0.0.2 → pypas_cli-0.0.4}/README.md +0 -0
- {pypas_cli-0.0.2 → pypas_cli-0.0.4}/justfile +0 -0
- {pypas_cli-0.0.2 → pypas_cli-0.0.4}/requirements-dev.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pypas-cli
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: Python Practical Assignments
|
|
5
5
|
Project-URL: homepage, https://pypas.es
|
|
6
6
|
Project-URL: repository, https://github.com/sdelquin/pypas-cli
|
|
@@ -33,6 +33,10 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
33
33
|
Classifier: Operating System :: OS Independent
|
|
34
34
|
Classifier: Programming Language :: Python :: 3
|
|
35
35
|
Requires-Python: >=3.10
|
|
36
|
+
Requires-Dist: prettyconf
|
|
37
|
+
Requires-Dist: pytest
|
|
38
|
+
Requires-Dist: requests
|
|
39
|
+
Requires-Dist: rich
|
|
36
40
|
Requires-Dist: setuptools
|
|
37
41
|
Requires-Dist: typer
|
|
38
42
|
Description-Content-Type: text/markdown
|
|
@@ -4,9 +4,16 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pypas-cli"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.4"
|
|
8
8
|
license = { file = "LICENSE" }
|
|
9
|
-
dependencies = [
|
|
9
|
+
dependencies = [
|
|
10
|
+
'setuptools',
|
|
11
|
+
'typer',
|
|
12
|
+
'prettyconf',
|
|
13
|
+
'requests',
|
|
14
|
+
'rich',
|
|
15
|
+
'pytest',
|
|
16
|
+
]
|
|
10
17
|
authors = [
|
|
11
18
|
{ email = "sdelquin@gmail.com" },
|
|
12
19
|
{ name = "Sergio Delgado Quintero" },
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.theme import Theme
|
|
3
|
+
|
|
4
|
+
custom_theme = Theme(
|
|
5
|
+
{
|
|
6
|
+
'info': 'cyan',
|
|
7
|
+
'warning': 'yellow',
|
|
8
|
+
'danger': 'bold red',
|
|
9
|
+
'success': 'bold green',
|
|
10
|
+
'note': 'bold blue',
|
|
11
|
+
}
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
console = Console(theme=custom_theme, highlight=False)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from .console import console
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def inside_exercise(func):
|
|
8
|
+
@functools.wraps(func)
|
|
9
|
+
def wrapper(*args, **kwargs):
|
|
10
|
+
if not os.path.exists('.pypas.toml'):
|
|
11
|
+
console.print('Current folder does not seem to be a pypas exercise!', style='danger')
|
|
12
|
+
else:
|
|
13
|
+
return func(*args, **kwargs)
|
|
14
|
+
|
|
15
|
+
return wrapper
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import zipfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import urljoin
|
|
7
|
+
|
|
8
|
+
import tomllib
|
|
9
|
+
from pypas import settings
|
|
10
|
+
|
|
11
|
+
from . import utils
|
|
12
|
+
from .console import console
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Exercise:
|
|
16
|
+
def __init__(self, exercise_slug: str):
|
|
17
|
+
self.slug = exercise_slug
|
|
18
|
+
self.url = urljoin(settings.PYPAS_EXERCISES_URLPATH, exercise_slug + '/')
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def zipname(self) -> str:
|
|
22
|
+
return f'{self.slug}.zip'
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def folder(self) -> Path:
|
|
26
|
+
return Path(self.slug)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def cwd_folder(self) -> str:
|
|
30
|
+
return f'./{self.folder}'
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def config(self) -> dict:
|
|
34
|
+
if not getattr(self, '_cfg', None) or not self._cfg: # type: ignore
|
|
35
|
+
self._cfg = Exercise.load_config()
|
|
36
|
+
return self._cfg
|
|
37
|
+
|
|
38
|
+
def folder_exists(self) -> bool:
|
|
39
|
+
return self.folder.exists()
|
|
40
|
+
|
|
41
|
+
def download(self) -> Path | None:
|
|
42
|
+
console.print(f'Getting exercise from: [italic]{self.url}')
|
|
43
|
+
if downloaded_zip := utils.download(self.url, self.zipname, save_temp=True):
|
|
44
|
+
self.downloaded_zip = downloaded_zip
|
|
45
|
+
return downloaded_zip
|
|
46
|
+
|
|
47
|
+
def unzip(self):
|
|
48
|
+
console.print('Inflating exercise bundle', end=' ')
|
|
49
|
+
with zipfile.ZipFile(self.downloaded_zip) as zip_ref:
|
|
50
|
+
zip_ref.extractall()
|
|
51
|
+
console.print('✔', style='success')
|
|
52
|
+
os.remove(self.downloaded_zip)
|
|
53
|
+
|
|
54
|
+
def open_docs(self):
|
|
55
|
+
os.system(f'{utils.get_open_cmd()} docs/README.pdf')
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_config(cls) -> Exercise | None:
|
|
59
|
+
try:
|
|
60
|
+
return cls(Exercise.load_config()['slug'])
|
|
61
|
+
except KeyError:
|
|
62
|
+
console.print(
|
|
63
|
+
f'Config file {settings.EXERCISE_CONFIG_FILE} is corrupt!', style='danger'
|
|
64
|
+
)
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def load_config(filename: str = settings.EXERCISE_CONFIG_FILE):
|
|
69
|
+
with open(filename, 'rb') as f:
|
|
70
|
+
return tomllib.load(f)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from sys import platform
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
from rich.progress import (
|
|
7
|
+
BarColumn,
|
|
8
|
+
DownloadColumn,
|
|
9
|
+
Progress,
|
|
10
|
+
TextColumn,
|
|
11
|
+
TimeRemainingColumn,
|
|
12
|
+
TransferSpeedColumn,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .console import console
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OS:
|
|
19
|
+
LINUX = 1
|
|
20
|
+
MACOS = 2
|
|
21
|
+
WINDOWS = 3
|
|
22
|
+
OTHER = 4
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def download(url: str, filename: str, save_temp=False, chunk_size=1024) -> Path | None:
|
|
26
|
+
# https://gist.github.com/yanqd0/c13ed29e29432e3cf3e7c38467f42f51
|
|
27
|
+
if save_temp:
|
|
28
|
+
tmp_file = tempfile.NamedTemporaryFile(delete=False)
|
|
29
|
+
target_file = tmp_file.name
|
|
30
|
+
else:
|
|
31
|
+
target_file = filename
|
|
32
|
+
try:
|
|
33
|
+
resp = requests.get(url, stream=True)
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
except Exception as err:
|
|
36
|
+
console.print(err, style='danger')
|
|
37
|
+
return None
|
|
38
|
+
with open(target_file, 'wb') as file, Progress(
|
|
39
|
+
TextColumn('[bold blue]{task.fields[filename]}'),
|
|
40
|
+
BarColumn(),
|
|
41
|
+
'[progress.percentage]{task.percentage:>3.1f}%',
|
|
42
|
+
'•',
|
|
43
|
+
DownloadColumn(),
|
|
44
|
+
'•',
|
|
45
|
+
TransferSpeedColumn(),
|
|
46
|
+
'•',
|
|
47
|
+
TimeRemainingColumn(),
|
|
48
|
+
) as progress:
|
|
49
|
+
total = int(resp.headers.get('content-length', 0))
|
|
50
|
+
task_id = progress.add_task('download', filename=filename, total=total)
|
|
51
|
+
for data in resp.iter_content(chunk_size=chunk_size):
|
|
52
|
+
size = file.write(data)
|
|
53
|
+
progress.update(task_id, advance=size)
|
|
54
|
+
return Path(target_file)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_os() -> int:
|
|
58
|
+
if platform.startswith('linux'):
|
|
59
|
+
return OS.LINUX
|
|
60
|
+
if platform == 'darwin':
|
|
61
|
+
return OS.MACOS
|
|
62
|
+
if platform == 'win32':
|
|
63
|
+
return OS.WINDOWS
|
|
64
|
+
return OS.OTHER
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_open_cmd() -> str:
|
|
68
|
+
match check_os():
|
|
69
|
+
case OS.LINUX:
|
|
70
|
+
return 'xdg-open'
|
|
71
|
+
case OS.MACOS:
|
|
72
|
+
return 'open'
|
|
73
|
+
case OS.WINDOWS:
|
|
74
|
+
return 'start'
|
|
75
|
+
case _:
|
|
76
|
+
return ''
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich import print
|
|
3
|
+
from rich.prompt import Confirm
|
|
4
|
+
|
|
5
|
+
from pypas import Exercise, console
|
|
6
|
+
from pypas.lib.decorators import inside_exercise
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(
|
|
9
|
+
add_completion=False,
|
|
10
|
+
help='✨ pypas',
|
|
11
|
+
no_args_is_help=True,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def get(exercise_slug: str):
|
|
17
|
+
if (exercise := Exercise(exercise_slug)).folder_exists():
|
|
18
|
+
console.print(f'Folder {exercise.cwd_folder} already exists!', style='warning')
|
|
19
|
+
console.print(
|
|
20
|
+
'[italic]If continue, files coming from server will [red]OVERWRITE[/red] your existing files'
|
|
21
|
+
)
|
|
22
|
+
if not Confirm.ask('Continue', default=False):
|
|
23
|
+
return
|
|
24
|
+
if exercise.download():
|
|
25
|
+
exercise.unzip()
|
|
26
|
+
console.print(f'Exercise is available at [note]{exercise.cwd_folder}[/note] [success]✔')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
@inside_exercise
|
|
31
|
+
def doc():
|
|
32
|
+
if exercise := Exercise.from_config():
|
|
33
|
+
exercise.open_docs()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.command()
|
|
37
|
+
@inside_exercise
|
|
38
|
+
def update():
|
|
39
|
+
print('TODO!')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == '__main__':
|
|
43
|
+
app()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from urllib.parse import urljoin
|
|
2
|
+
|
|
3
|
+
from prettyconf import config
|
|
4
|
+
|
|
5
|
+
PYPAS_BASE_URL = config('PYPAS_BASE_URL', default='https://pypas.es/')
|
|
6
|
+
PYPAS_EXERCISES_URLPATH = urljoin(PYPAS_BASE_URL, '/exercises/')
|
|
7
|
+
EXERCISE_CONFIG_FILE = config('EXERCISE_CONFIG_FILE', default='.pypas.toml')
|
pypas_cli-0.0.2/requirements.txt
DELETED
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# TODO
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|