skypro-cli-tool 0.1.1__tar.gz → 0.2.1__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.1.1 → skypro_cli_tool-0.2.1}/PKG-INFO +15 -7
- skypro_cli_tool-0.2.1/README.md +30 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/pyproject.toml +13 -1
- skypro_cli_tool-0.2.1/src/skypro/cli/app.py +55 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/cli/commands/summary.py +6 -14
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/cli/renders/summary_table.py +4 -5
- skypro_cli_tool-0.2.1/src/skypro/config/__init__.py +18 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/config/logging.py +5 -7
- skypro_cli_tool-0.2.1/src/skypro/config/service.py +44 -0
- skypro_cli_tool-0.2.1/src/skypro/config/settings.py +55 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/domain/services.py +2 -3
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/api/skypro/client.py +3 -3
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/http/client.py +2 -2
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/repositories/summary.py +5 -2
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/repositories/work.py +1 -1
- skypro_cli_tool-0.2.1/src/skypro/logs/skypro.log +83 -0
- skypro_cli_tool-0.2.1/src/skypro/logs/skypro.log.1 +101 -0
- skypro_cli_tool-0.2.1/src/skypro/logs/skypro.log.2 +109 -0
- skypro_cli_tool-0.2.1/src/skypro/logs/skypro.log.3 +108 -0
- skypro_cli_tool-0.2.1/src/skypro/logs/skypro.log.4 +101 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/main.py +1 -1
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/use_cases/get_summary_info.py +6 -3
- skypro_cli_tool-0.1.1/README.md +0 -22
- skypro_cli_tool-0.1.1/src/skypro/cli/app.py +0 -25
- skypro_cli_tool-0.1.1/src/skypro/config/settings.py +0 -29
- skypro_cli_tool-0.1.1/src/skypro/infra/http/__init__.py +0 -0
- skypro_cli_tool-0.1.1/src/skypro/logs/skypro.log +0 -11
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/cli/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/cli/commands/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/cli/renders/__init__.py +0 -0
- {skypro_cli_tool-0.1.1/src/skypro/config → skypro_cli_tool-0.2.1/src/skypro/domain}/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/domain/enums.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/domain/errors.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/domain/models.py +0 -0
- {skypro_cli_tool-0.1.1/src/skypro/domain → skypro_cli_tool-0.2.1/src/skypro/infra}/__init__.py +0 -0
- {skypro_cli_tool-0.1.1/src/skypro/infra → skypro_cli_tool-0.2.1/src/skypro/infra/api}/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/api/base.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/api/errors.py +0 -0
- {skypro_cli_tool-0.1.1/src/skypro/infra/api → skypro_cli_tool-0.2.1/src/skypro/infra/api/skypro}/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/api/skypro/dto.py +0 -0
- {skypro_cli_tool-0.1.1/src/skypro/infra/api/skypro → skypro_cli_tool-0.2.1/src/skypro/infra/http}/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/http/errors.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/repositories/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/infra/repositories/errors.py +0 -0
- /skypro_cli_tool-0.1.1/src/skypro/logs/skypro.log.1 → /skypro_cli_tool-0.2.1/src/skypro/logs/skypro.log.5 +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/use_cases/__init__.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/utils.py +0 -0
- {skypro_cli_tool-0.1.1 → skypro_cli_tool-0.2.1}/src/skypro/work_prices.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: skypro-cli-tool
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: SkyPro cli tool
|
|
5
5
|
Requires-Dist: aiohttp>=3.13.5
|
|
6
6
|
Requires-Dist: httpx>=0.28.1
|
|
@@ -14,21 +14,29 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
|
|
15
15
|
## How to start
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Show current config
|
|
18
18
|
|
|
19
19
|
```commandline
|
|
20
|
-
|
|
21
|
-
SKYPRO__PASSWORD=secret-password
|
|
20
|
+
sky-cli config
|
|
22
21
|
```
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
Reconfigure SkyPro access
|
|
25
24
|
|
|
26
25
|
```commandline
|
|
27
|
-
sky-cli
|
|
26
|
+
sky-cli config --skypro
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If SkyPro credentials are missing, the `summary` command exits with an error.
|
|
30
|
+
Configure them first with `sky-cli config --skypro`.
|
|
31
|
+
|
|
32
|
+
Check money you earned this month
|
|
33
|
+
|
|
34
|
+
```commandline
|
|
35
|
+
sky-cli summary
|
|
28
36
|
```
|
|
29
37
|
|
|
30
38
|
Another month or year
|
|
31
39
|
|
|
32
40
|
```commandline
|
|
33
|
-
sky-cli --month 3 --year 2026
|
|
41
|
+
sky-cli summary --month 3 --year 2026
|
|
34
42
|
```
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "skypro-cli-tool"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.1"
|
|
4
4
|
description = "SkyPro cli tool"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
@@ -13,6 +13,11 @@ dependencies = [
|
|
|
13
13
|
]
|
|
14
14
|
|
|
15
15
|
[dependency-groups]
|
|
16
|
+
dev = [
|
|
17
|
+
{ include-group = "test" },
|
|
18
|
+
{ include-group = "lint" },
|
|
19
|
+
"ipython>=9.13.0",
|
|
20
|
+
]
|
|
16
21
|
test = [
|
|
17
22
|
"freezegun>=1.5.5",
|
|
18
23
|
"pytest>=9.0.3",
|
|
@@ -59,8 +64,15 @@ preview = true
|
|
|
59
64
|
"ANN", # Missing type annotation
|
|
60
65
|
"S101", # Use of `assert` detected
|
|
61
66
|
"PLR6301", # Method could be a function, class method, or static method
|
|
67
|
+
"S105", # Possible hardcoded password
|
|
68
|
+
"S106", # Possible hardcoded password
|
|
62
69
|
"S311", # Standard pseudo-random generators are not suitable for cryptographic purposes
|
|
63
70
|
"PLR2004", # Magic value used in comparison
|
|
71
|
+
"RUF069", # Unreliable floating point equality comparison
|
|
72
|
+
]
|
|
73
|
+
"*/cli/app.py" = [
|
|
74
|
+
"FBT001", # Boolean-typed positional argument in function definition
|
|
75
|
+
"FBT003", # Boolean positional value in function call
|
|
64
76
|
]
|
|
65
77
|
|
|
66
78
|
[tool.ruff.lint.isort]
|
|
@@ -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
|
|
@@ -23,22 +23,21 @@ def render_summary_table(
|
|
|
23
23
|
table = Table(
|
|
24
24
|
Column('Тип', style='cyan'),
|
|
25
25
|
Column('Количество', style='magenta', justify='center'),
|
|
26
|
+
Column('Стоимость', style='magenta', justify='right'),
|
|
26
27
|
Column('Оплата', style='magenta', justify='right'),
|
|
27
28
|
title='Статистика работ за месяц',
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
for item in report:
|
|
31
|
-
if not item.quantity:
|
|
32
|
-
continue
|
|
33
|
-
|
|
34
32
|
table.add_row(
|
|
35
33
|
work_type_translate_map[item.work_type],
|
|
36
34
|
str(item.quantity),
|
|
37
35
|
format_price(item.price),
|
|
36
|
+
format_price(item.total),
|
|
38
37
|
)
|
|
39
38
|
|
|
40
39
|
table.add_section()
|
|
41
|
-
table.add_row('Итого', '', format_price(total))
|
|
42
|
-
table.add_row('После уплаты налога', '', format_price(total_after_tax))
|
|
40
|
+
table.add_row('Итого', '', '', format_price(total))
|
|
41
|
+
table.add_row('После уплаты налога', '', '', format_price(total_after_tax))
|
|
43
42
|
|
|
44
43
|
return table
|
|
@@ -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
|
+
)
|
|
@@ -5,9 +5,9 @@ from functools import cache
|
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
from rich.logging import RichHandler
|
|
7
7
|
|
|
8
|
-
from .settings import
|
|
8
|
+
from .settings import HOME_DIR, get_settings
|
|
9
9
|
|
|
10
|
-
LOG_DIR =
|
|
10
|
+
LOG_DIR = HOME_DIR / 'logs'
|
|
11
11
|
|
|
12
12
|
LOGGING_CONFIG = {
|
|
13
13
|
'version': 1,
|
|
@@ -27,10 +27,7 @@ LOGGING_CONFIG = {
|
|
|
27
27
|
'formatter': 'formatter',
|
|
28
28
|
},
|
|
29
29
|
},
|
|
30
|
-
'root': {
|
|
31
|
-
'handlers': ['file'],
|
|
32
|
-
'level': settings.log_level,
|
|
33
|
-
},
|
|
30
|
+
'root': {'handlers': ['file']},
|
|
34
31
|
'loggers': {
|
|
35
32
|
'httpx': {'handlers': ['file'], 'level': logging.WARNING, 'propagate': True},
|
|
36
33
|
},
|
|
@@ -39,7 +36,8 @@ LOGGING_CONFIG = {
|
|
|
39
36
|
|
|
40
37
|
@cache
|
|
41
38
|
def configure_logging() -> None:
|
|
42
|
-
LOG_DIR.mkdir(exist_ok=True)
|
|
39
|
+
LOG_DIR.mkdir(mode=0o700, exist_ok=True, parents=True)
|
|
40
|
+
LOGGING_CONFIG['root']['level'] = get_settings().log_level.upper()
|
|
43
41
|
logging.config.dictConfig(LOGGING_CONFIG)
|
|
44
42
|
|
|
45
43
|
|
|
@@ -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.get_secret_value():
|
|
25
|
+
password = current_settings.skypro.password.get_secret_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,55 @@
|
|
|
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
|
+
|
|
25
|
+
class Settings(BaseSettings, json_file=SETTINGS_FILE_PATH):
|
|
26
|
+
tax_percent: float = 6.0
|
|
27
|
+
default_request_timeout: int = 3
|
|
28
|
+
log_level: Literal['debug', 'info'] = 'info'
|
|
29
|
+
|
|
30
|
+
skypro: SkyProSettings = Field(default_factory=SkyProSettings)
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def settings_customise_sources(
|
|
34
|
+
cls,
|
|
35
|
+
settings_cls: type[BaseSettings],
|
|
36
|
+
**_: PydanticBaseSettingsSource,
|
|
37
|
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
|
38
|
+
return (JsonConfigSettingsSource(settings_cls),)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def save_settings(settings: Settings) -> None:
|
|
42
|
+
SETTINGS_FILE_PATH.parent.mkdir(mode=0o700, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
data = settings.model_dump(mode='json')
|
|
45
|
+
data['skypro']['password'] = settings.skypro.password.get_secret_value()
|
|
46
|
+
|
|
47
|
+
with SETTINGS_FILE_PATH.open(mode='w', encoding='utf-8') as f:
|
|
48
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
49
|
+
|
|
50
|
+
get_settings.cache_clear()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@cache
|
|
54
|
+
def get_settings() -> Settings:
|
|
55
|
+
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.get_secret_value(),
|
|
57
57
|
'csrfmiddlewaretoken': csrf_token,
|
|
58
58
|
},
|
|
59
59
|
headers={'Referer': str(self.cookies)},
|
|
@@ -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
|
)
|
|
@@ -4,7 +4,7 @@ from pydantic import ValidationError
|
|
|
4
4
|
|
|
5
5
|
from skypro.domain.enums import WorkType
|
|
6
6
|
from skypro.domain.models import WorkSummary
|
|
7
|
-
from skypro.infra.api.errors import ApiError
|
|
7
|
+
from skypro.infra.api.errors import ApiError, AuthenticationError
|
|
8
8
|
from skypro.infra.api.skypro.client import SkyProClient
|
|
9
9
|
from skypro.infra.http.client import HttpClient
|
|
10
10
|
from skypro.infra.http.errors import HttpError
|
|
@@ -16,8 +16,11 @@ async def get_summary(start_date: date, end_date: date) -> list[WorkSummary]:
|
|
|
16
16
|
async with HttpClient() as http_client:
|
|
17
17
|
skypro = SkyProClient(http_client)
|
|
18
18
|
account_data = await skypro.get_account_data(start_date, end_date)
|
|
19
|
+
except AuthenticationError as e:
|
|
20
|
+
message = 'SkyPro authentication failed. Check email and password.'
|
|
21
|
+
raise SummaryLoadError(message) from e
|
|
19
22
|
except (HttpError, ApiError, ValidationError) as e:
|
|
20
|
-
raise SummaryLoadError from e
|
|
23
|
+
raise SummaryLoadError(str(e) or 'Failed to load summary from SkyPro.') from e
|
|
21
24
|
|
|
22
25
|
return [
|
|
23
26
|
WorkSummary(WorkType.HOMEWORK, account_data.services_summary.homework),
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
2026-05-10 16:59:23 - INFO - SkyPro authentication successful
|
|
2
|
+
2026-05-10 16:59:23 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
|
|
3
|
+
2026-05-10 16:59:23 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
|
|
4
|
+
2026-05-10 16:59:23 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
|
|
5
|
+
2026-05-10 16:59:28 - INFO - Authenticating in SkyPro
|
|
6
|
+
2026-05-10 16:59:28 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
7
|
+
2026-05-10 16:59:28 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
8
|
+
2026-05-10 16:59:28 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
|
|
9
|
+
2026-05-10 16:59:28 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
|
|
10
|
+
2026-05-10 16:59:28 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
|
|
11
|
+
2026-05-10 16:59:29 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
|
|
12
|
+
2026-05-10 16:59:29 - INFO - SkyPro authentication successful
|
|
13
|
+
2026-05-10 16:59:29 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
|
|
14
|
+
2026-05-10 16:59:29 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
|
|
15
|
+
2026-05-10 16:59:29 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
|
|
16
|
+
2026-05-10 17:01:19 - INFO - Authenticating in SkyPro
|
|
17
|
+
2026-05-10 17:01:19 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
18
|
+
2026-05-10 17:01:20 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
19
|
+
2026-05-10 17:01:20 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
|
|
20
|
+
2026-05-10 17:01:20 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
|
|
21
|
+
2026-05-10 17:01:20 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
|
|
22
|
+
2026-05-10 17:01:20 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
|
|
23
|
+
2026-05-10 17:01:20 - INFO - SkyPro authentication successful
|
|
24
|
+
2026-05-10 17:01:20 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
|
|
25
|
+
2026-05-10 17:01:20 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
|
|
26
|
+
2026-05-10 17:01:21 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
|
|
27
|
+
2026-05-10 17:01:25 - INFO - Authenticating in SkyPro
|
|
28
|
+
2026-05-10 17:01:25 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
29
|
+
2026-05-10 17:01:25 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
30
|
+
2026-05-10 17:01:25 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
|
|
31
|
+
2026-05-10 17:01:25 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
|
|
32
|
+
2026-05-10 17:01:25 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
33
|
+
2026-05-10 17:01:25 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
34
|
+
2026-05-10 17:01:40 - INFO - Authenticating in SkyPro
|
|
35
|
+
2026-05-10 17:01:40 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
36
|
+
2026-05-10 17:01:41 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
37
|
+
2026-05-10 17:01:41 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
|
|
38
|
+
2026-05-10 17:01:41 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
|
|
39
|
+
2026-05-10 17:01:41 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
|
|
40
|
+
2026-05-10 17:01:41 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
|
|
41
|
+
2026-05-10 17:01:41 - INFO - SkyPro authentication successful
|
|
42
|
+
2026-05-10 17:01:41 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
|
|
43
|
+
2026-05-10 17:01:41 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
|
|
44
|
+
2026-05-10 17:01:42 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
|
|
45
|
+
2026-05-10 17:03:18 - INFO - Authenticating in SkyPro
|
|
46
|
+
2026-05-10 17:03:18 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
47
|
+
2026-05-10 17:03:18 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
48
|
+
2026-05-10 17:03:18 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
|
|
49
|
+
2026-05-10 17:03:19 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
|
|
50
|
+
2026-05-10 17:03:19 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
|
|
51
|
+
2026-05-10 17:03:19 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
|
|
52
|
+
2026-05-10 17:03:19 - INFO - SkyPro authentication successful
|
|
53
|
+
2026-05-10 17:03:19 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
|
|
54
|
+
2026-05-10 17:03:19 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
|
|
55
|
+
2026-05-10 17:03:19 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
|
|
56
|
+
2026-05-10 17:14:09 - INFO - Authenticating in SkyPro
|
|
57
|
+
2026-05-10 17:14:09 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
58
|
+
2026-05-10 17:14:10 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
59
|
+
2026-05-10 17:14:10 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
|
|
60
|
+
2026-05-10 17:14:10 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
|
|
61
|
+
2026-05-10 17:14:10 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
62
|
+
2026-05-10 17:14:10 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
63
|
+
2026-05-10 17:14:31 - INFO - Authenticating in SkyPro
|
|
64
|
+
2026-05-10 17:14:31 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
65
|
+
2026-05-10 17:14:32 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
66
|
+
2026-05-10 17:14:32 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
|
|
67
|
+
2026-05-10 17:14:33 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
|
|
68
|
+
2026-05-10 17:14:33 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
|
|
69
|
+
2026-05-10 17:14:33 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
|
|
70
|
+
2026-05-10 17:14:33 - INFO - SkyPro authentication successful
|
|
71
|
+
2026-05-10 17:14:33 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
|
|
72
|
+
2026-05-10 17:14:33 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
|
|
73
|
+
2026-05-10 17:14:33 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
|
|
74
|
+
2026-05-10 17:20:00 - INFO - Authenticating in SkyPro
|
|
75
|
+
2026-05-10 17:20:00 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
76
|
+
2026-05-10 17:38:44 - INFO - Authenticating in SkyPro
|
|
77
|
+
2026-05-10 17:38:44 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
|
|
78
|
+
2026-05-10 17:38:45 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
|
|
79
|
+
2026-05-10 17:38:45 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
|
|
80
|
+
2026-05-10 17:38:45 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
|
|
81
|
+
2026-05-10 17:38:45 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
|
|
82
|
+
2026-05-10 17:38:45 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
|
|
83
|
+
2026-05-10 17:38:45 - INFO - SkyPro authentication successful
|