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 +0 -0
- skypro/cli/__init__.py +0 -0
- skypro/cli/app.py +25 -0
- skypro/cli/commands/__init__.py +3 -0
- skypro/cli/commands/summary.py +38 -0
- skypro/cli/renders/__init__.py +3 -0
- skypro/cli/renders/summary_table.py +44 -0
- skypro/config/__init__.py +0 -0
- skypro/config/logging.py +51 -0
- skypro/config/settings.py +29 -0
- skypro/domain/__init__.py +0 -0
- skypro/domain/enums.py +9 -0
- skypro/domain/errors.py +10 -0
- skypro/domain/models.py +33 -0
- skypro/domain/services.py +37 -0
- skypro/infra/__init__.py +0 -0
- skypro/infra/api/__init__.py +0 -0
- skypro/infra/api/base.py +31 -0
- skypro/infra/api/errors.py +4 -0
- skypro/infra/api/skypro/__init__.py +0 -0
- skypro/infra/api/skypro/client.py +78 -0
- skypro/infra/api/skypro/dto.py +13 -0
- skypro/infra/http/__init__.py +0 -0
- skypro/infra/http/client.py +64 -0
- skypro/infra/http/errors.py +1 -0
- skypro/infra/repositories/__init__.py +4 -0
- skypro/infra/repositories/errors.py +7 -0
- skypro/infra/repositories/summary.py +30 -0
- skypro/infra/repositories/work.py +27 -0
- skypro/logs/skypro.log +11 -0
- skypro/logs/skypro.log.1 +100 -0
- skypro/main.py +14 -0
- skypro/use_cases/__init__.py +3 -0
- skypro/use_cases/get_summary_info.py +28 -0
- skypro/utils.py +14 -0
- skypro/work_prices.json +7 -0
- skypro_cli_tool-0.1.0.dist-info/METADATA +13 -0
- skypro_cli_tool-0.1.0.dist-info/RECORD +40 -0
- skypro_cli_tool-0.1.0.dist-info/WHEEL +4 -0
- skypro_cli_tool-0.1.0.dist-info/entry_points.txt +3 -0
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,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,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
|
skypro/config/logging.py
ADDED
|
@@ -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
skypro/domain/errors.py
ADDED
skypro/domain/models.py
ADDED
|
@@ -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)
|
skypro/infra/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
skypro/infra/api/base.py
ADDED
|
@@ -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)
|
|
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,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
|
skypro/logs/skypro.log.1
ADDED
|
@@ -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,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)
|
skypro/work_prices.json
ADDED
|
@@ -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,,
|