skypro-cli-tool 0.1.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.
skypro/__init__.py ADDED
File without changes
skypro/cli/__init__.py ADDED
File without changes
skypro/cli/app.py ADDED
@@ -0,0 +1,25 @@
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
+
11
+ app = typer.Typer()
12
+ console = Console()
13
+
14
+
15
+ @app.command(name='summary')
16
+ def get_summary(
17
+ year: int | None = Option(None, help='Год для расчета'),
18
+ month: int | None = Option(None, help='Месяц для расчета'),
19
+ *,
20
+ verbose: bool = typer.Option(False, '-v', '--verbose'), # noqa: FBT003
21
+ ) -> None:
22
+ """Расчет стоимости работ"""
23
+ if verbose:
24
+ enable_console_logging(console)
25
+ asyncio.run(commands.show_summary(console, year, month))
@@ -0,0 +1,3 @@
1
+ from .summary import show_summary
2
+
3
+ __all__ = ('show_summary',)
@@ -0,0 +1,38 @@
1
+ from calendar import IllegalMonthError
2
+ from datetime import UTC, date, datetime
3
+
4
+ from click.exceptions import Exit
5
+ from rich.console import Console
6
+
7
+ from skypro.cli.renders import render_summary_table
8
+ from skypro.domain.errors import DomainError
9
+ from skypro.infra.repositories.errors import RepositoryError
10
+ from skypro.use_cases import get_summary_info
11
+ from skypro.utils import get_month_range
12
+
13
+
14
+ async def show_summary(
15
+ console: Console,
16
+ year: int | None,
17
+ month: int | None,
18
+ ) -> None:
19
+ try:
20
+ start, end = resolve_period(year, month)
21
+
22
+ with console.status('[cyan]Расчет стоимости работ'):
23
+ report, total, after_tax = await get_summary_info(start, end)
24
+
25
+ table = render_summary_table(report, total, after_tax)
26
+ console.print(table)
27
+ except (RepositoryError, DomainError, IllegalMonthError) as e:
28
+ console.print(f'[bold red]Error: {e!s}[/]')
29
+ 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,3 @@
1
+ from .summary_table import render_summary_table
2
+
3
+ __all__ = ('render_summary_table',)
@@ -0,0 +1,44 @@
1
+ from decimal import Decimal
2
+
3
+ from rich.table import Column, Table
4
+
5
+ from skypro.domain.enums import WorkType
6
+ from skypro.domain.models import WorkReportItem
7
+ from skypro.utils import format_price
8
+
9
+ work_type_translate_map = {
10
+ WorkType.HOMEWORK: 'Домашние работы',
11
+ WorkType.CONSULTATION: 'Индивидуальные консультации',
12
+ WorkType.LIVE: 'Групповые занятия',
13
+ WorkType.DIPLOMA: 'Дипломные работы',
14
+ WorkType.COURSEWORK: 'Курсовые работы',
15
+ }
16
+
17
+
18
+ def render_summary_table(
19
+ report: list[WorkReportItem],
20
+ total: Decimal,
21
+ total_after_tax: Decimal,
22
+ ) -> Table:
23
+ table = Table(
24
+ Column('Тип', style='cyan'),
25
+ Column('Количество', style='magenta', justify='center'),
26
+ Column('Оплата', style='magenta', justify='right'),
27
+ title='Статистика работ за месяц',
28
+ )
29
+
30
+ for item in report:
31
+ if not item.quantity:
32
+ continue
33
+
34
+ table.add_row(
35
+ work_type_translate_map[item.work_type],
36
+ str(item.quantity),
37
+ format_price(item.price),
38
+ )
39
+
40
+ table.add_section()
41
+ table.add_row('Итого', '', format_price(total))
42
+ table.add_row('После уплаты налога', '', format_price(total_after_tax))
43
+
44
+ return table
File without changes
@@ -0,0 +1,51 @@
1
+ import logging
2
+ import logging.config
3
+ from functools import cache
4
+
5
+ from rich.console import Console
6
+ from rich.logging import RichHandler
7
+
8
+ from .settings import BASE_DIR, settings
9
+
10
+ LOG_DIR = BASE_DIR / 'logs'
11
+
12
+ LOGGING_CONFIG = {
13
+ 'version': 1,
14
+ 'disable_existing_loggers': False,
15
+ 'formatters': {
16
+ 'formatter': {
17
+ 'format': '%(asctime)s - %(levelname)s - %(message)s',
18
+ 'datefmt': '%Y-%m-%d %H:%M:%S',
19
+ },
20
+ },
21
+ 'handlers': {
22
+ 'file': {
23
+ 'class': 'logging.handlers.RotatingFileHandler',
24
+ 'filename': LOG_DIR / 'skypro.log',
25
+ 'maxBytes': 10 * 1024, # 10Mb
26
+ 'backupCount': 5,
27
+ 'formatter': 'formatter',
28
+ },
29
+ },
30
+ 'root': {
31
+ 'handlers': ['file'],
32
+ 'level': settings.log_level,
33
+ },
34
+ 'loggers': {
35
+ 'httpx': {'handlers': ['file'], 'level': logging.WARNING, 'propagate': True},
36
+ },
37
+ }
38
+
39
+
40
+ @cache
41
+ def configure_logging() -> None:
42
+ LOG_DIR.mkdir(exist_ok=True)
43
+ logging.config.dictConfig(LOGGING_CONFIG)
44
+
45
+
46
+ def enable_console_logging(console: Console) -> None:
47
+ root_logger = logging.getLogger()
48
+ console_handler = RichHandler(
49
+ console=console, rich_tracebacks=True, show_time=False
50
+ )
51
+ root_logger.addHandler(console_handler)
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, Field
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+
7
+ BASE_DIR = Path(__file__).resolve().parent.parent
8
+
9
+
10
+ class SkyProSettings(BaseModel):
11
+ email: str
12
+ password: str
13
+
14
+
15
+ class Settings(BaseSettings):
16
+ tax_percent: float = 6.0
17
+ default_request_timeout: int = 3
18
+ log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = 'INFO'
19
+
20
+ skypro: SkyProSettings = Field(default_factory=SkyProSettings)
21
+
22
+ model_config = SettingsConfigDict(
23
+ env_nested_delimiter='__',
24
+ env_file='.env',
25
+ extra='ignore',
26
+ )
27
+
28
+
29
+ settings = Settings()
File without changes
skypro/domain/enums.py ADDED
@@ -0,0 +1,9 @@
1
+ from enum import StrEnum
2
+
3
+
4
+ class WorkType(StrEnum):
5
+ HOMEWORK = 'homework'
6
+ COURSEWORK = 'coursework'
7
+ DIPLOMA = 'diploma'
8
+ LIVE = 'live'
9
+ CONSULTATION = 'consultation'
@@ -0,0 +1,10 @@
1
+ class DomainError(Exception): ...
2
+
3
+
4
+ class InvalidPriceError(DomainError): ...
5
+
6
+
7
+ class InvalidQuantityError(DomainError): ...
8
+
9
+
10
+ class MissingWorkPriceError(DomainError): ...
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+ from decimal import Decimal
3
+
4
+ from .enums import WorkType
5
+ from .errors import InvalidPriceError, InvalidQuantityError
6
+
7
+
8
+ @dataclass(slots=True, frozen=True)
9
+ class WorkPrice:
10
+ work_type: WorkType
11
+ price: Decimal
12
+
13
+ def __post_init__(self) -> None:
14
+ if self.price <= Decimal(0):
15
+ raise InvalidPriceError
16
+
17
+
18
+ @dataclass(slots=True, frozen=True)
19
+ class WorkSummary:
20
+ work_type: WorkType
21
+ quantity: int
22
+
23
+ def __post_init__(self) -> None:
24
+ if self.quantity < 0:
25
+ raise InvalidQuantityError
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class WorkReportItem:
30
+ work_type: WorkType
31
+ quantity: int
32
+ price: Decimal
33
+ total: Decimal
@@ -0,0 +1,37 @@
1
+ from decimal import Decimal
2
+
3
+ from skypro.config.settings import settings
4
+ from skypro.domain.errors import MissingWorkPriceError
5
+ from skypro.domain.models import WorkPrice, WorkReportItem, WorkSummary
6
+
7
+
8
+ def build_work_report(
9
+ prices: list[WorkPrice], summaries: list[WorkSummary]
10
+ ) -> list[WorkReportItem]:
11
+ price_map = {work_price.work_type: work_price.price for work_price in prices}
12
+
13
+ work_reports = []
14
+ for summary in summaries:
15
+ work_price = price_map.get(summary.work_type)
16
+ if work_price is None:
17
+ raise MissingWorkPriceError
18
+
19
+ work_reports.append(
20
+ WorkReportItem(
21
+ work_type=summary.work_type,
22
+ quantity=summary.quantity,
23
+ price=work_price,
24
+ total=work_price * summary.quantity,
25
+ )
26
+ )
27
+
28
+ return work_reports
29
+
30
+
31
+ def calculate_total(report: list[WorkReportItem]) -> Decimal:
32
+ return sum((item.total for item in report), Decimal(0))
33
+
34
+
35
+ def apply_tax(amount: Decimal) -> Decimal:
36
+ tax_rate = Decimal(str(settings.tax_percent)) / Decimal(100)
37
+ return amount * (Decimal(1) - tax_rate)
File without changes
File without changes
@@ -0,0 +1,31 @@
1
+ from typing import Any
2
+ from urllib.parse import urljoin
3
+
4
+ from httpx import Cookies, Response
5
+
6
+ from skypro.infra.http.client import HttpClient
7
+
8
+
9
+ class BaseApiClient:
10
+ base_url: str
11
+
12
+ def __init__(self, http_client: HttpClient) -> None:
13
+ self._http = http_client
14
+
15
+ @property
16
+ def cookies(self) -> Cookies:
17
+ return self._http.client.cookies
18
+
19
+ async def _get(self, url: str, **kwargs: Any) -> Response:
20
+ return await self._request('GET', url, **kwargs)
21
+
22
+ async def _post(self, url: str, **kwargs: Any) -> Response:
23
+ return await self._request('POST', url, **kwargs)
24
+
25
+ async def _request(self, method: str, path: str, **kwargs: Any) -> Response:
26
+ return await self._http.request(
27
+ method=method, url=self._build_url(path), **kwargs
28
+ )
29
+
30
+ def _build_url(self, path: str) -> str:
31
+ return urljoin(self.base_url, path)
@@ -0,0 +1,4 @@
1
+ class ApiError(Exception): ...
2
+
3
+
4
+ class AuthenticationError(ApiError): ...
File without changes
@@ -0,0 +1,78 @@
1
+ import logging
2
+ from datetime import date
3
+
4
+ from skypro.config.settings import settings
5
+ from skypro.infra.api.base import BaseApiClient
6
+ from skypro.infra.api.errors import AuthenticationError
7
+ from skypro.infra.http.client import HttpClient
8
+
9
+ from .dto import AccountDataResponse
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class SkyProClient(BaseApiClient):
15
+ base_url = 'https://operation-planning.sky.pro'
16
+ login_url = '/careusers/login/'
17
+
18
+ def __init__(self, http_client: HttpClient) -> None:
19
+ super().__init__(http_client)
20
+ self._is_authenticated = False
21
+
22
+ @property
23
+ def is_authenticated(self) -> bool:
24
+ return self._is_authenticated
25
+
26
+ async def get_account_data(
27
+ self, start_date: date, end_date: date
28
+ ) -> AccountDataResponse:
29
+ await self.login()
30
+
31
+ logger.info('Fetching account data from %s to %s', start_date, end_date)
32
+
33
+ response = await self._get(
34
+ '/mentor-cabinet/api/data/',
35
+ params={
36
+ 'start_date': start_date.strftime('%Y-%m-%d'),
37
+ 'end_date': end_date.strftime('%Y-%m-%d'),
38
+ },
39
+ )
40
+
41
+ return AccountDataResponse.model_validate(response.json())
42
+
43
+ async def login(self) -> None:
44
+ if self._is_authenticated:
45
+ logger.debug('SkyPro already authenticated')
46
+ return
47
+
48
+ logger.info('Authenticating in SkyPro')
49
+
50
+ csrf_token = await self._get_csrf_token()
51
+
52
+ response = await self._post(
53
+ self.login_url,
54
+ data={
55
+ 'email': settings.skypro.email,
56
+ 'password': settings.skypro.password,
57
+ 'csrfmiddlewaretoken': csrf_token,
58
+ },
59
+ headers={'Referer': str(self.cookies)},
60
+ )
61
+
62
+ if response.url.path == self.login_url:
63
+ raise AuthenticationError('Authentication failed')
64
+
65
+ self._is_authenticated = True
66
+
67
+ logger.info('SkyPro authentication successful')
68
+
69
+ async def _get_csrf_token(self) -> str:
70
+ logger.debug('Fetching csrf token')
71
+
72
+ await self._get(self.login_url)
73
+
74
+ csrf_token = self.cookies.get('csrftoken')
75
+ if csrf_token is None:
76
+ raise AuthenticationError('CSRF token not found')
77
+
78
+ return csrf_token
@@ -0,0 +1,13 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class ServiceSummary(BaseModel, populate_by_name=True):
5
+ homework: int = Field(0, alias='ДЗ', ge=0)
6
+ coursework: int = Field(0, alias='КР', ge=0)
7
+ diploma: int = Field(0, alias='ДР', ge=0)
8
+ live: int = Field(0, alias='Лайв', ge=0)
9
+ individual_consultation: int = Field(0, alias='ИК', ge=0)
10
+
11
+
12
+ class AccountDataResponse(BaseModel):
13
+ services_summary: ServiceSummary
File without changes
@@ -0,0 +1,64 @@
1
+ import logging
2
+ from types import TracebackType
3
+ from typing import Any, Self
4
+
5
+ import httpx
6
+ from httpx import AsyncClient, Request, Response
7
+
8
+ from skypro.config.settings import settings
9
+
10
+ from .errors import HttpError
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class HttpClient:
16
+ def __init__(self, timeout: int | None = None) -> None:
17
+ self._client = AsyncClient(
18
+ timeout=timeout or settings.default_request_timeout,
19
+ follow_redirects=True,
20
+ event_hooks={'request': [_log_request], 'response': [_log_response]},
21
+ )
22
+
23
+ async def __aenter__(self) -> Self:
24
+ return self
25
+
26
+ async def __aexit__(
27
+ self,
28
+ exc_type: type[BaseException] | None,
29
+ exc_val: BaseException | None,
30
+ exc_tb: TracebackType | None,
31
+ ) -> None:
32
+ await self._client.aclose()
33
+
34
+ @property
35
+ def client(self) -> AsyncClient:
36
+ return self._client
37
+
38
+ async def request(self, method: str, url: str, **kwargs: Any) -> Response:
39
+ try:
40
+ response = await self._client.request(method, url, **kwargs)
41
+ response.raise_for_status()
42
+ except httpx.HTTPStatusError as e:
43
+ raise HttpError(f'HTTP {e.response.status_code}: {method} {url}') from e
44
+ except httpx.HTTPError as e:
45
+ raise HttpError(f'Network error: {method} {url}') from e
46
+ else:
47
+ return response
48
+
49
+
50
+ async def _log_request(request: Request) -> None: # noqa: RUF029
51
+ logger.info(
52
+ 'HTTP request: %s %s',
53
+ request.method,
54
+ request.url,
55
+ )
56
+
57
+
58
+ async def _log_response(response: Response) -> None: # noqa: RUF029
59
+ logger.info(
60
+ 'HTTP response: %s %s -> %s',
61
+ response.request.method,
62
+ response.request.url,
63
+ response.status_code,
64
+ )
@@ -0,0 +1 @@
1
+ class HttpError(Exception): ...
@@ -0,0 +1,4 @@
1
+ from .summary import get_summary
2
+ from .work import get_works_prices
3
+
4
+ __all__ = ('get_summary', 'get_works_prices')
@@ -0,0 +1,7 @@
1
+ class RepositoryError(Exception): ...
2
+
3
+
4
+ class PriceLoadError(RepositoryError): ...
5
+
6
+
7
+ class SummaryLoadError(RepositoryError): ...
@@ -0,0 +1,30 @@
1
+ from datetime import date
2
+
3
+ from pydantic import ValidationError
4
+
5
+ from skypro.domain.enums import WorkType
6
+ from skypro.domain.models import WorkSummary
7
+ from skypro.infra.api.errors import ApiError
8
+ from skypro.infra.api.skypro.client import SkyProClient
9
+ from skypro.infra.http.client import HttpClient
10
+ from skypro.infra.http.errors import HttpError
11
+ from skypro.infra.repositories.errors import SummaryLoadError
12
+
13
+
14
+ async def get_summary(start_date: date, end_date: date) -> list[WorkSummary]:
15
+ try:
16
+ async with HttpClient() as http_client:
17
+ skypro = SkyProClient(http_client)
18
+ account_data = await skypro.get_account_data(start_date, end_date)
19
+ except (HttpError, ApiError, ValidationError) as e:
20
+ raise SummaryLoadError from e
21
+
22
+ return [
23
+ WorkSummary(WorkType.HOMEWORK, account_data.services_summary.homework),
24
+ WorkSummary(WorkType.COURSEWORK, account_data.services_summary.coursework),
25
+ WorkSummary(WorkType.DIPLOMA, account_data.services_summary.diploma),
26
+ WorkSummary(WorkType.LIVE, account_data.services_summary.live),
27
+ WorkSummary(
28
+ WorkType.CONSULTATION, account_data.services_summary.individual_consultation
29
+ ),
30
+ ]
@@ -0,0 +1,27 @@
1
+ import json
2
+ from decimal import Decimal, InvalidOperation
3
+
4
+ from skypro.config.settings import BASE_DIR
5
+ from skypro.domain.enums import WorkType
6
+ from skypro.domain.models import WorkPrice
7
+
8
+ from .errors import PriceLoadError
9
+
10
+
11
+ def get_works_prices() -> list[WorkPrice]:
12
+ try:
13
+ with (BASE_DIR / 'work_prices.json').open() as f:
14
+ data = json.load(f)
15
+ except (FileNotFoundError, json.JSONDecodeError) as e:
16
+ raise PriceLoadError from e
17
+
18
+ if not isinstance(data, dict):
19
+ raise PriceLoadError
20
+
21
+ try:
22
+ return [
23
+ WorkPrice(work_type=WorkType(work_type), price=Decimal(str(price)))
24
+ for work_type, price in data.items()
25
+ ]
26
+ except (ValueError, InvalidOperation) as e:
27
+ raise PriceLoadError from e
skypro/logs/skypro.log ADDED
@@ -0,0 +1,11 @@
1
+ 2026-05-04 23:05:39 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
2
+ 2026-05-04 23:05:39 - WARNING - Executing <Task pending name='Task-3' coro=<get_summary() running at /Users/vadim/PycharmProjects/skypro-money/src/skypro/infra/repositories/summary.py:18> cb=[gather.<locals>._done_callback() at /Users/vadim/.pyenv/versions/3.13.2/lib/python3.13/asyncio/tasks.py:820] created at /Users/vadim/.pyenv/versions/3.13.2/lib/python3.13/asyncio/tasks.py:748> took 0.234 seconds
3
+ 2026-05-04 23:05:39 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
4
+ 2026-05-04 23:05:39 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
5
+ 2026-05-04 23:05:40 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
6
+ 2026-05-04 23:05:40 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
7
+ 2026-05-04 23:05:40 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
8
+ 2026-05-04 23:05:40 - INFO - SkyPro authentication successful
9
+ 2026-05-04 23:05:40 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
10
+ 2026-05-04 23:05:40 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
11
+ 2026-05-04 23:05:40 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
@@ -0,0 +1,100 @@
1
+ 2026-05-04 21:51:43 - INFO - Authenticating in SkyPro
2
+ 2026-05-04 21:51:43 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
3
+ 2026-05-04 21:51:43 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
4
+ 2026-05-04 21:51:43 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
5
+ 2026-05-04 21:51:44 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
6
+ 2026-05-04 21:51:44 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
7
+ 2026-05-04 21:51:44 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
8
+ 2026-05-04 21:51:44 - INFO - SkyPro authentication successful
9
+ 2026-05-04 21:51:44 - INFO - Fetching account data from 2026-05-01 to 2026-05-31
10
+ 2026-05-04 21:51:44 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31
11
+ 2026-05-04 21:51:44 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31 -> 200
12
+ 2026-05-04 22:01:00 - INFO - Authenticating in SkyPro
13
+ 2026-05-04 22:01:00 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
14
+ 2026-05-04 22:01:00 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
15
+ 2026-05-04 22:01:00 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
16
+ 2026-05-04 22:01:01 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
17
+ 2026-05-04 22:01:01 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
18
+ 2026-05-04 22:01:01 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
19
+ 2026-05-04 22:01:01 - INFO - SkyPro authentication successful
20
+ 2026-05-04 22:01:01 - INFO - Fetching account data from 2026-05-01 to 2026-05-31
21
+ 2026-05-04 22:01:01 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31
22
+ 2026-05-04 22:01:01 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31 -> 200
23
+ 2026-05-04 22:05:36 - INFO - Authenticating in SkyPro
24
+ 2026-05-04 22:05:36 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
25
+ 2026-05-04 22:05:37 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
26
+ 2026-05-04 22:05:37 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
27
+ 2026-05-04 22:05:37 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
28
+ 2026-05-04 22:05:37 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
29
+ 2026-05-04 22:05:38 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
30
+ 2026-05-04 22:05:38 - INFO - SkyPro authentication successful
31
+ 2026-05-04 22:05:38 - INFO - Fetching account data from 2026-05-01 to 2026-05-31
32
+ 2026-05-04 22:05:38 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31
33
+ 2026-05-04 22:05:38 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31 -> 200
34
+ 2026-05-04 22:05:52 - INFO - Authenticating in SkyPro
35
+ 2026-05-04 22:05:52 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
36
+ 2026-05-04 22:05:53 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
37
+ 2026-05-04 22:05:53 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
38
+ 2026-05-04 22:05:53 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
39
+ 2026-05-04 22:05:53 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
40
+ 2026-05-04 22:05:53 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
41
+ 2026-05-04 22:05:53 - INFO - SkyPro authentication successful
42
+ 2026-05-04 22:05:53 - INFO - Fetching account data from 2026-05-01 to 2026-05-31
43
+ 2026-05-04 22:05:53 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31
44
+ 2026-05-04 22:05:54 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31 -> 200
45
+ 2026-05-04 22:24:16 - INFO - Authenticating in SkyPro
46
+ 2026-05-04 22:24:16 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
47
+ 2026-05-04 22:24:16 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
48
+ 2026-05-04 22:24:16 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
49
+ 2026-05-04 22:24:16 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
50
+ 2026-05-04 22:24:16 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
51
+ 2026-05-04 22:24:16 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
52
+ 2026-05-04 22:24:16 - INFO - SkyPro authentication successful
53
+ 2026-05-04 22:24:16 - INFO - Fetching account data from 2026-05-01 to 2026-05-31
54
+ 2026-05-04 22:24:16 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31
55
+ 2026-05-04 22:24:16 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-05-01&end_date=2026-05-31 -> 200
56
+ 2026-05-04 22:31:14 - INFO - Authenticating in SkyPro
57
+ 2026-05-04 22:31:14 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
58
+ 2026-05-04 22:31:14 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
59
+ 2026-05-04 22:31:14 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
60
+ 2026-05-04 22:31:14 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
61
+ 2026-05-04 22:31:14 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
62
+ 2026-05-04 22:31:14 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
63
+ 2026-05-04 22:31:14 - INFO - SkyPro authentication successful
64
+ 2026-05-04 22:31:14 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
65
+ 2026-05-04 22:31:14 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
66
+ 2026-05-04 22:31:14 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
67
+ 2026-05-04 22:31:17 - INFO - Authenticating in SkyPro
68
+ 2026-05-04 22:31:17 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
69
+ 2026-05-04 22:31:17 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
70
+ 2026-05-04 22:31:17 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
71
+ 2026-05-04 22:31:17 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
72
+ 2026-05-04 22:31:17 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
73
+ 2026-05-04 22:31:17 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
74
+ 2026-05-04 22:31:17 - INFO - SkyPro authentication successful
75
+ 2026-05-04 22:31:17 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
76
+ 2026-05-04 22:31:17 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
77
+ 2026-05-04 22:31:18 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
78
+ 2026-05-04 22:33:28 - INFO - Authenticating in SkyPro
79
+ 2026-05-04 22:33:28 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
80
+ 2026-05-04 22:33:29 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
81
+ 2026-05-04 22:33:29 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
82
+ 2026-05-04 22:33:29 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
83
+ 2026-05-04 22:33:29 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
84
+ 2026-05-04 22:33:29 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
85
+ 2026-05-04 22:33:29 - INFO - SkyPro authentication successful
86
+ 2026-05-04 22:33:29 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
87
+ 2026-05-04 22:33:29 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
88
+ 2026-05-04 22:33: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
89
+ 2026-05-04 22:33:41 - INFO - Authenticating in SkyPro
90
+ 2026-05-04 22:33:41 - INFO - HTTP request: GET https://operation-planning.sky.pro/careusers/login/
91
+ 2026-05-04 22:33:41 - INFO - HTTP response: GET https://operation-planning.sky.pro/careusers/login/ -> 200
92
+ 2026-05-04 22:33:41 - INFO - HTTP request: POST https://operation-planning.sky.pro/careusers/login/
93
+ 2026-05-04 22:33:41 - INFO - HTTP response: POST https://operation-planning.sky.pro/careusers/login/ -> 302
94
+ 2026-05-04 22:33:41 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/
95
+ 2026-05-04 22:33:41 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/ -> 200
96
+ 2026-05-04 22:33:41 - INFO - SkyPro authentication successful
97
+ 2026-05-04 22:33:41 - INFO - Fetching account data from 2026-04-01 to 2026-04-30
98
+ 2026-05-04 22:33:41 - INFO - HTTP request: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30
99
+ 2026-05-04 22:33:41 - INFO - HTTP response: GET https://operation-planning.sky.pro/mentor-cabinet/api/data/?start_date=2026-04-01&end_date=2026-04-30 -> 200
100
+ 2026-05-04 23:05:39 - INFO - Authenticating in SkyPro
skypro/main.py ADDED
@@ -0,0 +1,14 @@
1
+ import locale
2
+
3
+ from skypro.cli.app import app
4
+ from skypro.config.logging import configure_logging
5
+
6
+
7
+ def main() -> None:
8
+ locale.setlocale(locale.LC_ALL, 'ru_RU.UTF-8')
9
+ configure_logging()
10
+ app()
11
+
12
+
13
+ if __name__ == '__main__':
14
+ main()
@@ -0,0 +1,3 @@
1
+ from .get_summary_info import SummaryInfo, get_summary_info
2
+
3
+ __all__ = ('SummaryInfo', 'get_summary_info')
@@ -0,0 +1,28 @@
1
+ import asyncio
2
+ from asyncio import gather
3
+ from datetime import date
4
+ from decimal import Decimal
5
+ from typing import NamedTuple
6
+
7
+ from skypro.domain.models import WorkReportItem
8
+ from skypro.domain.services import apply_tax, build_work_report, calculate_total
9
+ from skypro.infra.repositories import get_summary, get_works_prices
10
+
11
+
12
+ class SummaryInfo(NamedTuple):
13
+ report: list[WorkReportItem]
14
+ total: Decimal
15
+ total_after_tax: Decimal
16
+
17
+
18
+ async def get_summary_info(start_date: date, end_date: date) -> SummaryInfo:
19
+ work_prices, work_summaries = await gather(
20
+ asyncio.to_thread(get_works_prices),
21
+ get_summary(start_date, end_date),
22
+ )
23
+
24
+ report = build_work_report(work_prices, work_summaries)
25
+ total = calculate_total(report)
26
+ total_after_tax = apply_tax(total)
27
+
28
+ return SummaryInfo(report, total, total_after_tax)
skypro/utils.py ADDED
@@ -0,0 +1,14 @@
1
+ import locale
2
+ from calendar import monthrange
3
+ from datetime import date
4
+ from decimal import Decimal
5
+
6
+
7
+ def get_month_range(year: int, month: int) -> tuple[date, date]:
8
+ first_day = 1
9
+ _, last_day = monthrange(year, month)
10
+ return date(year, month, first_day), date(year, month, last_day)
11
+
12
+
13
+ def format_price(price: Decimal) -> str:
14
+ return locale.currency(price, grouping=True)
@@ -0,0 +1,7 @@
1
+ {
2
+ "homework": 319,
3
+ "coursework": 1064,
4
+ "diploma": 1596,
5
+ "live": 1596,
6
+ "consultation": 1064
7
+ }
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.3
2
+ Name: skypro-cli-tool
3
+ Version: 0.1.0
4
+ Summary: SkyPro cli tool
5
+ Requires-Dist: aiohttp>=3.13.5
6
+ Requires-Dist: httpx>=0.28.1
7
+ Requires-Dist: pydantic>=2.13.3
8
+ Requires-Dist: pydantic-settings>=2.14.0
9
+ Requires-Dist: typer>=0.25.0
10
+ Requires-Python: >=3.13
11
+ Description-Content-Type: text/markdown
12
+
13
+ # SkyPro cli tool
@@ -0,0 +1,40 @@
1
+ skypro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ skypro/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ skypro/cli/app.py,sha256=YJ94Ciz9WCy4G335OkIafT1YfE_YYlDtuQY5L8z75qo,675
4
+ skypro/cli/commands/__init__.py,sha256=5uOc3rgt5JD878OVJ60_wdKMoBgFxaK4EdFNDNcKIAI,63
5
+ skypro/cli/commands/summary.py,sha256=BYtWAsG65eTlD_Cn5erUYE3c8hWrq24KIVOZN8E2i9o,1235
6
+ skypro/cli/renders/__init__.py,sha256=bPVaU1BSAIG7yWF4EQRLU6WATLBQceiSFnB2Cnt-ahM,85
7
+ skypro/cli/renders/summary_table.py,sha256=WbAY_v12ilUOLGCH9ZHeuE7lw9TzS0SboHGuUqOpzU0,1357
8
+ skypro/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ skypro/config/logging.py,sha256=KCW9MBgOPj-lqJjrBhzo0MFr02TwT4vdfdyq_bL3gnY,1293
10
+ skypro/config/settings.py,sha256=8XHSWJFsn-ZFH2XKvXVkZPOIErtC8JL6X58PyPyvodo,674
11
+ skypro/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ skypro/domain/enums.py,sha256=oTu6G_M6bKhl7pgFGmeHrvPiXNcVUla6XJxqNP-slNA,184
13
+ skypro/domain/errors.py,sha256=6bOlsrDIyTPClcUWdlPBvlTLU4RxpE1_S4yWrYSZszc,173
14
+ skypro/domain/models.py,sha256=XiL3Y24jTb5xoYVILjb5EpK0cOSD5GF2LqDUo7h5rf4,694
15
+ skypro/domain/services.py,sha256=E_VgTfIbrh6MwMoxhzhVRhPdRet7b1W5m93SsFF6aXk,1130
16
+ skypro/infra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ skypro/infra/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ skypro/infra/api/base.py,sha256=o99NE2SpPA6-R1HoT9uO2p1tf__jSl3qOfG8Al4PP78,893
19
+ skypro/infra/api/errors.py,sha256=VSNjNdTKrobsdOFnGQnWtEv6ufASdIrYz5eos6AiLY0,74
20
+ skypro/infra/api/skypro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ skypro/infra/api/skypro/client.py,sha256=v6PF_ZY5hqg3k8wmOdvb06AV19dP3HyFHwTzLkrT-bQ,2272
22
+ skypro/infra/api/skypro/dto.py,sha256=1N-VlAWTEkw9JkAYmh4TAH6LTSq_yhInOfTE6K9Jdu4,434
23
+ skypro/infra/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ skypro/infra/http/client.py,sha256=5kRXN3ezLsHoYpbWG6-zIbB3AvZAqCKyKm9m0az6FlY,1790
25
+ skypro/infra/http/errors.py,sha256=dNFAzMYmi5nGymB1iiBz3Q-H8syHmeMbGnsgAM23dEk,32
26
+ skypro/infra/repositories/__init__.py,sha256=SOsWY5frUBTFDGWMQA3qF5nnRyit7SmhCsOM3VuAmRo,115
27
+ skypro/infra/repositories/errors.py,sha256=Zy4AtG7TFwrr4pn7s2pQiO9sVf8cR9p30pNrB7sAaQk,130
28
+ skypro/infra/repositories/summary.py,sha256=zvQv8JGXS-5B8BMR9X4bbCUIDjTsWfW_jBcI7FbQAhU,1221
29
+ skypro/infra/repositories/work.py,sha256=oK6QSpV-C1SXjaMhYBDX_kI5_kQJnPuQmc6k6iDcytk,777
30
+ skypro/logs/skypro.log,sha256=IkZUCPzhiMTa7CXMOZs87L-fz_DEShukwskY1yHMIMw,1471
31
+ skypro/logs/skypro.log.1,sha256=LUSfa6iIrU2daZcBnWjrrgCOxMm49OEMyTOfzIggRnY,10143
32
+ skypro/main.py,sha256=EA1EaY5nDE8TbbY8VEXsZoxPMKByKLeTFnhi4MgLmck,245
33
+ skypro/use_cases/__init__.py,sha256=GZPkx5vtQNLSfzmFGsW4bGfe4jEMI_gno7ElzykK9JY,107
34
+ skypro/use_cases/get_summary_info.py,sha256=QhE9Ef4WrejPLASNPoTkGJHt5GSc0-l0R23ag_L97zA,851
35
+ skypro/utils.py,sha256=afJojVeD6x_MQUBvQ4iemTEuWoKWl4dnZIkhmhVQ9tA,388
36
+ skypro/work_prices.json,sha256=odv-_n_kLe3_cgkJOaRKsu2_Riq2gWd436nadxPwcek,103
37
+ skypro_cli_tool-0.1.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
38
+ skypro_cli_tool-0.1.0.dist-info/entry_points.txt,sha256=lY6wQRNleBz_IMlrcUltMJs6V3LrRI1xWbdrHV_JPFE,46
39
+ skypro_cli_tool-0.1.0.dist-info/METADATA,sha256=PcIQuzwfizeGJeMhzeW2H2UeOLiqZhoRF8iAfKe_FGE,329
40
+ skypro_cli_tool-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ sky-cli = skypro.main:main
3
+