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.
@@ -2,3 +2,6 @@ dist
2
2
  *.egg-info
3
3
  *.pyc
4
4
  .python-version
5
+ .DS_Store
6
+ .env
7
+ .sandbox
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pypas-cli
3
- Version: 0.0.2
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.2"
7
+ version = "0.0.4"
8
8
  license = { file = "LICENSE" }
9
- dependencies = ['setuptools', 'typer']
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,3 @@
1
+ from .lib.console import console # noqa
2
+ from .lib import utils # noqa
3
+ from .lib.exercise import Exercise # noqa
@@ -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')
File without changes
@@ -1 +0,0 @@
1
- # TODO
@@ -1,16 +0,0 @@
1
- import typer
2
-
3
- app = typer.Typer(
4
- add_completion=False,
5
- help='✨ pypas',
6
- no_args_is_help=True,
7
- )
8
-
9
-
10
- @app.command()
11
- def run():
12
- print('Hi there!')
13
-
14
-
15
- if __name__ == '__main__':
16
- app()
File without changes
File without changes
File without changes
File without changes