skypro-cli-tool 0.2.0__tar.gz → 0.3.2__tar.gz

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