skypro-cli-tool 0.2.0__tar.gz → 0.3.2__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.
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/PKG-INFO +15 -5
- skypro_cli_tool-0.3.2/README.md +30 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/pyproject.toml +40 -2
- skypro_cli_tool-0.3.2/src/skypro/cli/app.py +55 -0
- skypro_cli_tool-0.3.2/src/skypro/cli/commands/__init__.py +3 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/cli/commands/summary.py +6 -14
- skypro_cli_tool-0.3.2/src/skypro/config/__init__.py +18 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/config/logging.py +7 -8
- skypro_cli_tool-0.3.2/src/skypro/config/service.py +44 -0
- skypro_cli_tool-0.3.2/src/skypro/config/settings.py +62 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/domain/services.py +2 -3
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/api/skypro/client.py +3 -3
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/api/skypro/dto.py +6 -1
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/http/client.py +2 -2
- skypro_cli_tool-0.3.2/src/skypro/infra/repositories/summary.py +55 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/repositories/work.py +1 -1
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/main.py +1 -1
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/use_cases/get_summary_info.py +6 -3
- skypro_cli_tool-0.2.0/README.md +0 -21
- skypro_cli_tool-0.2.0/src/skypro/cli/app.py +0 -33
- skypro_cli_tool-0.2.0/src/skypro/cli/commands/__init__.py +0 -4
- skypro_cli_tool-0.2.0/src/skypro/cli/commands/initialize.py +0 -36
- skypro_cli_tool-0.2.0/src/skypro/config/settings.py +0 -127
- skypro_cli_tool-0.2.0/src/skypro/infra/http/__init__.py +0 -0
- skypro_cli_tool-0.2.0/src/skypro/infra/repositories/summary.py +0 -30
- skypro_cli_tool-0.2.0/src/skypro/logs/skypro.log +0 -107
- skypro_cli_tool-0.2.0/src/skypro/logs/skypro.log.1 +0 -108
- skypro_cli_tool-0.2.0/src/skypro/logs/skypro.log.2 +0 -101
- skypro_cli_tool-0.2.0/src/skypro/logs/skypro.log.3 +0 -100
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/__init__.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/cli/__init__.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/cli/renders/__init__.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/cli/renders/summary_table.py +0 -0
- {skypro_cli_tool-0.2.0/src/skypro/config → skypro_cli_tool-0.3.2/src/skypro/domain}/__init__.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/domain/enums.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/domain/errors.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/domain/models.py +0 -0
- {skypro_cli_tool-0.2.0/src/skypro/domain → skypro_cli_tool-0.3.2/src/skypro/infra}/__init__.py +0 -0
- {skypro_cli_tool-0.2.0/src/skypro/infra → skypro_cli_tool-0.3.2/src/skypro/infra/api}/__init__.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/api/base.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/api/errors.py +0 -0
- {skypro_cli_tool-0.2.0/src/skypro/infra/api → skypro_cli_tool-0.3.2/src/skypro/infra/api/skypro}/__init__.py +0 -0
- {skypro_cli_tool-0.2.0/src/skypro/infra/api/skypro → skypro_cli_tool-0.3.2/src/skypro/infra/http}/__init__.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/http/errors.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/repositories/__init__.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/infra/repositories/errors.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/use_cases/__init__.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/utils.py +0 -0
- {skypro_cli_tool-0.2.0 → skypro_cli_tool-0.3.2}/src/skypro/work_prices.json +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: skypro-cli-tool
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: SkyPro cli tool
|
|
5
5
|
Requires-Dist: aiohttp>=3.13.5
|
|
6
|
+
Requires-Dist: click>=8.4.2
|
|
6
7
|
Requires-Dist: httpx>=0.28.1
|
|
7
8
|
Requires-Dist: pydantic>=2.13.3
|
|
8
|
-
Requires-Dist: pydantic-settings
|
|
9
|
+
Requires-Dist: pydantic-settings>=2.14.0
|
|
9
10
|
Requires-Dist: typer>=0.25.0
|
|
10
11
|
Requires-Python: >=3.13
|
|
11
12
|
Description-Content-Type: text/markdown
|
|
@@ -14,13 +15,22 @@ Description-Content-Type: text/markdown
|
|
|
14
15
|
|
|
15
16
|
## How to start
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
Show current config
|
|
18
19
|
|
|
19
20
|
```commandline
|
|
20
|
-
sky-cli
|
|
21
|
+
sky-cli config
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
Reconfigure SkyPro access
|
|
25
|
+
|
|
26
|
+
```commandline
|
|
27
|
+
sky-cli config --skypro
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If SkyPro credentials are missing, the `summary` command exits with an error.
|
|
31
|
+
Configure them first with `sky-cli config --skypro`.
|
|
32
|
+
|
|
33
|
+
Check money you earned this month
|
|
24
34
|
|
|
25
35
|
```commandline
|
|
26
36
|
sky-cli summary
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SkyPro cli tool
|
|
2
|
+
|
|
3
|
+
## How to start
|
|
4
|
+
|
|
5
|
+
Show current config
|
|
6
|
+
|
|
7
|
+
```commandline
|
|
8
|
+
sky-cli config
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Reconfigure SkyPro access
|
|
12
|
+
|
|
13
|
+
```commandline
|
|
14
|
+
sky-cli config --skypro
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
If SkyPro credentials are missing, the `summary` command exits with an error.
|
|
18
|
+
Configure them first with `sky-cli config --skypro`.
|
|
19
|
+
|
|
20
|
+
Check money you earned this month
|
|
21
|
+
|
|
22
|
+
```commandline
|
|
23
|
+
sky-cli summary
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Another month or year
|
|
27
|
+
|
|
28
|
+
```commandline
|
|
29
|
+
sky-cli summary --month 3 --year 2026
|
|
30
|
+
```
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "skypro-cli-tool"
|
|
3
|
-
version = "0.2
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "SkyPro cli tool"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
7
7
|
dependencies = [
|
|
8
8
|
"aiohttp>=3.13.5",
|
|
9
|
+
"click>=8.4.2",
|
|
9
10
|
"httpx>=0.28.1",
|
|
10
11
|
"pydantic>=2.13.3",
|
|
11
|
-
"pydantic-settings
|
|
12
|
+
"pydantic-settings>=2.14.0",
|
|
12
13
|
"typer>=0.25.0",
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
[dependency-groups]
|
|
17
|
+
dev = [
|
|
18
|
+
{ include-group = "test" },
|
|
19
|
+
{ include-group = "lint" },
|
|
20
|
+
"ipython>=9.13.0",
|
|
21
|
+
]
|
|
16
22
|
test = [
|
|
17
23
|
"freezegun>=1.5.5",
|
|
18
24
|
"pytest>=9.0.3",
|
|
@@ -22,7 +28,9 @@ test = [
|
|
|
22
28
|
"pytest-mock>=3.15.1",
|
|
23
29
|
]
|
|
24
30
|
lint = [
|
|
31
|
+
"mypy>=2.3.0",
|
|
25
32
|
"ruff>=0.15.12",
|
|
33
|
+
"typos>=1.48.0",
|
|
26
34
|
]
|
|
27
35
|
|
|
28
36
|
[project.scripts]
|
|
@@ -65,6 +73,10 @@ preview = true
|
|
|
65
73
|
"PLR2004", # Magic value used in comparison
|
|
66
74
|
"RUF069", # Unreliable floating point equality comparison
|
|
67
75
|
]
|
|
76
|
+
"*/cli/app.py" = [
|
|
77
|
+
"FBT001", # Boolean-typed positional argument in function definition
|
|
78
|
+
"FBT003", # Boolean positional value in function call
|
|
79
|
+
]
|
|
68
80
|
|
|
69
81
|
[tool.ruff.lint.isort]
|
|
70
82
|
combine-as-imports = true
|
|
@@ -98,3 +110,29 @@ build-backend = "uv_build"
|
|
|
98
110
|
[tool.uv.build-backend]
|
|
99
111
|
module-name = "skypro"
|
|
100
112
|
module-root = "src"
|
|
113
|
+
|
|
114
|
+
[tool.mypy]
|
|
115
|
+
show_error_codes = true
|
|
116
|
+
ignore_missing_imports = true
|
|
117
|
+
disallow_incomplete_defs = true
|
|
118
|
+
disallow_untyped_defs = true
|
|
119
|
+
warn_unused_ignores = true
|
|
120
|
+
warn_no_return = false
|
|
121
|
+
warn_unreachable = true
|
|
122
|
+
no_implicit_optional = true
|
|
123
|
+
allow_redefinition = true
|
|
124
|
+
exclude_gitignore = true
|
|
125
|
+
plugins = ["pydantic.mypy"]
|
|
126
|
+
strict = true
|
|
127
|
+
|
|
128
|
+
[tool.pydantic-mypy]
|
|
129
|
+
init_forbid_extra = true
|
|
130
|
+
init_typed = true
|
|
131
|
+
warn_required_dynamic_aliases = true
|
|
132
|
+
|
|
133
|
+
[[tool.mypy.overrides]]
|
|
134
|
+
module = ['tests.*']
|
|
135
|
+
allow_untyped_defs = true
|
|
136
|
+
disable_error_code = [
|
|
137
|
+
"no-untyped-def", # Function is missing a type annotation
|
|
138
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from typer import Option
|
|
7
|
+
|
|
8
|
+
from skypro.config import (
|
|
9
|
+
configure_skypro_settings,
|
|
10
|
+
enable_console_logging,
|
|
11
|
+
get_settings,
|
|
12
|
+
is_skypro_configured,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from . import commands
|
|
16
|
+
|
|
17
|
+
app = typer.Typer()
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command(name='summary')
|
|
22
|
+
def get_summary(
|
|
23
|
+
year: int | None = Option(None, help='Год для расчета'),
|
|
24
|
+
month: int | None = Option(None, help='Месяц для расчета'),
|
|
25
|
+
verbose: bool = typer.Option(False, '-v', '--verbose'),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Расчет стоимости работ"""
|
|
28
|
+
if not is_skypro_configured():
|
|
29
|
+
console.print(
|
|
30
|
+
'[red]Настройки SkyPro не заданы. '
|
|
31
|
+
'Укажите их командой `sky-cli config --skypro`.[/]'
|
|
32
|
+
)
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
|
|
35
|
+
if verbose:
|
|
36
|
+
enable_console_logging(console)
|
|
37
|
+
|
|
38
|
+
asyncio.run(commands.show_summary(console, year, month))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command(name='config')
|
|
42
|
+
def configure(
|
|
43
|
+
skypro: bool = Option(
|
|
44
|
+
False,
|
|
45
|
+
'--skypro',
|
|
46
|
+
help='Изменить настройки доступа к SkyPro',
|
|
47
|
+
),
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Просмотр текущего конфига или настройка доступа к SkyPro"""
|
|
50
|
+
if skypro:
|
|
51
|
+
configure_skypro_settings(console)
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
settings = get_settings().model_dump(mode='json')
|
|
55
|
+
console.print(json.dumps(settings, ensure_ascii=False, indent=2))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from calendar import IllegalMonthError
|
|
2
|
-
from datetime import UTC,
|
|
2
|
+
from datetime import UTC, datetime
|
|
3
3
|
|
|
4
4
|
from click.exceptions import Exit
|
|
5
5
|
from rich.console import Console
|
|
@@ -8,7 +8,6 @@ from skypro.cli.renders import render_summary_table
|
|
|
8
8
|
from skypro.domain.errors import DomainError
|
|
9
9
|
from skypro.infra.repositories.errors import RepositoryError
|
|
10
10
|
from skypro.use_cases import get_summary_info
|
|
11
|
-
from skypro.utils import get_month_range
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
async def show_summary(
|
|
@@ -16,23 +15,16 @@ async def show_summary(
|
|
|
16
15
|
year: int | None,
|
|
17
16
|
month: int | None,
|
|
18
17
|
) -> None:
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
now = datetime.now(UTC).astimezone()
|
|
19
|
+
year = year or now.year
|
|
20
|
+
month = month or now.month
|
|
21
21
|
|
|
22
|
+
try:
|
|
22
23
|
with console.status('[cyan]Расчет стоимости работ'):
|
|
23
|
-
report, total, after_tax = await get_summary_info(
|
|
24
|
+
report, total, after_tax = await get_summary_info(year, month)
|
|
24
25
|
|
|
25
26
|
table = render_summary_table(report, total, after_tax)
|
|
26
27
|
console.print(table)
|
|
27
28
|
except (RepositoryError, DomainError, IllegalMonthError) as e:
|
|
28
29
|
console.print(f'[bold red]Error: {e!s}[/]')
|
|
29
30
|
raise Exit(code=1) from e
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def resolve_period(year: int | None, month: int | None) -> tuple[date, date]:
|
|
33
|
-
now = datetime.now(UTC).astimezone()
|
|
34
|
-
target_year = year or now.year
|
|
35
|
-
target_month = month or now.month
|
|
36
|
-
|
|
37
|
-
start, end = get_month_range(target_year, target_month)
|
|
38
|
-
return start, end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .logging import configure_logging, enable_console_logging
|
|
2
|
+
from .service import (
|
|
3
|
+
configure_skypro_settings,
|
|
4
|
+
ensure_skypro_configured,
|
|
5
|
+
is_skypro_configured,
|
|
6
|
+
)
|
|
7
|
+
from .settings import BASE_DIR, get_settings, save_settings
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
'BASE_DIR',
|
|
11
|
+
'configure_logging',
|
|
12
|
+
'configure_skypro_settings',
|
|
13
|
+
'enable_console_logging',
|
|
14
|
+
'ensure_skypro_configured',
|
|
15
|
+
'get_settings',
|
|
16
|
+
'is_skypro_configured',
|
|
17
|
+
'save_settings',
|
|
18
|
+
)
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import logging.config
|
|
3
3
|
from functools import cache
|
|
4
|
+
from typing import Any
|
|
4
5
|
|
|
5
6
|
from rich.console import Console
|
|
6
7
|
from rich.logging import RichHandler
|
|
7
8
|
|
|
8
|
-
from .settings import
|
|
9
|
+
from .settings import HOME_DIR, get_settings
|
|
9
10
|
|
|
10
|
-
LOG_DIR =
|
|
11
|
+
LOG_DIR = HOME_DIR / 'logs'
|
|
11
12
|
|
|
12
|
-
LOGGING_CONFIG = {
|
|
13
|
+
LOGGING_CONFIG: dict[str, Any] = {
|
|
13
14
|
'version': 1,
|
|
14
15
|
'disable_existing_loggers': False,
|
|
15
16
|
'formatters': {
|
|
@@ -27,10 +28,7 @@ LOGGING_CONFIG = {
|
|
|
27
28
|
'formatter': 'formatter',
|
|
28
29
|
},
|
|
29
30
|
},
|
|
30
|
-
'root': {
|
|
31
|
-
'handlers': ['file'],
|
|
32
|
-
'level': settings.log_level,
|
|
33
|
-
},
|
|
31
|
+
'root': {'handlers': ['file']},
|
|
34
32
|
'loggers': {
|
|
35
33
|
'httpx': {'handlers': ['file'], 'level': logging.WARNING, 'propagate': True},
|
|
36
34
|
},
|
|
@@ -39,7 +37,8 @@ LOGGING_CONFIG = {
|
|
|
39
37
|
|
|
40
38
|
@cache
|
|
41
39
|
def configure_logging() -> None:
|
|
42
|
-
LOG_DIR.mkdir(exist_ok=True)
|
|
40
|
+
LOG_DIR.mkdir(mode=0o700, exist_ok=True, parents=True)
|
|
41
|
+
LOGGING_CONFIG['root']['level'] = get_settings().log_level.upper()
|
|
43
42
|
logging.config.dictConfig(LOGGING_CONFIG)
|
|
44
43
|
|
|
45
44
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from pydantic import SecretStr
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from .settings import Settings, get_settings, save_settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def is_skypro_configured(settings: Settings | None = None) -> bool:
|
|
8
|
+
current_settings = settings or get_settings()
|
|
9
|
+
return bool(current_settings.skypro.email and current_settings.skypro.password)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def configure_skypro_settings(console: Console) -> Settings:
|
|
13
|
+
current_settings = get_settings()
|
|
14
|
+
|
|
15
|
+
console.print('[cyan]Настройка доступа к SkyPro[/]')
|
|
16
|
+
|
|
17
|
+
current_email = current_settings.skypro.email or ''
|
|
18
|
+
email_prompt = 'Email'
|
|
19
|
+
if current_email:
|
|
20
|
+
email_prompt = f'Email [{current_email}]'
|
|
21
|
+
|
|
22
|
+
email = console.input(f'{email_prompt}: ').strip() or current_email
|
|
23
|
+
password = console.input('Password: ', password=True).strip()
|
|
24
|
+
if not password and current_settings.skypro.password:
|
|
25
|
+
password = current_settings.skypro.password_value
|
|
26
|
+
|
|
27
|
+
updated_settings = current_settings.model_copy(
|
|
28
|
+
update={
|
|
29
|
+
'skypro': current_settings.skypro.model_copy(
|
|
30
|
+
update={'email': email, 'password': SecretStr(password)}
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
save_settings(updated_settings)
|
|
35
|
+
return updated_settings
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def ensure_skypro_configured(console: Console) -> Settings:
|
|
39
|
+
settings = get_settings()
|
|
40
|
+
if is_skypro_configured(settings):
|
|
41
|
+
return settings
|
|
42
|
+
|
|
43
|
+
console.print('[yellow]Конфигурация SkyPro не найдена. Заполним ее сейчас.[/]')
|
|
44
|
+
return configure_skypro_settings(console)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from functools import cache
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field, SecretStr
|
|
7
|
+
from pydantic_settings import (
|
|
8
|
+
BaseSettings,
|
|
9
|
+
JsonConfigSettingsSource,
|
|
10
|
+
PydanticBaseSettingsSource,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
|
14
|
+
|
|
15
|
+
HOME_DIR = Path.home() / '.skypro'
|
|
16
|
+
|
|
17
|
+
SETTINGS_FILE_PATH = HOME_DIR / 'settings.json'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SkyProSettings(BaseModel):
|
|
21
|
+
email: str | None = None
|
|
22
|
+
password: SecretStr | None = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def password_value(self) -> str:
|
|
26
|
+
if not self.password:
|
|
27
|
+
raise RuntimeError('Password does not set')
|
|
28
|
+
|
|
29
|
+
return self.password.get_secret_value()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Settings(BaseSettings, json_file=SETTINGS_FILE_PATH):
|
|
33
|
+
tax_percent: float = 6.0
|
|
34
|
+
default_request_timeout: int = 3
|
|
35
|
+
log_level: Literal['debug', 'info'] = 'info'
|
|
36
|
+
|
|
37
|
+
skypro: SkyProSettings = Field(default_factory=SkyProSettings)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def settings_customise_sources( # type: ignore[override]
|
|
41
|
+
cls,
|
|
42
|
+
settings_cls: type[BaseSettings],
|
|
43
|
+
**_: PydanticBaseSettingsSource,
|
|
44
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
45
|
+
return (JsonConfigSettingsSource(settings_cls),)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def save_settings(settings: Settings) -> None:
|
|
49
|
+
SETTINGS_FILE_PATH.parent.mkdir(mode=0o700, exist_ok=True)
|
|
50
|
+
|
|
51
|
+
data = settings.model_dump(mode='json')
|
|
52
|
+
data['skypro']['password'] = settings.skypro.password_value
|
|
53
|
+
|
|
54
|
+
with SETTINGS_FILE_PATH.open(mode='w', encoding='utf-8') as f:
|
|
55
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
56
|
+
|
|
57
|
+
get_settings.cache_clear()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@cache
|
|
61
|
+
def get_settings() -> Settings:
|
|
62
|
+
return Settings()
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
|
|
3
|
-
from skypro.config.settings import settings
|
|
4
3
|
from skypro.domain.errors import MissingWorkPriceError
|
|
5
4
|
from skypro.domain.models import WorkPrice, WorkReportItem, WorkSummary
|
|
6
5
|
|
|
@@ -32,6 +31,6 @@ def calculate_total(report: list[WorkReportItem]) -> Decimal:
|
|
|
32
31
|
return sum((item.total for item in report), Decimal(0))
|
|
33
32
|
|
|
34
33
|
|
|
35
|
-
def apply_tax(amount: Decimal) -> Decimal:
|
|
36
|
-
tax_rate = Decimal(str(
|
|
34
|
+
def apply_tax(amount: Decimal, tax_percent: float) -> Decimal:
|
|
35
|
+
tax_rate = Decimal(str(tax_percent)) / Decimal(100)
|
|
37
36
|
return amount * (Decimal(1) - tax_rate)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from datetime import date
|
|
3
3
|
|
|
4
|
-
from skypro.config
|
|
4
|
+
from skypro.config import get_settings
|
|
5
5
|
from skypro.infra.api.base import BaseApiClient
|
|
6
6
|
from skypro.infra.api.errors import AuthenticationError
|
|
7
7
|
from skypro.infra.http.client import HttpClient
|
|
@@ -52,8 +52,8 @@ class SkyProClient(BaseApiClient):
|
|
|
52
52
|
response = await self._post(
|
|
53
53
|
self.login_url,
|
|
54
54
|
data={
|
|
55
|
-
'email':
|
|
56
|
-
'password':
|
|
55
|
+
'email': get_settings().skypro.email,
|
|
56
|
+
'password': get_settings().skypro.password_value,
|
|
57
57
|
'csrfmiddlewaretoken': csrf_token,
|
|
58
58
|
},
|
|
59
59
|
headers={'Referer': str(self.cookies)},
|
|
@@ -9,5 +9,10 @@ class ServiceSummary(BaseModel, populate_by_name=True):
|
|
|
9
9
|
individual_consultation: int = Field(0, alias='ИК', ge=0)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class ServiceByProfession(BaseModel):
|
|
13
|
+
profession: str
|
|
14
|
+
services: ServiceSummary
|
|
15
|
+
|
|
16
|
+
|
|
12
17
|
class AccountDataResponse(BaseModel):
|
|
13
|
-
|
|
18
|
+
services_by_profession: list[ServiceByProfession]
|
|
@@ -5,7 +5,7 @@ from typing import Any, Self
|
|
|
5
5
|
import httpx
|
|
6
6
|
from httpx import AsyncClient, Request, Response
|
|
7
7
|
|
|
8
|
-
from skypro.config
|
|
8
|
+
from skypro.config import get_settings
|
|
9
9
|
|
|
10
10
|
from .errors import HttpError
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|
|
15
15
|
class HttpClient:
|
|
16
16
|
def __init__(self, timeout: int | None = None) -> None:
|
|
17
17
|
self._client = AsyncClient(
|
|
18
|
-
timeout=timeout or
|
|
18
|
+
timeout=timeout or get_settings().default_request_timeout,
|
|
19
19
|
follow_redirects=True,
|
|
20
20
|
event_hooks={'request': [_log_request], 'response': [_log_response]},
|
|
21
21
|
)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from datetime import date
|
|
4
|
+
from itertools import starmap
|
|
5
|
+
|
|
6
|
+
from pydantic import ValidationError
|
|
7
|
+
|
|
8
|
+
from skypro.domain.enums import WorkType
|
|
9
|
+
from skypro.domain.models import WorkSummary
|
|
10
|
+
from skypro.infra.api.errors import ApiError, AuthenticationError
|
|
11
|
+
from skypro.infra.api.skypro.client import SkyProClient
|
|
12
|
+
from skypro.infra.api.skypro.dto import ServiceByProfession, ServiceSummary
|
|
13
|
+
from skypro.infra.http.client import HttpClient
|
|
14
|
+
from skypro.infra.http.errors import HttpError
|
|
15
|
+
from skypro.infra.repositories.errors import SummaryLoadError
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def get_summary(start_date: date, end_date: date) -> list[WorkSummary]:
|
|
21
|
+
try:
|
|
22
|
+
async with HttpClient() as http_client:
|
|
23
|
+
skypro = SkyProClient(http_client)
|
|
24
|
+
account_data = await skypro.get_account_data(start_date, end_date)
|
|
25
|
+
except AuthenticationError as e:
|
|
26
|
+
message = 'SkyPro authentication failed. Check email and password.'
|
|
27
|
+
raise SummaryLoadError(message) from e
|
|
28
|
+
except (HttpError, ApiError, ValidationError) as e:
|
|
29
|
+
raise SummaryLoadError(str(e) or 'Failed to load summary from SkyPro.') from e
|
|
30
|
+
|
|
31
|
+
return _get_summary_by_service_by_profession(*account_data.services_by_profession)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_summary_by_service_by_profession(
|
|
35
|
+
*services_by_profession: ServiceByProfession,
|
|
36
|
+
) -> list[WorkSummary]:
|
|
37
|
+
work_summary: dict[WorkType, int] = defaultdict(int)
|
|
38
|
+
|
|
39
|
+
for sbp in services_by_profession:
|
|
40
|
+
logger.debug('Calculate work summary by %r', sbp.profession)
|
|
41
|
+
for summary in _get_summary_by_service_summary(sbp.services):
|
|
42
|
+
work_summary[summary.work_type] += summary.quantity
|
|
43
|
+
return list(starmap(WorkSummary, work_summary.items()))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _get_summary_by_service_summary(
|
|
47
|
+
service_summary: ServiceSummary,
|
|
48
|
+
) -> list[WorkSummary]:
|
|
49
|
+
return [
|
|
50
|
+
WorkSummary(WorkType.HOMEWORK, service_summary.homework),
|
|
51
|
+
WorkSummary(WorkType.COURSEWORK, service_summary.coursework),
|
|
52
|
+
WorkSummary(WorkType.DIPLOMA, service_summary.diploma),
|
|
53
|
+
WorkSummary(WorkType.LIVE, service_summary.live),
|
|
54
|
+
WorkSummary(WorkType.CONSULTATION, service_summary.individual_consultation),
|
|
55
|
+
]
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from asyncio import gather
|
|
3
|
-
from datetime import date
|
|
4
3
|
from decimal import Decimal
|
|
5
4
|
from typing import NamedTuple
|
|
6
5
|
|
|
6
|
+
from skypro.config import get_settings
|
|
7
7
|
from skypro.domain.models import WorkReportItem
|
|
8
8
|
from skypro.domain.services import apply_tax, build_work_report, calculate_total
|
|
9
9
|
from skypro.infra.repositories import get_summary, get_works_prices
|
|
10
|
+
from skypro.utils import get_month_range
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class SummaryInfo(NamedTuple):
|
|
@@ -15,7 +16,9 @@ class SummaryInfo(NamedTuple):
|
|
|
15
16
|
total_after_tax: Decimal
|
|
16
17
|
|
|
17
18
|
|
|
18
|
-
async def get_summary_info(
|
|
19
|
+
async def get_summary_info(year: int, month: int) -> SummaryInfo:
|
|
20
|
+
start_date, end_date = get_month_range(year, month)
|
|
21
|
+
|
|
19
22
|
work_prices, work_summaries = await gather(
|
|
20
23
|
asyncio.to_thread(get_works_prices),
|
|
21
24
|
get_summary(start_date, end_date),
|
|
@@ -23,6 +26,6 @@ async def get_summary_info(start_date: date, end_date: date) -> SummaryInfo:
|
|
|
23
26
|
|
|
24
27
|
report = build_work_report(work_prices, work_summaries)
|
|
25
28
|
total = calculate_total(report)
|
|
26
|
-
total_after_tax = apply_tax(total)
|
|
29
|
+
total_after_tax = apply_tax(total, get_settings().tax_percent)
|
|
27
30
|
|
|
28
31
|
return SummaryInfo(report, total, total_after_tax)
|
skypro_cli_tool-0.2.0/README.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# SkyPro cli tool
|
|
2
|
-
|
|
3
|
-
## How to start
|
|
4
|
-
|
|
5
|
-
Initialise app
|
|
6
|
-
|
|
7
|
-
```commandline
|
|
8
|
-
sky-cli init
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Check money you are earned this month
|
|
12
|
-
|
|
13
|
-
```commandline
|
|
14
|
-
sky-cli summary
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Another month or year
|
|
18
|
-
|
|
19
|
-
```commandline
|
|
20
|
-
sky-cli summary --month 3 --year 2026
|
|
21
|
-
```
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
|
|
3
|
-
import typer
|
|
4
|
-
from rich.console import Console
|
|
5
|
-
from typer import Option
|
|
6
|
-
|
|
7
|
-
from skypro.config.logging import enable_console_logging
|
|
8
|
-
|
|
9
|
-
from . import commands
|
|
10
|
-
from .commands import initialize_settings
|
|
11
|
-
|
|
12
|
-
app = typer.Typer()
|
|
13
|
-
console = Console()
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@app.command(name='summary')
|
|
17
|
-
def get_summary(
|
|
18
|
-
year: int | None = Option(None, help='Год для расчета'),
|
|
19
|
-
month: int | None = Option(None, help='Месяц для расчета'),
|
|
20
|
-
*,
|
|
21
|
-
verbose: bool = typer.Option(False, '-v', '--verbose'), # noqa: FBT003
|
|
22
|
-
) -> None:
|
|
23
|
-
"""Расчет стоимости работ"""
|
|
24
|
-
if verbose:
|
|
25
|
-
enable_console_logging(console)
|
|
26
|
-
asyncio.run(commands.show_summary(console, year, month))
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@app.command(name='init')
|
|
30
|
-
def init_settings() -> None:
|
|
31
|
-
"""Установка настроек."""
|
|
32
|
-
asyncio.run(initialize_settings(console))
|
|
33
|
-
console.print('[green]OK[/]')
|