diff --git a/.github/scripts/autorelease.py b/.github/scripts/autorelease.py index a551364..c6c7bc8 100644 --- a/.github/scripts/autorelease.py +++ b/.github/scripts/autorelease.py @@ -46,17 +46,30 @@ def get_release_tags(github_token: str) -> List[str]: params["page"] += 1 -def main(github_token: str, current_branch: str) -> None: - branch_pattern = r"^v(?P\d+)$" - branch_matching = re.match(branch_pattern, current_branch, re.I) - if not branch_matching: - logger.info("Branch %s is not a valid release branch", current_branch) - return +RELEASE_BRANCH = "master" +LEGACY_BRANCH_PATTERN = re.compile(r"^v(?P\d+)$", re.I) + + +def get_major_release(current_branch: str, project_version_str: str) -> Optional[str]: + if current_branch == RELEASE_BRANCH: + return str(Version(project_version_str).major) + + branch_matching = LEGACY_BRANCH_PATTERN.match(current_branch) + if branch_matching: + return branch_matching.group("major_release") - major_release = branch_matching.group("major_release") + return None + + +def main(github_token: str, current_branch: str) -> None: project_version_str = get_project_version() logger.info("Project version %s", project_version_str) + major_release = get_major_release(current_branch, project_version_str) + if major_release is None: + logger.info("Branch %s is not a valid release branch", current_branch) + return + if not project_version_str.startswith(major_release): logger.info( "Release tag %s is invalid, major releases in the %s branch must start with %s", diff --git a/.github/workflows/autorelease.yml b/.github/workflows/autorelease.yml index aafcd8b..ec7cfb9 100644 --- a/.github/workflows/autorelease.yml +++ b/.github/workflows/autorelease.yml @@ -3,21 +3,29 @@ name: Autorelease on: pull_request: types: [ closed ] + branches: [ master, v2 ] jobs: autorelease: if: github.event.pull_request.merged == true - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ '3.8.16' ] + steps: - uses: actions/checkout@v3 - name: Set up python uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: ${{ matrix.python-version }} - name: Set up PDM uses: pdm-project/setup-pdm@v3 + with: + python-version: ${{ matrix.python-version }} + version: 2.20.1 - name: Install dependencies run: pdm install -dG release diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index dfc09ae..83782b7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,13 +6,27 @@ on: jobs: build-and-publish: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ '3.8.16' ] + permissions: id-token: write + steps: - uses: actions/checkout@v3 + + - name: Set up python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Set up PDM uses: pdm-project/setup-pdm@v3 + with: + python-version: ${{ matrix.python-version }} + version: 2.20.1 - name: Publish package distributions to PyPI run: pdm publish diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 1a1efa7..eedbec2 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -3,7 +3,7 @@ name: Linters on: push: pull_request: - branches: [ master ] + branches: [ master, v2 ] jobs: build: @@ -18,6 +18,7 @@ jobs: uses: pdm-project/setup-pdm@v3 with: python-version: ${{ matrix.python-version }} + version: 2.20.1 - name: Install dependencies run: | diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index ba54f03..6e82481 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -3,7 +3,7 @@ name: Tests on: push: pull_request: - branches: [ master ] + branches: [ master, v2 ] jobs: test: @@ -19,6 +19,7 @@ jobs: uses: pdm-project/setup-pdm@v3 with: python-version: ${{ matrix.python-version }} + version: 2.20.1 - name: Install dependencies run: | diff --git a/README.md b/README.md index a6efcd2..3f112c1 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,307 @@ [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) # huntflow-api-client -Huntflow API Client for Python +Async Python client for the [Huntflow API](https://api.huntflow.ai/v2/docs). It wraps [httpx](https://www.python-httpx.org/), adds Bearer authentication, optional automatic token refresh, and typed helpers for major resources. ## Installation -Install using `pip install huntflow-api-client` + +```bash +pip install huntflow-api-client +``` + +Requires Python **3.8.1+**. Main dependencies: [httpx](https://www.python-httpx.org/), `pydantic` v2, `email-validator`. + +## Integration overview + +1. Obtain an access token (and optionally a refresh token) using the flows described in the [Huntflow API documentation](https://api.huntflow.ai/v2/docs) (OAuth, service account, or your Huntflow product settings — whichever applies to your integration). +2. Create a `HuntflowAPI` instance with your API base URL and either a static `ApiToken` (`token=`) or a `token_proxy=`. If both are supplied, **`token_proxy` wins** and `token` is ignored. +3. Call `await api.request(...)` for any endpoint, or use entity classes (e.g. `Applicant`, `Vacancy`) for typed request/response models. + +The client appends `/v2` to `base_url` for all requests. Paths you pass to `request()` are relative to that versioned root (for example `GET` `"/accounts"`, not `"/v2/accounts"`). + +### Base URL + +The constructor default is `https://api.huntflow.dev` (development). For production, pass your real API host, for example: + +```python +HuntflowAPI("https://api.huntflow.ai", token=token) +``` + +Use the base URL Huntflow provides for your environment (no trailing `/v2`). + +## Quick start (access token only) + +Minimal setup: pass `ApiToken` with an `access_token`. Fine for short scripts. For **persisted** refresh across restarts or processes, use **`HuntflowTokenProxy`** and storage (see [Token proxy, storage, and locks](#token-proxy-storage-and-locks)). You can still set **`auto_refresh_tokens=True`** with `token=` — refresh then updates the in-memory token only (see that section for details). + +```python +import asyncio + +from huntflow_api_client import HuntflowAPI +from huntflow_api_client.tokens.token import ApiToken + + +async def main() -> None: + token = ApiToken(access_token="YOUR_ACCESS_TOKEN") + api = HuntflowAPI("https://api.huntflow.ai", token=token) + + response = await api.request("GET", "/accounts") + accounts = response.json() + print(accounts) + + +asyncio.run(main()) +``` + +## Using resource entities + +Entity classes take a `HuntflowAPI` instance and return Pydantic models parsed from JSON. + +```python +import asyncio + +from huntflow_api_client import HuntflowAPI +from huntflow_api_client.entities import Applicant +from huntflow_api_client.tokens.token import ApiToken + + +async def main() -> None: + api = HuntflowAPI( + "https://api.huntflow.ai", + token=ApiToken(access_token="YOUR_ACCESS_TOKEN"), + ) + applicants = Applicant(api) + + page = await applicants.list(account_id=1, count=10, page=1) + for item in page.items: + print(item.id, item.first_name, item.last_name) + + +asyncio.run(main()) +``` + +Other entities live under `huntflow_api_client.entities` (vacancies, webhooks, dictionaries, etc.). Each method docstring links to the matching OpenAPI operation where applicable. + +## Token proxy, storage, and locks + +`HuntflowAPI` authenticates every request using an **`AbstractTokenProxy`**. Most apps pass **`token=`** or **`token_proxy=HuntflowTokenProxy(...)`**. Subclass **`AbstractTokenProxy`** only for uncommon setups (custom token sources, extra logging, and so on). + +- If you pass **`token=`** (`ApiToken`), the client wraps it in **`DummyHuntflowTokenProxy`**. With **`auto_refresh_tokens=True`**, refreshed tokens stay **in memory** on that proxy only (nothing is persisted). You still need **`refresh_token`** set on `ApiToken`, otherwise refresh cannot run. +- For persisted refresh, pass **`token_proxy=`** — typically **`HuntflowTokenProxy`**, which loads and saves tokens through **`AbstractHuntflowTokenStorage`**. + +### Storage (`AbstractHuntflowTokenStorage`) + +Implementations must: + +- **`get()`** — return an `ApiToken` (at least `access_token`; include `refresh_token` if you use refresh). +- **`update(token)`** — persist the token after a successful `/token/refresh` (and any fields you care about, e.g. `expiration_timestamp`). + +The built-in **`HuntflowTokenFileStorage`** reads/writes a JSON file with the same keys as `ApiToken` (`access_token`, `refresh_token`, optional timestamps). The file is overwritten on refresh. + +### Locker (`AbstractLocker`) + +When **`auto_refresh_tokens=True`**, several coroutines can hit token expiry at once. **`HuntflowTokenProxy`** can use a locker so only one refresh runs; others wait or retry. + +- If **`locker=None`** (default), no synchronization is applied: concurrent refreshes are possible under load. Prefer a locker whenever **one storage** is shared by **many concurrent requests**. +- **`AsyncioLockLocker`** — sufficient for **one process / one event loop** (see [`examples/api_client_with_simple_locks.py`](examples/api_client_with_simple_locks.py)). +- For **multiple workers or hosts**, use a **distributed lock** (Redis, etc.) implementing **`AbstractLocker`**, together with storage that all instances share. + +### Wiring `HuntflowTokenProxy` + +```python +from huntflow_api_client import HuntflowAPI +from huntflow_api_client.tokens.locker import AsyncioLockLocker +from huntflow_api_client.tokens.proxy import HuntflowTokenProxy +from huntflow_api_client.tokens.storage import HuntflowTokenFileStorage + +storage = HuntflowTokenFileStorage("/secure/huntflow_token.json") +locker = AsyncioLockLocker() +token_proxy = HuntflowTokenProxy(storage, locker=locker) + +api = HuntflowAPI( + "https://api.huntflow.ai", + token_proxy=token_proxy, + auto_refresh_tokens=True, +) +``` + +Seed the JSON file once with `access_token` and `refresh_token` from Huntflow before starting. + +### Example: Redis-backed storage and lock + +The package does **not** depend on Redis; install it separately (`pip install "redis>=4.2"` so `redis.asyncio` and async locks behave consistently). Use one async Redis client for both storage and the lock. **Populate the token key** before the first API call (same JSON shape as the file storage). + +This example uses an accessor-style flow: +`RedisTokenProxy -> RedisTokenAccessor -> storage`. +`RedisTokenProxy` implements the SDK token contract, `RedisTokenAccessor` is responsible +for token retrieval and update operations, and `RedisLockLocker` synchronizes refresh +between concurrent requests. + +```python +import asyncio +import json +import time +from typing import Any, Dict, Optional + +from redis.asyncio import Redis +from redis.asyncio.lock import Lock +from redis.exceptions import LockError + +from huntflow_api_client import HuntflowAPI +from huntflow_api_client.tokens.locker import AbstractLocker +from huntflow_api_client.tokens.proxy import ( + AbstractTokenProxy, + convert_refresh_result_to_hf_token, + get_auth_headers, + get_refresh_token_data, +) +from huntflow_api_client.tokens.token import ApiToken + +POLL_INTERVAL = 0.2 + + +class RedisLockLocker(AbstractLocker): + """Coordinates token refresh across concurrent workers. + + One caller acquires the lock and performs refresh; others wait until + the lock is released and then continue with updated token data. + """ + + def __init__(self, redis: Redis, name: str = "huntflow:token_refresh") -> None: + self._lock = Lock(redis, name=name, timeout=30.0, blocking=False) + + async def acquire(self) -> bool: + try: + return bool(await self._lock.acquire()) + except LockError: + return False + + async def wait_for_lock(self) -> None: + while await self._lock.locked(): + await asyncio.sleep(POLL_INTERVAL) + + async def release(self) -> None: + try: + await self._lock.release() + except LockError: + return + + +class RedisTokenAccessor: + """Layer for token read/update operations. + + Keeps Redis calls in one place and exposes lock-related operations + used by the proxy. + """ + + def __init__( + self, + redis: Redis, + locker: AbstractLocker, + token_key: str = "huntflow:token", + ) -> None: + self._redis = redis + self._locker = locker + self._token_key = token_key + + async def get(self, bypass_lock: bool = False) -> Optional[Dict[str, Any]]: + if not bypass_lock: + await self._locker.wait_for_lock() + raw = await self._redis.get(self._token_key) + if not raw: + return None + return json.loads(raw) + + async def update(self, token: ApiToken) -> None: + await self._redis.set(self._token_key, json.dumps(token.dict())) + + async def lock_for_update(self) -> bool: + return await self._locker.acquire() + + async def release_lock(self) -> None: + await self._locker.release() + + +class RedisTokenProxy(AbstractTokenProxy): + """`AbstractTokenProxy` implementation over accessor + locker. + + Returns auth headers, provides refresh payload, saves refreshed token, + and checks whether another worker has already updated the token. + """ + + def __init__(self, accessor: RedisTokenAccessor) -> None: + self._accessor = accessor + self._token: Optional[ApiToken] = None + self._last_read_timestamp: Optional[float] = None + + async def get_auth_header(self) -> Dict[str, str]: + data = await self._accessor.get() + if data is None: + raise KeyError("Token not found in Redis. Seed access_token and refresh_token first.") + self._token = ApiToken.from_dict(data) + self._last_read_timestamp = time.time() + return get_auth_headers(self._token) + + async def get_refresh_data(self) -> Dict[str, str]: + if self._token is None: + data = await self._accessor.get() + if data is None: + raise KeyError("Token not found in Redis. Seed access_token and refresh_token first.") + self._token = ApiToken.from_dict(data) + return get_refresh_token_data(self._token) + + async def update(self, refresh_result: dict) -> None: + assert self._token is not None + self._token = convert_refresh_result_to_hf_token(refresh_result, self._token) + await self._accessor.update(self._token) + + async def lock_for_update(self) -> bool: + return await self._accessor.lock_for_update() + + async def release_lock(self) -> None: + await self._accessor.release_lock() + + async def is_updated(self) -> bool: + if self._last_read_timestamp is None: + return False + current_data = await self._accessor.get(bypass_lock=True) + if current_data is None: + return False + current = ApiToken.from_dict(current_data) + last_refresh_timestamp = current.last_refresh_timestamp or 0.0 + return last_refresh_timestamp > self._last_read_timestamp + + +def build_api(redis: Redis) -> HuntflowAPI: + locker = RedisLockLocker(redis, name="huntflow:token_refresh") + accessor = RedisTokenAccessor(redis, locker=locker, token_key="huntflow:token") + token_proxy = RedisTokenProxy(accessor) + return HuntflowAPI( + "https://api.huntflow.ai", + token_proxy=token_proxy, + auto_refresh_tokens=True, + ) +``` + +## Raw HTTP access + +Every method on entities ultimately uses `HuntflowAPI.request`, which mirrors [`httpx.AsyncClient.request`](https://www.python-httpx.org/api/#asyncclient) (`json`, `params`, `files`, `timeout`, etc.). Entity methods usually serialize typed request models (for example `ApplicantCreateRequest.jsonable_dict(...)`); with `request()` you build the JSON yourself. + +```python +account_id = 1 +payload = {"first_name": "John", "last_name": "Doe"} # match API schema + +response = await api.request( + "POST", + f"/accounts/{account_id}/applicants", + json=payload, +) +``` + +Errors from non-success status codes are turned into typed exceptions in `huntflow_api_client.errors` (for example `NotFoundError`, `BadRequestError`, `TokenExpiredError`, `AuthorizationError`). + +## Links + +- [Huntflow API v2 documentation](https://api.huntflow.ai/v2/docs) +- [Package on PyPI](https://pypi.org/project/huntflow-api-client/) +- [Source code](https://github.com/huntflow/huntflow-api-client-python) diff --git a/huntflow_api_client/entities/__init__.py b/huntflow_api_client/entities/__init__.py index 2d3a213..04b4b85 100644 --- a/huntflow_api_client/entities/__init__.py +++ b/huntflow_api_client/entities/__init__.py @@ -18,9 +18,11 @@ from huntflow_api_client.entities.organization_settings import OrganizationSettings from huntflow_api_client.entities.production_calendars import ProductionCalendar from huntflow_api_client.entities.questionary import ApplicantsQuestionary +from huntflow_api_client.entities.recommendation import Recommendation from huntflow_api_client.entities.regions import Region from huntflow_api_client.entities.rejection_reason import RejectionReason from huntflow_api_client.entities.resume import Resume +from huntflow_api_client.entities.survey_type_a import SurveyTypeA from huntflow_api_client.entities.survey_type_q import SurveyTypeQ from huntflow_api_client.entities.tags import AccountTag, ApplicantTag from huntflow_api_client.entities.user_settings import UserSettings @@ -53,14 +55,16 @@ "MultiVacancy", "OrganizationSettings", "ProductionCalendar", + "Recommendation", "Region", "RejectionReason", "Resume", + "SurveyTypeA", + "SurveyTypeQ", "User", "UsersManagement", "UserSettings", "Vacancy", "VacancyRequest", "Webhook", - "SurveyTypeQ", ) diff --git a/huntflow_api_client/entities/applicant_on_vacancy.py b/huntflow_api_client/entities/applicant_on_vacancy.py index e65e574..75bbf60 100644 --- a/huntflow_api_client/entities/applicant_on_vacancy.py +++ b/huntflow_api_client/entities/applicant_on_vacancy.py @@ -77,6 +77,6 @@ async def move_applicant_to_child_vacancy( response = await self._api.request( "PUT", f"/accounts/{account_id}/applicants/vacancy/{vacancy_id}/split", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(exclude_unset=True), ) return ApplicantVacancySplitResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/applicants.py b/huntflow_api_client/entities/applicants.py index 02f1c23..d959cf2 100644 --- a/huntflow_api_client/entities/applicants.py +++ b/huntflow_api_client/entities/applicants.py @@ -6,16 +6,18 @@ GetEntityMixin, ListEntityMixin, ) -from huntflow_api_client.models.consts import AgreementState, ApplicantSearchField +from huntflow_api_client.models.consts import AgreementStateRequest, ApplicantSearchField from huntflow_api_client.models.request.applicants import ( ApplicantCreateRequest, ApplicantUpdateRequest, ) from huntflow_api_client.models.response.applicants import ( + ApplicantCreateAgreementLinkResponse, ApplicantCreateResponse, ApplicantItem, ApplicantListResponse, ApplicantSearchByCursorResponse, + ApplicantSendAgreementResponse, ) @@ -27,7 +29,7 @@ async def list( page: Optional[int] = 1, status: Optional[int] = None, vacancy_id: Optional[int] = None, - agreement_state: Optional[AgreementState] = None, + agreement_state: Optional[AgreementStateRequest] = None, ) -> ApplicantListResponse: """ API method reference https://api.huntflow.ai/v2/docs#get-/accounts/-account_id-/applicants @@ -108,7 +110,7 @@ async def patch( response = await self._api.request( "PATCH", f"/accounts/{account_id}/applicants/{applicant_id}", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(exclude_unset=True), ) return ApplicantItem.model_validate(response.json()) @@ -186,3 +188,41 @@ async def search_by_cursor( response = await self._api.request("GET", path, params=params) return ApplicantSearchByCursorResponse.model_validate(response.json()) + + async def create_agreement_link( + self, + account_id: int, + applicant_id: int, + ) -> ApplicantCreateAgreementLinkResponse: + """ + API method reference: + https://api.huntflow.ai/v2/docs#post-/accounts/-account_id-/applicants/-applicant_id-/agreement_link + + :param account_id: Organization ID + :param applicant_id: Applicant ID + :return: Link to interact with agreement + """ + response = await self._api.request( + "POST", + f"/accounts/{account_id}/applicants/{applicant_id}/agreement_link", + ) + return ApplicantCreateAgreementLinkResponse.model_validate(response.json()) + + async def send_agreement_via_email( + self, + account_id: int, + applicant_id: int, + ) -> ApplicantSendAgreementResponse: + """ + API method reference: + https://api.huntflow.ai/v2/docs#post-/accounts/-account_id-/applicants/-applicant_id-/agreement_email + + :param account_id: Organization ID + :param applicant_id: Applicant ID + :return: Job_id and recipient email of agreement + """ + response = await self._api.request( + "POST", + f"/accounts/{account_id}/applicants/{applicant_id}/agreement_email", + ) + return ApplicantSendAgreementResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/dictionaries.py b/huntflow_api_client/entities/dictionaries.py index 43e3c32..ce9f0c9 100644 --- a/huntflow_api_client/entities/dictionaries.py +++ b/huntflow_api_client/entities/dictionaries.py @@ -43,7 +43,11 @@ async def create( :return: An object that contains the task ID of the delayed background update task """ path = f"/accounts/{account_id}/dictionaries" - response = await self._api.request("POST", path, json=data.jsonable_dict(exclude_none=True)) + response = await self._api.request( + "POST", + path, + json=data.jsonable_dict(exclude_unset=True), + ) return DictionaryTaskResponse.model_validate(response.json()) async def get(self, account_id: int, dict_code: str) -> DictionaryResponse: @@ -75,5 +79,5 @@ async def update( :return: An object that contains the task ID of the delayed background update task """ path = f"/accounts/{account_id}/dictionaries/{dict_code}" - response = await self._api.request("PUT", path, json=data.jsonable_dict(exclude_none=True)) + response = await self._api.request("PUT", path, json=data.jsonable_dict(exclude_unset=True)) return DictionaryTaskResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/file.py b/huntflow_api_client/entities/file.py index 482e6dd..479739c 100644 --- a/huntflow_api_client/entities/file.py +++ b/huntflow_api_client/entities/file.py @@ -1,4 +1,4 @@ -from typing import BinaryIO, Optional, Union +from typing import BinaryIO, Dict, Optional, Tuple, Union from huntflow_api_client.entities.base import BaseEntity from huntflow_api_client.models.request.file import UploadFileHeaders @@ -12,6 +12,7 @@ async def upload( headers: UploadFileHeaders, file: Union[bytes, BinaryIO], preset: Optional[str] = None, + filename: Optional[str] = None, ) -> UploadResponse: """ API method reference https://api.huntflow.ai/v2/docs#post-/accounts/-account_id-/upload @@ -20,16 +21,24 @@ async def upload( :param file: File :param preset: Preset :param headers: Headers + :param filename: Filename :return: Additional data """ data = {} if preset: data["preset"] = preset + + files: Dict[str, Union[Union[bytes, BinaryIO], Tuple[str, Union[bytes, BinaryIO]]]] = {} + if filename: + files["file"] = (filename, file) + else: + files["file"] = file + response = await self._api.request( "POST", f"/accounts/{account_id}/upload", - files={"file": file}, + files=files, data=data, headers=headers.jsonable_dict(exclude_none=True, by_alias=True), ) diff --git a/huntflow_api_client/entities/multi_vacancies.py b/huntflow_api_client/entities/multi_vacancies.py index 8f809b5..bc13e6e 100644 --- a/huntflow_api_client/entities/multi_vacancies.py +++ b/huntflow_api_client/entities/multi_vacancies.py @@ -26,7 +26,7 @@ async def create( response = await self._api.request( "POST", f"/accounts/{account_id}/multi-vacancies", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(exclude_unset=True), ) return MultiVacancyResponse.model_validate(response.json()) @@ -52,6 +52,6 @@ async def update( response = await self._api.request( method, f"/accounts/{account_id}/multi-vacancies/{vacancy_id}", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(exclude_unset=True), ) return MultiVacancyResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/organization_settings.py b/huntflow_api_client/entities/organization_settings.py index 353f7dd..863e1ec 100644 --- a/huntflow_api_client/entities/organization_settings.py +++ b/huntflow_api_client/entities/organization_settings.py @@ -1,4 +1,5 @@ from huntflow_api_client.entities.base import BaseEntity +from huntflow_api_client.models.response.interview_types import InterviewTypesListResponse from huntflow_api_client.models.response.organization_settings import ( BaseSurveySchemaTypeWithSchemas, CloseReasonsListResponse, @@ -53,3 +54,15 @@ async def get_applicant_survey_form( f"/accounts/{account_id}/surveys/type_a/{survey_id}", ) return BaseSurveySchemaTypeWithSchemas.model_validate(response.json()) + + async def get_interview_types(self, account_id: int) -> InterviewTypesListResponse: + """ + API method reference + https://api.huntflow.ai/v2/docs#get-/accounts/-account_id-/interview_types + + :param account_id: Organization ID + + :return: List of interview types + """ + response = await self._api.request("GET", f"/accounts/{account_id}/interview_types") + return InterviewTypesListResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/production_calendars.py b/huntflow_api_client/entities/production_calendars.py index c27536a..26bdbeb 100644 --- a/huntflow_api_client/entities/production_calendars.py +++ b/huntflow_api_client/entities/production_calendars.py @@ -55,7 +55,7 @@ async def get_non_working_days_in_period( calendar_id: int, deadline: datetime.date, start: Optional[datetime.date] = None, - verbose: Optional[bool] = True, + verbose: Optional[bool] = False, ) -> NonWorkingDaysResponse: """ API method reference diff --git a/huntflow_api_client/entities/recommendation.py b/huntflow_api_client/entities/recommendation.py new file mode 100644 index 0000000..10b9855 --- /dev/null +++ b/huntflow_api_client/entities/recommendation.py @@ -0,0 +1,40 @@ +from typing import Dict, Optional, Union + +from huntflow_api_client.entities.base import BaseEntity, ListEntityMixin +from huntflow_api_client.models.consts import RecommendationProcessingStatus +from huntflow_api_client.models.response.recommendation import RecommendationListResponse + + +class Recommendation(BaseEntity, ListEntityMixin): + async def list( + self, + account_id: int, + vacancy_id: int, + count: int = 30, + processing_status: RecommendationProcessingStatus = RecommendationProcessingStatus.ALL, + next_page_cursor: Optional[str] = None, + ) -> RecommendationListResponse: + """ + API method reference + https://api.huntflow.ai/v2/docs#get-/accounts/-account_id-/recommendations/-vacancy_id- + + :param account_id: Organization ID + :param vacancy_id: Vacancy ID + :param count: Number of items per page + :param next_page_cursor: Next page cursor + :param processing_status: Get all recommendations or processed/unprocessed only + :return: A list of applicants recommended for a vacancy + """ + params: Dict[str, Union[str, int]] + if next_page_cursor is not None: + params = {"next_page_cursor": next_page_cursor} + else: + params = {"count": count} + params["processing_status"] = processing_status.value + + response = await self._api.request( + "GET", + f"/accounts/{account_id}/recommendations/{vacancy_id}", + params=params, + ) + return RecommendationListResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/resume.py b/huntflow_api_client/entities/resume.py index e664937..899ba52 100644 --- a/huntflow_api_client/entities/resume.py +++ b/huntflow_api_client/entities/resume.py @@ -83,7 +83,7 @@ async def update( response = await self._api.request( "PUT", f"/accounts/{account_id}/applicants/{applicant_id}/externals/{external_id}", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(), ) return ApplicantResumeResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/survey_type_a.py b/huntflow_api_client/entities/survey_type_a.py new file mode 100644 index 0000000..b251450 --- /dev/null +++ b/huntflow_api_client/entities/survey_type_a.py @@ -0,0 +1,65 @@ +from huntflow_api_client.entities.base import BaseEntity, GetEntityMixin, ListEntityMixin +from huntflow_api_client.models.response.survey import ( + SurveyAnswerTypeAResponse, + SurveySchemasTypeAListResponse, + SurveySchemaTypeAResponse, +) + + +class SurveyTypeA(BaseEntity, GetEntityMixin, ListEntityMixin): + async def list( + self, + account_id: int, + active: bool = True, + ) -> SurveySchemasTypeAListResponse: + """ + API method reference + https://api.huntflow.ai/v2/docs#get-/accounts/-account_id-/surveys/type_a + + :param account_id: Organization ID + :param active: Shows only active schemas + :return: List of all applicant feedback forms in organization. + """ + params = {"active": active} + response = await self._api.request( + "GET", + f"/accounts/{account_id}/surveys/type_a", + params=params, + ) + return SurveySchemasTypeAListResponse.model_validate(response.json()) + + async def get(self, account_id: int, survey_id: int) -> SurveySchemaTypeAResponse: + """ + API method reference + https://api.huntflow.ai/v2/docs#get-/accounts/-account_id-/surveys/type_a/-survey_id- + + :param account_id: Organization ID + :param survey_id: Survey ID + :return: An applicant feedback forms in organization. + """ + response = await self._api.request( + "GET", + f"/accounts/{account_id}/surveys/type_a/{survey_id}", + ) + return SurveySchemaTypeAResponse.model_validate(response.json()) + + async def get_applicant_answer( + self, + account_id: int, + survey_id: int, + answer_id: int, + ) -> SurveyAnswerTypeAResponse: + """ + API method reference + https://api.huntflow.ai/v2/docs#get-/accounts/-account_id-/surveys/type_a/-survey_id-/answers/-answer_id- + + :param account_id: Organization ID + :param survey_id: Survey ID + :param answer_id: Answer ID + :return: Returns answer of applicant feedback form. + """ + response = await self._api.request( + "GET", + f"/accounts/{account_id}/surveys/type_a/{survey_id}/answers/{answer_id}", + ) + return SurveyAnswerTypeAResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/tags.py b/huntflow_api_client/entities/tags.py index d4cc948..fa86f41 100644 --- a/huntflow_api_client/entities/tags.py +++ b/huntflow_api_client/entities/tags.py @@ -43,7 +43,7 @@ async def create( response = await self._api.request( "POST", f"/accounts/{account_id}/tags", - json=account_tag.jsonable_dict(exclude_none=True), + json=account_tag.jsonable_dict(), ) return AccountTagResponse.model_validate(response.json()) @@ -65,7 +65,7 @@ async def update( response = await self._api.request( "PUT", f"/accounts/{account_id}/tags/{account_tag_id}", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(), ) return AccountTagResponse.model_validate(response.json()) @@ -112,7 +112,7 @@ async def update( response = await self._api.request( "POST", f"/accounts/{account_id}/applicants/{applicant_id}/tags", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(), ) return ApplicantTagsListResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/user_settings.py b/huntflow_api_client/entities/user_settings.py index b020190..9943127 100644 --- a/huntflow_api_client/entities/user_settings.py +++ b/huntflow_api_client/entities/user_settings.py @@ -1,6 +1,12 @@ from huntflow_api_client.entities.base import BaseEntity +from huntflow_api_client.models.common import StatusResponse +from huntflow_api_client.models.request.user_settings import ( + ExchangeEmailAccountRequest, + OtherEmailAccountRequest, +) from huntflow_api_client.models.response.user_settings import ( CalendarAccountsListResponse, + EmailAccount, EmailAccountsListResponse, ) @@ -23,3 +29,87 @@ async def get_calendar_accounts(self) -> CalendarAccountsListResponse: """ response = await self._api.request("GET", "/calendar_accounts") return CalendarAccountsListResponse.model_validate(response.json()) + + async def create_exchange_email_account( + self, + data: ExchangeEmailAccountRequest, + ) -> EmailAccount: + """ + API method reference https://api.huntflow.ai/v2/docs#post-/email_accounts/exchange + + :param data: Exchange user email account data + :return: Created Exchange user email account for API robot. + """ + response = await self._api.request( + "POST", + "/email_accounts/exchange", + json=data.jsonable_dict(exclude_none=True), + ) + return EmailAccount.model_validate(response.json()) + + async def update_exchange_email_account( + self, + data: ExchangeEmailAccountRequest, + account_email_id: int, + ) -> EmailAccount: + """ + API method reference + https://api.huntflow.ai/v2/docs#put-/email_accounts/exchange/-account_email_id- + + :param account_email_id: Email account ID + :param data: Exchange user email account data + :return: Updated Exchange user email account for API robot. + """ + response = await self._api.request( + "PUT", + f"/email_accounts/exchange/{account_email_id}", + json=data.jsonable_dict(exclude_none=True), + ) + return EmailAccount.model_validate(response.json()) + + async def create_other_email_account( + self, + data: OtherEmailAccountRequest, + ) -> EmailAccount: + """ + API method reference https://api.huntflow.ai/v2/docs#post-/email_accounts/other + + :param data: SMTP/POP3/IMAP user email account data + :return: Created SMTP/POP3/IMAP user email account for API robot. + """ + response = await self._api.request( + "POST", + "/email_accounts/other", + json=data.jsonable_dict(exclude_none=True), + ) + return EmailAccount.model_validate(response.json()) + + async def update_other_email_account( + self, + data: OtherEmailAccountRequest, + account_email_id: int, + ) -> EmailAccount: + """ + API method reference + https://api.huntflow.ai/v2/docs#put-/email_accounts/other/-account_email_id- + + :param account_email_id: Email account ID + :param data: SMTP/POP3/IMAP user email account data + :return: Updated SMTP/POP3/IMAP user email account for API robot. + """ + response = await self._api.request( + "PUT", + f"/email_accounts/other/{account_email_id}", + json=data.jsonable_dict(exclude_none=True), + ) + return EmailAccount.model_validate(response.json()) + + async def delete_email_account(self, account_email_id: int) -> StatusResponse: + """ + API method reference + https://api.huntflow.ai/v2/docs#delete-/email_accounts/-account_email_id- + + :param account_email_id: Email account ID + """ + response = await self._api.request("DELETE", f"/email_accounts/{account_email_id}") + return StatusResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/users_management.py b/huntflow_api_client/entities/users_management.py index 5e50d7e..5b4b904 100644 --- a/huntflow_api_client/entities/users_management.py +++ b/huntflow_api_client/entities/users_management.py @@ -1,6 +1,7 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from huntflow_api_client.entities.base import BaseEntity +from huntflow_api_client.models.consts import MemberType from huntflow_api_client.models.request.users_management import ForeignUserRequest from huntflow_api_client.models.response.users_management import ( CreatedUserControlTaskResponse, @@ -15,6 +16,7 @@ class UsersManagement(BaseEntity): async def get_users_with_foreign( self, account_id: int, + member_types: Optional[List[MemberType]] = None, count: Optional[int] = 30, page: Optional[int] = 1, ) -> ForeignUsersListResponse: @@ -23,6 +25,7 @@ async def get_users_with_foreign( https://api.huntflow.ai/v2/docs#get-/accounts/-account_id-/users/foreign :param account_id: Organization ID + :param member_types: Array of member types :param count: Number of items per page :param page: Page number @@ -32,6 +35,8 @@ async def get_users_with_foreign( All identifiers in response are foreign. """ params: Dict[str, Any] = {"count": count, "page": page} + if member_types: + params["member_types"] = [member_type.value for member_type in member_types] response = await self._api.request( "GET", f"/accounts/{account_id}/users/foreign", diff --git a/huntflow_api_client/entities/vacancies.py b/huntflow_api_client/entities/vacancies.py index c0248de..acea6b6 100644 --- a/huntflow_api_client/entities/vacancies.py +++ b/huntflow_api_client/entities/vacancies.py @@ -96,7 +96,7 @@ async def create(self, account_id: int, data: VacancyCreateRequest) -> VacancyCr response = await self._api.request( "POST", f"/accounts/{account_id}/vacancies", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(exclude_unset=True), ) return VacancyCreateResponse.model_validate(response.json()) @@ -118,7 +118,7 @@ async def update( response = await self._api.request( "PUT", f"/accounts/{account_id}/vacancies/{vacancy_id}", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(exclude_unset=True), ) return VacancyResponse.model_validate(response.json()) @@ -150,7 +150,7 @@ async def patch( response = await self._api.request( "PATCH", f"/accounts/{account_id}/vacancies/{vacancy_id}", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(exclude_unset=True), ) return VacancyResponse.model_validate(response.json()) @@ -174,7 +174,7 @@ async def assign_coworker( response = await self._api.request( "PUT", f"/accounts/{account_id}/vacancies/{vacancy_id}/members/{account_member_id}", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(), ) return StatusResponse.model_validate(response.json()) @@ -304,7 +304,7 @@ async def close(self, account_id: int, vacancy_id: int, data: VacancyCloseReques await self._api.request( "POST", f"/accounts/{account_id}/vacancies/{vacancy_id}/state/close", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(), ) async def hold(self, account_id: int, vacancy_id: int, data: VacancyHoldRequest) -> None: @@ -319,7 +319,7 @@ async def hold(self, account_id: int, vacancy_id: int, data: VacancyHoldRequest) await self._api.request( "POST", f"/accounts/{account_id}/vacancies/{vacancy_id}/state/hold", - json=data.jsonable_dict(exclude_none=True), + json=data.jsonable_dict(), ) async def resume(self, account_id: int, vacancy_id: int) -> None: diff --git a/huntflow_api_client/entities/vacancy_requests.py b/huntflow_api_client/entities/vacancy_requests.py index b89eccb..f818946 100644 --- a/huntflow_api_client/entities/vacancy_requests.py +++ b/huntflow_api_client/entities/vacancy_requests.py @@ -76,6 +76,6 @@ async def create( response = await self._api.request( "POST", path, - json=request_data.jsonable_dict(exclude_none=True), + json=request_data.jsonable_dict(exclude_unset=True), ) return VacancyRequestResponse.model_validate(response.json()) diff --git a/huntflow_api_client/entities/webhooks.py b/huntflow_api_client/entities/webhooks.py index 5c0c7a9..a60d204 100644 --- a/huntflow_api_client/entities/webhooks.py +++ b/huntflow_api_client/entities/webhooks.py @@ -1,23 +1,35 @@ +from typing import Any, Dict, Optional + from huntflow_api_client.entities.base import ( BaseEntity, CreateEntityMixin, DeleteEntityMixin, ListEntityMixin, ) +from huntflow_api_client.models.consts import WebhookType from huntflow_api_client.models.request.webhooks import WebhookRequest from huntflow_api_client.models.response.webhooks import WebhookResponse, WebhooksListResponse class Webhook(BaseEntity, ListEntityMixin, CreateEntityMixin, DeleteEntityMixin): - async def list(self, account_id: int) -> WebhooksListResponse: + async def list( + self, + account_id: int, + webhook_type: Optional[WebhookType] = None, + ) -> WebhooksListResponse: """ API method reference https://api.huntflow.ai/v2/docs#get-/accounts/-account_id-/hooks :param account_id: Organization ID + :param webhook_type: Webhook type. If no value provided, webhooks of all types will be + returned. :return: List of webhooks """ path = f"/accounts/{account_id}/hooks" - response = await self._api.request("GET", path) + params: Dict[str, Any] = {} + if webhook_type: + params["webhook_type"] = webhook_type.value + response = await self._api.request("GET", path, params=params) return WebhooksListResponse.model_validate(response.json()) async def create(self, account_id: int, data: WebhookRequest) -> WebhookResponse: @@ -29,7 +41,7 @@ async def create(self, account_id: int, data: WebhookRequest) -> WebhookResponse :return: Information about the webhook """ path = f"/accounts/{account_id}/hooks" - response = await self._api.request("POST", path, json=data.jsonable_dict(exclude_none=True)) + response = await self._api.request("POST", path, json=data.jsonable_dict()) return WebhookResponse.model_validate(response.json()) async def delete(self, account_id: int, webhook_id: int) -> None: diff --git a/huntflow_api_client/models/common.py b/huntflow_api_client/models/common.py index 8d458f1..0a63297 100644 --- a/huntflow_api_client/models/common.py +++ b/huntflow_api_client/models/common.py @@ -7,6 +7,7 @@ from huntflow_api_client.models.consts import ( CalendarEventReminderMethod, + CalendarEventStatus, EmailContactType, EventReminderMultiplier, MemberType, @@ -169,6 +170,7 @@ class EmailRecipient(BaseModel): name: Optional[str] = Field( None, description="Name of email recipient", + alias="displayName", ) email: str = Field(..., description="Email address") @@ -234,14 +236,21 @@ class CalendarEventReminder(BaseModel): method: CalendarEventReminderMethod = Field(..., description="Reminder method") -class CalendarEventAttendee(BaseModel): +class CalendarEventAttendeeRequest(BaseModel): member: Optional[PositiveInt] = Field(None, description="Coworker ID") name: Optional[str] = Field(None, description="Attendee name", alias="displayName") - email: EmailStr = Field(..., description="Attendee email") + email: str = Field(..., description="Attendee email") model_config = ConfigDict(populate_by_name=True) +class CalendarEventAttendeeResponse(CalendarEventAttendeeRequest): + status: Optional[CalendarEventStatus] = Field( + None, + alias="responseStatus", + ) + + class SurveyQuestionaryRespondent(BaseModel): applicant_id: int = Field(..., description="Applicant ID") diff --git a/huntflow_api_client/models/consts.py b/huntflow_api_client/models/consts.py index 57dc449..bdb1e3b 100644 --- a/huntflow_api_client/models/consts.py +++ b/huntflow_api_client/models/consts.py @@ -1,5 +1,7 @@ from enum import Enum +from huntflow_api_client.utils import extend_enum + class WebhookEvent(str, Enum): APPLICANT = "APPLICANT" @@ -7,6 +9,13 @@ class WebhookEvent(str, Enum): RESPONSE = "RESPONSE" OFFER = "OFFER" VACANCY_REQUEST = "VACANCY-REQUEST" + RECRUITMENT_EVALUATION = "RECRUITMENT-EVALUATION" + SURVEY_QUESTIONARY = "SURVEY-QUESTIONARY" + + +class WebhookType(str, Enum): + USER = "USER" + APPLICATION = "APPLICATION" class MemberType(str, Enum): @@ -62,11 +71,15 @@ class FieldType(str, Enum): html = "html" -class AgreementState(str, Enum): +class AgreementStateRequest(str, Enum): not_sent = "not_sent" sent = "sent" accepted = "accepted" declined = "declined" + + +@extend_enum(AgreementStateRequest) +class AgreementStateResponse(str, Enum): send_error = "send_error" @@ -154,6 +167,7 @@ class ActionLogType(str, Enum): VACANCY_EXTERNAL = "VACANCY_EXTERNAL" ACCOUNT_MEMBER = "ACCOUNT_MEMBER" DOWNLOAD_APPLICANTS = "DOWNLOAD_APPLICANTS" + PASSWORD_CHANGE = "PASSWORD_CHANGE" class SurveyType(str, Enum): @@ -171,3 +185,30 @@ class UserControlTaskStatus(str, Enum): PENDING = "PENDING" SUCCESS = "SUCCESS" FAILED = "FAILED" + + +class RecommendationProcessingStatus(str, Enum): + ALL = "ALL" + PROCESSED = "PROCESSED" + UNPROCESSED = "UNPROCESSED" + + +class RecommendationStatus(str, Enum): + TAKEN = "TAKEN" + TAKEN_OTHER = "TAKEN_OTHER" + DECLINED = "DECLINED" + + +class InterviewType(str, Enum): + USER = "user" + INTERVIEW = "interview" + + +class ExchangeAccessType(str, Enum): + DEFAULT = "DEFAULT" + IMPERSONATION = "IMPERSONATION" + + +class EmailInboundType(str, Enum): + DEFAULT = "IMAP" + IMPERSONATION = "POP3" diff --git a/huntflow_api_client/models/request/applicant_logs.py b/huntflow_api_client/models/request/applicant_logs.py index 8b1a6f8..d6178d9 100644 --- a/huntflow_api_client/models/request/applicant_logs.py +++ b/huntflow_api_client/models/request/applicant_logs.py @@ -8,7 +8,7 @@ ApplicantLogIm, ApplicantLogSms, ApplicantOffer, - CalendarEventAttendee, + CalendarEventAttendeeRequest, CalendarEventReminder, JsonRequestModel, ) @@ -25,7 +25,7 @@ class ApplicantLogCalendarEvent(BaseModel): event_type: CalendarEventType = Field(..., description="Calendar event type") description: Optional[str] = Field(None, description="Event description (comment)") calendar: PositiveInt = Field(..., description="Calendar ID") - attendees: List[CalendarEventAttendee] = Field( + attendees: List[CalendarEventAttendeeRequest] = Field( ..., description="Event attendees (participants)", ) diff --git a/huntflow_api_client/models/request/applicants.py b/huntflow_api_client/models/request/applicants.py index 16286ab..4ed6e67 100644 --- a/huntflow_api_client/models/request/applicants.py +++ b/huntflow_api_client/models/request/applicants.py @@ -5,7 +5,7 @@ from huntflow_api_client.models.common import ( Applicant, - CalendarEventAttendee, + CalendarEventAttendeeRequest, CalendarEventReminder, JsonRequestModel, ) @@ -31,6 +31,11 @@ class ApplicantSocial(BaseModel): value: str = Field(..., description="Value") +class ApplicantSite(BaseModel): + site_type: Literal["MAX"] = Field(..., description="Type") + value: str = Field(..., description="Value") + + class ApplicantCreateRequest(Applicant, JsonRequestModel): birthday: Optional[date] = Field(None, description="Date of birth") externals: Optional[List[ApplicantResumeCreate]] = Field( @@ -43,6 +48,11 @@ class ApplicantCreateRequest(Applicant, JsonRequestModel): max_length=1, description="List of applicant's social accounts", ) + site: List[ApplicantSite] = Field( + [], + max_length=1, + description="List of applicant's sites", + ) class ApplicantUpdateRequest(Applicant, JsonRequestModel): @@ -52,6 +62,11 @@ class ApplicantUpdateRequest(Applicant, JsonRequestModel): max_length=1, description="List of applicant's social accounts", ) + site: Optional[List[ApplicantSite]] = Field( + None, + max_length=1, + description="List of applicant's sites", + ) class ApplicantEvent(BaseModel): @@ -67,7 +82,7 @@ class ApplicantEvent(BaseModel): event_type: CalendarEventType = Field(..., description="Calendar event type") description: Optional[str] = Field(None, description="Event description (comment)") calendar: PositiveInt = Field(..., description="Calendar ID") - attendees: List[CalendarEventAttendee] = Field( + attendees: List[CalendarEventAttendeeRequest] = Field( ..., description="Event attendees (participants)", ) diff --git a/huntflow_api_client/models/request/resume.py b/huntflow_api_client/models/request/resume.py index 2739332..60f972a 100644 --- a/huntflow_api_client/models/request/resume.py +++ b/huntflow_api_client/models/request/resume.py @@ -13,7 +13,7 @@ class ApplicantResumeUpdateRequest(JsonRequestModel): account_source: Optional[PositiveInt] = Field(..., description="Resume source ID") data: Optional[ApplicantResumeUpdateData] = Field(..., description="Resume data") files: Optional[List[PositiveInt]] = Field( - [], + None, max_length=1, description="Upload files
" "List of file's ID attached to the applicant resume", ) diff --git a/huntflow_api_client/models/request/user_settings.py b/huntflow_api_client/models/request/user_settings.py new file mode 100644 index 0000000..d3c021a --- /dev/null +++ b/huntflow_api_client/models/request/user_settings.py @@ -0,0 +1,35 @@ +from typing import Optional + +from pydantic import AnyHttpUrl, EmailStr, Field, PositiveInt + +from huntflow_api_client.models.common import JsonRequestModel +from huntflow_api_client.models.consts import EmailInboundType, ExchangeAccessType + + +class ExchangeEmailAccountRequest(JsonRequestModel): + """ + Model for EXCHANGE email connection + """ + + access_type: ExchangeAccessType = Field(..., description="Exchange access type") + email: EmailStr = Field(..., description="Email address") + ews_url: AnyHttpUrl = Field(..., description="Exchange server URL") + password: str = Field(..., description="Exchange password") + user: Optional[str] = Field(None, description="Exchange user name") + + +class OtherEmailAccountRequest(JsonRequestModel): + """ + Model for IMAP/POP3 & SMTP email connection + """ + + email: EmailStr = Field(..., description="Email address") + password: str = Field(..., description="Mailbox password") + + inbound_host: str = Field(..., description="Inbound mail server URL without protocol") + inbound_port: Optional[PositiveInt] = Field(None, description="Inbound mail port") + inbound_ssl: bool = Field(False, description="Inbound mail server uses SSL") + outbound_host: str = Field(..., description="Outbound mail server URL without protocol") + outbound_port: Optional[PositiveInt] = Field(None, description="Outbound mail port") + outbound_ssl: bool = Field(False, description="Outbound mail server uses SSL") + type: EmailInboundType = Field(..., description="Type of inbound mail server") diff --git a/huntflow_api_client/models/request/webhooks.py b/huntflow_api_client/models/request/webhooks.py index f9226cb..8298a30 100644 --- a/huntflow_api_client/models/request/webhooks.py +++ b/huntflow_api_client/models/request/webhooks.py @@ -3,7 +3,7 @@ from pydantic import Field from huntflow_api_client.models.common import JsonRequestModel -from huntflow_api_client.models.consts import WebhookEvent +from huntflow_api_client.models.consts import WebhookEvent, WebhookType class WebhookRequest(JsonRequestModel): @@ -11,3 +11,4 @@ class WebhookRequest(JsonRequestModel): url: str = Field(..., description="Webhook URL") active: bool = Field(..., description="Webhook activity flag") webhook_events: List[WebhookEvent] = Field(..., description="List of webhook events") + type: WebhookType = Field(default=WebhookType.USER, description="Webhook type") diff --git a/huntflow_api_client/models/response/accounts.py b/huntflow_api_client/models/response/accounts.py index bf48347..2da3d4f 100644 --- a/huntflow_api_client/models/response/accounts.py +++ b/huntflow_api_client/models/response/accounts.py @@ -1,6 +1,6 @@ from typing import List, Optional -from pydantic import AnyHttpUrl, BaseModel, EmailStr, Field, PositiveInt +from pydantic import AnyHttpUrl, BaseModel, Field, PositiveInt from huntflow_api_client.models.consts import MemberType @@ -9,7 +9,7 @@ class MeResponse(BaseModel): id: PositiveInt = Field(..., description="User ID") name: Optional[str] = Field(None, description="User name") position: Optional[str] = Field(None, description="User occupation") - email: Optional[EmailStr] = Field(None, description="Email address") + email: Optional[str] = Field(None, description="Email address") phone: Optional[str] = Field(None, description="Phone number") locale: str = Field(..., description="User locale") diff --git a/huntflow_api_client/models/response/action_logs.py b/huntflow_api_client/models/response/action_logs.py index 7ce9ee9..70517e8 100644 --- a/huntflow_api_client/models/response/action_logs.py +++ b/huntflow_api_client/models/response/action_logs.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import List, Optional -from pydantic import BaseModel, EmailStr, Field, PositiveInt +from pydantic import BaseModel, Field, PositiveInt from huntflow_api_client.models.consts import ActionLogType @@ -9,7 +9,7 @@ class User(BaseModel): id: PositiveInt = Field(..., description="Coworker ID") name: str = Field(..., description="Coworker name") - email: Optional[EmailStr] = Field(None, description="Email") + email: Optional[str] = Field(None, description="Email") phone: Optional[str] = Field(None, description="Phone number") meta: Optional[dict] = Field(None, description="Additional information") diff --git a/huntflow_api_client/models/response/applicant_logs.py b/huntflow_api_client/models/response/applicant_logs.py index 074af87..24c4896 100644 --- a/huntflow_api_client/models/response/applicant_logs.py +++ b/huntflow_api_client/models/response/applicant_logs.py @@ -5,7 +5,8 @@ from huntflow_api_client.models.common import ( ApplicantOffer, - CalendarEventAttendee, + CalendarEventAttendeeResponse, + EmailRecipient, File, PaginatedResponse, VacancyQuotaItem, @@ -15,11 +16,11 @@ CalendarEventReminderMethod, CalendarEventStatus, CalendarEventType, - EmailContactType, SurveyType, Transparency, ) from huntflow_api_client.models.response.applicant_offers import ApplicantVacancyOffer +from huntflow_api_client.models.response.survey import SurveyTypeARespondent class BaseSurveySchemaType(BaseModel): @@ -35,6 +36,10 @@ class SurveySchemaTypeQLogResponse(BaseSurveySchemaType): title: Optional[str] = Field(..., description="Survey title") +class SurveySchemaTypeALogResponse(BaseSurveySchemaType): + pass + + class ApplicantLogSurveyQuestionary(BaseModel): id: int = Field(..., description="Survey questionary ID") survey: SurveySchemaTypeQLogResponse = Field(..., description="Survey schema") @@ -48,13 +53,14 @@ class ApplicantLogSurveyQuestionary(BaseModel): ) -class EmailRecipient(BaseModel): - type: Optional[EmailContactType] = Field(None, description="Type of the email contact") - name: Optional[str] = Field( - None, - description="Name of email recipient", +class ApplicantLogSurveyAnswerTypeA(BaseModel): + id: int = Field(..., description="Survey questionary ID") + created: datetime = Field( + ..., + description="Date and time of creating an survey questionary of type A", ) - email: str = Field(..., description="Email address") + respondent: SurveyTypeARespondent = Field(..., description="Who created the survey answer") + survey: SurveySchemaTypeALogResponse = Field(..., description="Survey schema") class ApplicantLogAccountInfo(BaseModel): @@ -111,7 +117,7 @@ class ApplicantLogCalendarEvent(BaseModel): ) foreign: Optional[str] = Field(None, description="Foreign ID of event") location: Optional[str] = Field(None, description="Event location") - attendees: List[CalendarEventAttendee] = Field( + attendees: List[CalendarEventAttendeeResponse] = Field( [], description="Event attendees (participants)", ) @@ -122,6 +128,10 @@ class ApplicantLogCalendarEvent(BaseModel): status: CalendarEventStatus = Field(..., description="Event status") transparency: Transparency = Field(..., description="Event transparency (availability)") recurrence: Optional[List] = None + private: Optional[bool] = Field( + None, + description="Flag indicating private visibility of the event", + ) class ApplicantLogEmailResponse(BaseModel): @@ -168,6 +178,10 @@ class ApplicantLogItem(BaseModel): None, description="Survey questionary", ) + survey_answer_of_type_a: Optional[ApplicantLogSurveyAnswerTypeA] = Field( + None, + description="Survey answer of type A object", + ) class ApplicantLogResponse(PaginatedResponse): diff --git a/huntflow_api_client/models/response/applicants.py b/huntflow_api_client/models/response/applicants.py index e4eacc0..13724dd 100644 --- a/huntflow_api_client/models/response/applicants.py +++ b/huntflow_api_client/models/response/applicants.py @@ -1,10 +1,10 @@ from datetime import date, datetime from typing import List, Optional, Union -from pydantic import BaseModel, ConfigDict, EmailStr, Field, PositiveInt +from pydantic import BaseModel, ConfigDict, Field, PositiveInt from huntflow_api_client.models.common import Applicant, PaginatedResponse -from huntflow_api_client.models.consts import AgreementState as AgreementStateEnum +from huntflow_api_client.models.consts import AgreementStateResponse as AgreementStateEnum class ApplicantTag(BaseModel): @@ -15,6 +15,7 @@ class ApplicantTag(BaseModel): class ApplicantLink(BaseModel): id: Optional[int] = Field(None, description="Link ID") status: int = Field(..., description="Vacancy status ID") + rejection_reason: Optional[int] = Field(None, description="Rejection reason ID") updated: datetime = Field( ..., description="The date of the applicant's update at a vacancy", @@ -61,6 +62,12 @@ class ApplicantSocial(BaseModel): verification_date: Optional[datetime] = Field(None, description="Verification date") +class ApplicantSite(BaseModel): + id: PositiveInt = Field(..., description="Site ID") + site_type: str = Field(..., description="Type") + value: str = Field(..., description="Value") + + class ApplicantItem(Applicant): id: int = Field(..., description="Applicant ID") account: int = Field(..., description="Organization ID") @@ -73,7 +80,7 @@ class ApplicantItem(Applicant): None, description="Date and time of adding an applicant", ) - email: Union[EmailStr, str, None] = Field( + email: Union[str, str, None] = Field( None, description="Email address", ) @@ -86,6 +93,7 @@ class ApplicantItem(Applicant): ) doubles: List[ApplicantDouble] = Field(..., description="List of duplicates") social: List[ApplicantSocial] = Field(..., description="List of applicant's social accounts") + site: List[ApplicantSite] = Field(..., description="List of applicant's sites") class ApplicantListResponse(PaginatedResponse): @@ -111,6 +119,7 @@ class ApplicantCreateResponse(Applicant): ) external: List[ApplicantResume] = Field(..., description="Applicant's resume") social: List[ApplicantSocial] = Field(..., description="List of applicant's social accounts") + site: List[ApplicantSite] = Field(..., description="List of applicant's sites") class ApplicantSearchItem(BaseModel): @@ -121,7 +130,7 @@ class ApplicantSearchItem(BaseModel): birthday: Optional[date] = Field(None, description="Date of birth") phone: Optional[str] = Field(None, description="Phone number") skype: Optional[str] = Field(None, description="Skype login") - email: Union[EmailStr, str, None] = Field(None, description="Email address") + email: Union[str, str, None] = Field(None, description="Email address") money: Optional[str] = Field(None, description="Salary expectation") position: Optional[str] = Field(None, description="Candidate’s occupation") company: Optional[str] = Field(None, description="Candidate’s place of work") @@ -133,3 +142,12 @@ class ApplicantSearchItem(BaseModel): class ApplicantSearchByCursorResponse(BaseModel): items: List[ApplicantSearchItem] = Field(..., description="List of applicants") next_page_cursor: Optional[str] = Field(None, description="Next page cursor") + + +class ApplicantCreateAgreementLinkResponse(BaseModel): + link: str = Field(..., description="Link to agreement") + + +class ApplicantSendAgreementResponse(BaseModel): + sent_to: str = Field(..., description="Email recipient") + job_id: str = Field(..., description="Job ID") diff --git a/huntflow_api_client/models/response/coworkers.py b/huntflow_api_client/models/response/coworkers.py index d9b0dd9..d1b5636 100644 --- a/huntflow_api_client/models/response/coworkers.py +++ b/huntflow_api_client/models/response/coworkers.py @@ -1,6 +1,6 @@ from typing import List, Optional -from pydantic import BaseModel, ConfigDict, EmailStr, Field +from pydantic import BaseModel, ConfigDict, Field from huntflow_api_client.models.common import PaginatedResponse from huntflow_api_client.models.consts import MemberType @@ -18,7 +18,7 @@ class CoworkerResponse(BaseModel): name: Optional[str] = Field(None, description="Coworker name") type: MemberType = Field(..., description="Coworker type (role)") head: Optional[int] = Field(None, description="Head user ID") - email: Optional[EmailStr] = Field(None, description="Email") + email: Optional[str] = Field(None, description="Email") meta: Optional[dict] = Field(None, description="Additional meta information") permissions: List[Permission] = Field(default_factory=list, description="Coworker permissions") diff --git a/huntflow_api_client/models/response/delayed_tasks.py b/huntflow_api_client/models/response/delayed_tasks.py index a99b269..6c55094 100644 --- a/huntflow_api_client/models/response/delayed_tasks.py +++ b/huntflow_api_client/models/response/delayed_tasks.py @@ -38,3 +38,4 @@ class DelayedTaskResponse(BaseModel): description="Date and time of the last task update (ISO 8601)", ) states_log: t.List[TaskLog] = Field(..., description="Task change log") + result: t.Optional[t.Dict] = Field(None, description="Task execution result") diff --git a/huntflow_api_client/models/response/dictionaries.py b/huntflow_api_client/models/response/dictionaries.py index 91976ee..94508f5 100644 --- a/huntflow_api_client/models/response/dictionaries.py +++ b/huntflow_api_client/models/response/dictionaries.py @@ -29,6 +29,10 @@ class DictionaryItem(BaseModel): description="The unique identifier in the customer's internal system", ) created: datetime = Field(..., description="Date and time of creating a dictionary") + last_sync: Optional[datetime] = Field( + None, + description="Date and time of last dictionary synchronization", + ) class DictionariesListResponse(BaseModel): @@ -61,4 +65,8 @@ class DictionaryResponse(BaseModel): description="The unique identifier in the customer's internal system", ) created: datetime = Field(..., description="Date and time of creating a dictionary") + last_sync: Optional[datetime] = Field( + None, + description="Date and time of last dictionary synchronization", + ) fields: List[DictionaryField] = Field(..., description="List of dictionary fields") diff --git a/huntflow_api_client/models/response/email_templates.py b/huntflow_api_client/models/response/email_templates.py index cd7baca..f69c9c3 100644 --- a/huntflow_api_client/models/response/email_templates.py +++ b/huntflow_api_client/models/response/email_templates.py @@ -1,13 +1,13 @@ import typing as t -from pydantic import BaseModel, EmailStr, Field, PositiveInt +from pydantic import BaseModel, Field, PositiveInt from huntflow_api_client.models.common import EmailFollowup, File class MailTemplateAttendee(BaseModel): type: str = Field(..., description="Attendee type") - email: EmailStr = Field(..., description="Attendee email") + email: str = Field(..., description="Attendee email") class MailTemplateDivision(BaseModel): diff --git a/huntflow_api_client/models/response/interview_types.py b/huntflow_api_client/models/response/interview_types.py new file mode 100644 index 0000000..a0c403e --- /dev/null +++ b/huntflow_api_client/models/response/interview_types.py @@ -0,0 +1,21 @@ +from typing import List + +from pydantic import BaseModel, Field + +from huntflow_api_client.models.consts import InterviewType as InterviewTypeEnum + + +class InterviewType(BaseModel): + id: int = Field(..., description="Interview type ID", examples=[20]) + name: str = Field(..., description="Interview type name", examples=["Phone interview"]) + account: int = Field(..., description="Organization ID", examples=[42]) + order: int = Field(..., description="Order number") + type: InterviewTypeEnum = Field( + ..., + description="Type of the interview", + examples=[InterviewTypeEnum.USER], + ) + + +class InterviewTypesListResponse(BaseModel): + items: List[InterviewType] diff --git a/huntflow_api_client/models/response/recommendation.py b/huntflow_api_client/models/response/recommendation.py new file mode 100644 index 0000000..4fe337d --- /dev/null +++ b/huntflow_api_client/models/response/recommendation.py @@ -0,0 +1,34 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel, Field + +from huntflow_api_client.models.consts import RecommendationStatus + + +class RecommendationItem(BaseModel): + id: int = Field(..., description="Recommendation ID") + vacancy_id: int = Field(..., description="Vacancy ID") + applicant_id: int = Field(..., description="Applicant ID") + rank: int = Field(..., description="Position of the recommendation in the ranking list.") + created_at: datetime = Field( + ..., + description="Date and time when the recommendation was created.", + ) + updated_at: datetime = Field( + ..., + description="Date and time when the recommendation was last updated.", + ) + resolved_by_user: Optional[int] = Field( + None, + description="ID of the recruiter who resolved recommendation. null if not processed yet.", + ) + status: Optional[RecommendationStatus] = Field( + None, + description="Current status of the recommendation. null if not processed yet.", + ) + + +class RecommendationListResponse(BaseModel): + items: List[RecommendationItem] + next_page_cursor: Optional[str] = None diff --git a/huntflow_api_client/models/response/survey.py b/huntflow_api_client/models/response/survey.py index e985098..7588e1a 100644 --- a/huntflow_api_client/models/response/survey.py +++ b/huntflow_api_client/models/response/survey.py @@ -65,3 +65,28 @@ class SurveyQuestionaryAnswerResponse(BaseModel): respondent: SurveyQuestionaryRespondentWithName survey_questionary: SurveyQuestionaryCreatedInfo data: dict = Field(..., description="Answer data") + + +class SurveySchemasTypeAListResponse(BaseModel): + items: List[BaseSurveySchemaType] = Field(..., description="List of type a survey schemas") + + +class SurveySchemaTypeAResponse(BaseSurveySchemaTypeWithSchemas): + type: SurveyType = Field( + SurveyType.TYPE_A, + description="Type of survey", + frozen=True, + ) + + +class SurveyTypeARespondent(BaseModel): + account_id: int = Field(..., description="Account ID") + name: str = Field(..., description="Name of the user who created the survey answer") + + +class SurveyAnswerTypeAResponse(BaseModel): + id: int = Field(..., description="Survey answer of type A ID") + created: datetime.datetime = Field(..., description="Date and time of creating an answer") + survey: SurveySchemaTypeAResponse = Field(..., description="Survey schema") + respondent: SurveyTypeARespondent = Field(..., description="Who created the survey answer") + data: dict = Field(..., description="Answer data") diff --git a/huntflow_api_client/models/response/users.py b/huntflow_api_client/models/response/users.py index 2e90fa2..37eefb6 100644 --- a/huntflow_api_client/models/response/users.py +++ b/huntflow_api_client/models/response/users.py @@ -1,6 +1,6 @@ from typing import List, Optional -from pydantic import BaseModel, ConfigDict, EmailStr, Field +from pydantic import BaseModel, ConfigDict, Field from huntflow_api_client.models.consts import MemberType from huntflow_api_client.models.response.coworkers import Permission @@ -11,7 +11,7 @@ class UserResponse(BaseModel): name: Optional[str] = Field(None, description="User name") type: MemberType = Field(..., description="User type (role)") head: Optional[int] = Field(None, description="Head user ID") - email: Optional[EmailStr] = Field(None, description="Email") + email: Optional[str] = Field(None, description="Email") meta: Optional[dict] = Field(None, description="Additional meta information") permissions: List[Permission] = Field(default_factory=list, description="User permissions") diff --git a/huntflow_api_client/models/response/vacancies.py b/huntflow_api_client/models/response/vacancies.py index d213751..e827503 100644 --- a/huntflow_api_client/models/response/vacancies.py +++ b/huntflow_api_client/models/response/vacancies.py @@ -81,7 +81,7 @@ class VacancyChild(VacancyItem): class VacancyResponse(VacancyChild): blocks: Optional[List[VacancyChild]] = Field( - [], + None, description="Affiliate vacancies if vacancy is a multiple", ) diff --git a/huntflow_api_client/models/response/vacancy_requests.py b/huntflow_api_client/models/response/vacancy_requests.py index 7ea8386..8121ca7 100644 --- a/huntflow_api_client/models/response/vacancy_requests.py +++ b/huntflow_api_client/models/response/vacancy_requests.py @@ -1,7 +1,7 @@ import datetime import typing as t -from pydantic import BaseModel, EmailStr, Field, PositiveInt +from pydantic import BaseModel, Field, PositiveInt from huntflow_api_client.models.common import File, PaginatedResponse from huntflow_api_client.models.consts import VacancyRequestStatus @@ -16,14 +16,14 @@ class UserInfo(BaseModel): ..., description="Name of coworker who create the vacancy request", ) - email: EmailStr = Field(..., description="Email of coworker who create the vacancy request") + email: str = Field(..., description="Email of coworker who create the vacancy request") class VacancyRequestApprovalState(BaseModel): id: PositiveInt = Field(..., description="Approval ID") status: VacancyRequestStatus = Field(..., description="Approval status") - email: EmailStr = Field( + email: str = Field( ..., description="Email, which was used to send the request for approval", ) diff --git a/huntflow_api_client/models/response/webhooks.py b/huntflow_api_client/models/response/webhooks.py index f21e96d..7806d74 100644 --- a/huntflow_api_client/models/response/webhooks.py +++ b/huntflow_api_client/models/response/webhooks.py @@ -3,7 +3,7 @@ from pydantic import AnyHttpUrl, BaseModel, Field, PositiveInt -from huntflow_api_client.models.consts import WebhookEvent +from huntflow_api_client.models.consts import WebhookEvent, WebhookType class WebhookResponse(BaseModel): @@ -13,6 +13,7 @@ class WebhookResponse(BaseModel): created: datetime = Field(..., description="Date and time of creating a webhook") active: bool = Field(..., description="Webhook activity flag") webhook_events: List[WebhookEvent] = Field(..., description="List of webhook events") + type: WebhookType = Field(..., description="Webhook type") class WebhooksListResponse(BaseModel): diff --git a/huntflow_api_client/utils.py b/huntflow_api_client/utils.py new file mode 100644 index 0000000..6cb6c3e --- /dev/null +++ b/huntflow_api_client/utils.py @@ -0,0 +1,16 @@ +from enum import Enum +from typing import Any, Callable, Dict, Type, TypeVar + +T = TypeVar("T", bound=Enum) + + +def extend_enum(inherited_enum: Type[T]) -> Callable: + def wrapper(added_enum: Type[T]) -> Enum: + joined: Dict[str, Any] = {} + for item in inherited_enum: + joined[item.name] = item.value + for item in added_enum: + joined[item.name] = item.value + return Enum(added_enum.__name__, joined) + + return wrapper diff --git a/pdm.lock b/pdm.lock index f0d1e85..1797014 100644 --- a/pdm.lock +++ b/pdm.lock @@ -4,25 +4,28 @@ [metadata] groups = ["default", "lint", "release", "test"] strategy = ["cross_platform"] -lock_version = "4.4.1" -content_hash = "sha256:c41d63f0f442711a2033d6579af1aa0c3a6957cc346e2ca59ea2533f7a644a19" +lock_version = "4.5.0" +content_hash = "sha256:88fc9b3b2cc1a67ef5b07cef6a0416a9d012a3baba79ecb5d5ce69016444fb0d" + +[[metadata.targets]] +requires_python = ">=3.8.1" [[package]] name = "annotated-types" -version = "0.6.0" +version = "0.7.0" requires_python = ">=3.8" summary = "Reusable constraint types to use with typing.Annotated" dependencies = [ "typing-extensions>=4.0.0; python_version < \"3.9\"", ] files = [ - {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, - {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] [[package]] name = "anyio" -version = "4.3.0" +version = "4.5.2" requires_python = ">=3.8" summary = "High level compatibility layer for multiple asynchronous event loop implementations" dependencies = [ @@ -32,23 +35,23 @@ dependencies = [ "typing-extensions>=4.1; python_version < \"3.11\"", ] files = [ - {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, - {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, ] [[package]] name = "attrs" -version = "23.2.0" -requires_python = ">=3.7" +version = "25.3.0" +requires_python = ">=3.8" summary = "Classes Without Boilerplate" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [[package]] name = "black" -version = "24.3.0" +version = "24.8.0" requires_python = ">=3.8" summary = "The uncompromising code formatter." dependencies = [ @@ -61,51 +64,52 @@ dependencies = [ "typing-extensions>=4.0.1; python_version < \"3.11\"", ] files = [ - {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, - {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, - {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, - {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, - {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, - {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, - {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, - {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, - {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, - {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, - {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, - {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, - {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, - {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, - {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, - {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, - {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, - {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, - {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, - {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, - {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, - {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, ] [[package]] name = "certifi" -version = "2024.2.2" +version = "2025.1.31" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." files = [ - {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, - {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" requires_python = ">=3.7" summary = "Composable command line interface toolkit" dependencies = [ "colorama; platform_system == \"Windows\"", + "importlib-metadata; python_version < \"3.8\"", ] files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [[package]] @@ -130,7 +134,7 @@ files = [ [[package]] name = "email-validator" -version = "2.1.1" +version = "2.2.0" requires_python = ">=3.8" summary = "A robust email address syntax and deliverability validation library." dependencies = [ @@ -138,85 +142,86 @@ dependencies = [ "idna>=2.0.0", ] files = [ - {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, - {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, ] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.2" requires_python = ">=3.7" summary = "Backport of PEP 654 (exception groups)" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [[package]] name = "flake8" -version = "7.0.0" +version = "7.1.2" requires_python = ">=3.8.1" summary = "the modular source code checker: pep8 pyflakes and co" dependencies = [ "mccabe<0.8.0,>=0.7.0", - "pycodestyle<2.12.0,>=2.11.0", + "pycodestyle<2.13.0,>=2.12.0", "pyflakes<3.3.0,>=3.2.0", ] files = [ - {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, - {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, + {file = "flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a"}, + {file = "flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd"}, ] [[package]] name = "flake8-bugbear" -version = "24.2.6" +version = "24.12.12" requires_python = ">=3.8.1" summary = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." dependencies = [ - "attrs>=19.2.0", + "attrs>=22.2.0", "flake8>=6.0.0", ] files = [ - {file = "flake8-bugbear-24.2.6.tar.gz", hash = "sha256:f9cb5f2a9e792dd80ff68e89a14c12eed8620af8b41a49d823b7a33064ac9658"}, - {file = "flake8_bugbear-24.2.6-py3-none-any.whl", hash = "sha256:663ef5de80cd32aacd39d362212983bc4636435a6f83700b4ed35acbd0b7d1b8"}, + {file = "flake8_bugbear-24.12.12-py3-none-any.whl", hash = "sha256:1b6967436f65ca22a42e5373aaa6f2d87966ade9aa38d4baf2a1be550767545e"}, + {file = "flake8_bugbear-24.12.12.tar.gz", hash = "sha256:46273cef0a6b6ff48ca2d69e472f41420a42a46e24b2a8972e4f0d6733d12a64"}, ] [[package]] name = "flake8-builtins" -version = "2.2.0" +version = "2.5.0" requires_python = ">=3.8" summary = "Check for python builtins being used as variables or parameters" dependencies = [ "flake8", ] files = [ - {file = "flake8_builtins-2.2.0-py3-none-any.whl", hash = "sha256:7ee5766d9c60e5d579dfda84e65c6d0e6c26005f6f59cb9bf722462d7987a807"}, - {file = "flake8_builtins-2.2.0.tar.gz", hash = "sha256:392d5af3a0720c5a863aa93dc47f48c879081345a143fe9f20d995fe9ff5686a"}, + {file = "flake8_builtins-2.5.0-py3-none-any.whl", hash = "sha256:8cac7c52c6f0708c0902b46b385bc7e368a9068965083796f1431c0d2e6550cf"}, + {file = "flake8_builtins-2.5.0.tar.gz", hash = "sha256:bdaa3dd823e4f5308c5e712d19fa5f69daa52781ea874f5ea9c3637bcf56faa6"}, ] [[package]] name = "flake8-commas" -version = "2.1.0" +version = "4.0.0" +requires_python = ">=3.8" summary = "Flake8 lint for trailing commas." dependencies = [ - "flake8>=2", + "flake8>=5", ] files = [ - {file = "flake8-commas-2.1.0.tar.gz", hash = "sha256:940441ab8ee544df564ae3b3f49f20462d75d5c7cac2463e0b27436e2050f263"}, - {file = "flake8_commas-2.1.0-py2.py3-none-any.whl", hash = "sha256:ebb96c31e01d0ef1d0685a21f3f0e2f8153a0381430e748bf0bbbb5d5b453d54"}, + {file = "flake8_commas-4.0.0-py3-none-any.whl", hash = "sha256:cad476d71ba72e8b941a8508d5b9ffb6b03e50f7102982474f085ad0d674b685"}, + {file = "flake8_commas-4.0.0.tar.gz", hash = "sha256:a68834b42a9a31c94ca790efe557a932c0eae21a3479c6b9a23c4dc077e3ea96"}, ] [[package]] name = "flake8-comprehensions" -version = "3.14.0" +version = "3.15.0" requires_python = ">=3.8" summary = "A flake8 plugin to help you write better list/set/dict comprehensions." dependencies = [ - "flake8!=3.2.0,>=3.0", + "flake8!=3.2,>=3", ] files = [ - {file = "flake8_comprehensions-3.14.0-py3-none-any.whl", hash = "sha256:7b9d07d94aa88e62099a6d1931ddf16c344d4157deedf90fe0d8ee2846f30e97"}, - {file = "flake8_comprehensions-3.14.0.tar.gz", hash = "sha256:81768c61bfc064e1a06222df08a2580d97de10cb388694becaf987c331c6c0cf"}, + {file = "flake8_comprehensions-3.15.0-py3-none-any.whl", hash = "sha256:b7e027bbb52be2ceb779ee12484cdeef52b0ad3c1fcb8846292bdb86d3034681"}, + {file = "flake8_comprehensions-3.15.0.tar.gz", hash = "sha256:923c22603e0310376a6b55b03efebdc09753c69f2d977755cba8bb73458a5d4d"}, ] [[package]] @@ -243,15 +248,15 @@ files = [ [[package]] name = "freezegun" -version = "1.4.0" +version = "1.5.1" requires_python = ">=3.7" summary = "Let your Python tests travel through time" dependencies = [ "python-dateutil>=2.7", ] files = [ - {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"}, - {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"}, + {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, + {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, ] [[package]] @@ -259,6 +264,9 @@ name = "h11" version = "0.14.0" requires_python = ">=3.7" summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -298,22 +306,22 @@ files = [ [[package]] name = "idna" -version = "3.6" -requires_python = ">=3.5" +version = "3.10" +requires_python = ">=3.6" summary = "Internationalized Domain Names in Applications (IDNA)" files = [ - {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, - {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [[package]] name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" +version = "2.1.0" +requires_python = ">=3.8" summary = "brain-dead simple config-ini parsing" files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -338,42 +346,53 @@ files = [ [[package]] name = "mypy" -version = "1.9.0" +version = "1.14.1" requires_python = ">=3.8" summary = "Optional static typing for Python" dependencies = [ "mypy-extensions>=1.0.0", "tomli>=1.1.0; python_version < \"3.11\"", - "typing-extensions>=4.1.0", -] -files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + "typing-extensions>=4.6.0", +] +files = [ + {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, + {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, + {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, + {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, + {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, + {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, + {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, + {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, + {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, + {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, + {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, + {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, + {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, + {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, + {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, + {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, + {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, + {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, + {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, + {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, + {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, + {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, + {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, + {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, + {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, + {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, + {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, ] [[package]] @@ -388,12 +407,12 @@ files = [ [[package]] name = "packaging" -version = "24.0" -requires_python = ">=3.7" +version = "24.2" +requires_python = ">=3.8" summary = "Core utilities for Python packages" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] @@ -408,137 +427,158 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.0" +version = "4.3.6" requires_python = ">=3.8" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" requires_python = ">=3.8" summary = "plugin and hook calling mechanisms for python" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [[package]] name = "pycodestyle" -version = "2.11.1" +version = "2.12.1" requires_python = ">=3.8" summary = "Python style guide checker" files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] [[package]] name = "pydantic" -version = "2.6.4" +version = "2.10.6" requires_python = ">=3.8" summary = "Data validation using Python type hints" dependencies = [ - "annotated-types>=0.4.0", - "pydantic-core==2.16.3", - "typing-extensions>=4.6.1", + "annotated-types>=0.6.0", + "pydantic-core==2.27.2", + "typing-extensions>=4.12.2", ] files = [ - {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, - {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, ] [[package]] name = "pydantic-core" -version = "2.16.3" +version = "2.27.2" requires_python = ">=3.8" -summary = "" +summary = "Core functionality for Pydantic validation and serialization" dependencies = [ "typing-extensions!=4.7.0,>=4.6.0", ] files = [ - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, - {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, - {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, - {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, - {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, - {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, - {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, - {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, - {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, - {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, - {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, - {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, - {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, - {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, - {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, - {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, - {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, - {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, - {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, - {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, - {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, - {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, - {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, - {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, - {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, - {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, - {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, - {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, - {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, - {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, ] [[package]] @@ -559,6 +599,7 @@ summary = "pytest: simple powerful testing with Python" dependencies = [ "colorama; sys_platform == \"win32\"", "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "importlib-metadata>=0.12; python_version < \"3.8\"", "iniconfig", "packaging", "pluggy<2.0,>=0.12", @@ -571,15 +612,15 @@ files = [ [[package]] name = "pytest-asyncio" -version = "0.23.6" +version = "0.23.8" requires_python = ">=3.8" summary = "Pytest support for asyncio" dependencies = [ "pytest<9,>=7.0.0", ] files = [ - {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, - {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, ] [[package]] @@ -624,22 +665,22 @@ files = [ [[package]] name = "setuptools" -version = "69.2.0" +version = "75.3.2" requires_python = ">=3.8" summary = "Easily download, build, install, upgrade, and uninstall Python packages" files = [ - {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, - {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, + {file = "setuptools-75.3.2-py3-none-any.whl", hash = "sha256:90ab613b6583fc02d5369cbca13ea26ea0e182d1df2d943ee9cbe81d4c61add9"}, + {file = "setuptools-75.3.2.tar.gz", hash = "sha256:3c1383e1038b68556a382c1e8ded8887cd20141b0eb5708a6c8d277de49364f5"}, ] [[package]] name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" summary = "Python 2 and 3 compatibility utilities" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -664,20 +705,50 @@ files = [ [[package]] name = "tomli" -version = "2.0.1" -requires_python = ">=3.7" +version = "2.2.1" +requires_python = ">=3.8" summary = "A lil' TOML parser" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.13.2" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, ] diff --git a/pyproject.toml b/pyproject.toml index cdb2753..ef5a8a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "huntflow-api-client" -version = "0.1.4" +version = "2.13.7" description = "Huntflow API Client for Python" authors = [ {name = "Developers huntflow", email = "developer@huntflow.ru"}, @@ -22,6 +22,10 @@ classifiers = [ "Programming Language :: Python :: 3.11", ] +[project.urls] +Documentation = "https://api.huntflow.ai/v2/docs" +Repository = "https://github.com/huntflow/huntflow-api-client-python" + [build-system] requires = ["pdm-backend"] build-backend = "pdm.backend" diff --git a/tests/test_entities/test_applicants.py b/tests/test_entities/test_applicants.py index 1e46cb7..d5507c5 100644 --- a/tests/test_entities/test_applicants.py +++ b/tests/test_entities/test_applicants.py @@ -1,3 +1,4 @@ +import json from typing import Any, Dict from pytest_httpx import HTTPXMock @@ -9,10 +10,12 @@ ApplicantUpdateRequest, ) from huntflow_api_client.models.response.applicants import ( + ApplicantCreateAgreementLinkResponse, ApplicantCreateResponse, ApplicantItem, ApplicantListResponse, ApplicantSearchByCursorResponse, + ApplicantSendAgreementResponse, ) from huntflow_api_client.tokens.proxy import HuntflowTokenProxy from tests.api import BASE_URL, VERSIONED_BASE_URL @@ -55,6 +58,7 @@ "agreement": None, "doubles": [], "social": [], + "site": [], }, { "first_name": "Test", @@ -93,6 +97,7 @@ "agreement": {"state": None, "decision_date": None}, "doubles": [], "social": [], + "site": [], }, ], } @@ -141,6 +146,13 @@ "verification_date": "2020-01-01T00:00:00+03:00", }, ], + "site": [ + { + "id": 1, + "site_type": "MAX", + "value": "https://max.ru/u/1xBeccJnjIvmlJflCNJjZOVtCJXNzrciOTWpebTSQLPspjUoBgj", + }, + ], } APPLICANT_CREATE_REQUEST: Dict[str, Any] = { "first_name": "John", @@ -163,6 +175,12 @@ }, ], "social": [{"social_type": "TELEGRAM", "value": "TelegramUsername"}], + "site": [ + { + "site_type": "MAX", + "value": "https://max.ru/u/1xBeccJnjIvmlJflCNJjZOVtCJXNzrciOTWpebTSQLPspjUoBgj", + }, + ], } APPLICANT_CREATE_RESPONSE: Dict[str, Any] = { "first_name": "John", @@ -198,8 +216,15 @@ "verification_date": "2020-01-01T00:00:00+03:00", }, ], + "site": [ + { + "id": 1, + "site_type": "MAX", + "value": "https://max.ru/u/1xBeccJnjIvmlJflCNJjZOVtCJXNzrciOTWpebTSQLPspjUoBgj", + }, + ], } -APPLICANT_PATCH_REQUEST: Dict[str, Any] = {"first_name": "Newname"} +APPLICANT_PATCH_REQUEST: Dict[str, Any] = {"first_name": "Newname", "social": []} APPLICANT_PATCH_RESPONSE: Dict[str, Any] = { "first_name": "Newname", "last_name": "Doe", @@ -245,6 +270,7 @@ "verification_date": "2020-01-01T00:00:00+03:00", }, ], + "site": [], } APPLICANT_SEARCH_BY_CURSOR_RESPONSE = { @@ -269,6 +295,12 @@ "next_page_cursor": "3VudCI6IjoXIjogW10IiwgMy4wXX0=", } +APPLICANT_CREATE_AGREEMENT_RESPONSE: Dict[str, Any] = {"link": "https://agreement.example/1"} +APPLICANT_SEND_AGREEMENT_RESPONSE: Dict[str, Any] = { + "sent_to": "user@example.com", + "job_id": "job-id-1111", +} + async def test_list_applicant( httpx_mock: HTTPXMock, @@ -324,6 +356,7 @@ async def test_patch_applicant( ) -> None: httpx_mock.add_response( url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/applicants/{APPLICANT_ID}", + match_content=json.dumps(APPLICANT_PATCH_REQUEST).encode(), json=APPLICANT_PATCH_RESPONSE, ) api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) @@ -385,3 +418,35 @@ async def test_applicant_search_by_cursor( assert response == ApplicantSearchByCursorResponse.model_validate( APPLICANT_SEARCH_BY_CURSOR_RESPONSE, ) + + +async def test_create_agreement_link( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/applicants/{APPLICANT_ID}/agreement_link", + json=APPLICANT_CREATE_AGREEMENT_RESPONSE, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + + applicants = Applicant(api_client) + + response = await applicants.create_agreement_link(ACCOUNT_ID, APPLICANT_ID) + assert response == ApplicantCreateAgreementLinkResponse(**APPLICANT_CREATE_AGREEMENT_RESPONSE) + + +async def test_send_agreement_via_email( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/applicants/{APPLICANT_ID}/agreement_email", + json=APPLICANT_SEND_AGREEMENT_RESPONSE, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + + applicants = Applicant(api_client) + + response = await applicants.send_agreement_via_email(ACCOUNT_ID, APPLICANT_ID) + assert response == ApplicantSendAgreementResponse(**APPLICANT_SEND_AGREEMENT_RESPONSE) diff --git a/tests/test_entities/test_organization_settings.py b/tests/test_entities/test_organization_settings.py index 95887dd..b808607 100644 --- a/tests/test_entities/test_organization_settings.py +++ b/tests/test_entities/test_organization_settings.py @@ -4,6 +4,7 @@ from huntflow_api_client import HuntflowAPI from huntflow_api_client.entities.organization_settings import OrganizationSettings +from huntflow_api_client.models.response.interview_types import InterviewTypesListResponse from huntflow_api_client.models.response.organization_settings import ( BaseSurveySchemaTypeWithSchemas, CloseReasonsListResponse, @@ -27,6 +28,17 @@ "schema": {}, "ui_schema": {}, } +INTERVIEW_TYPES_RESPONSE: Dict[str, Any] = { + "items": [ + { + "id": 20, + "name": "Phone interview", + "account": 42, + "order": 0, + "type": "user", + }, + ], +} async def test_get_hold_reasons( @@ -72,3 +84,18 @@ async def test_get_applicant_survey_form( response = await settings.get_applicant_survey_form(ACCOUNT_ID, SURVEY_ID) assert response == BaseSurveySchemaTypeWithSchemas(**SURVEY_FORM_RESPONSE) + + +async def test_get_interview_types( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/interview_types", + json=INTERVIEW_TYPES_RESPONSE, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + settings = OrganizationSettings(api_client) + + response = await settings.get_interview_types(ACCOUNT_ID) + assert response == InterviewTypesListResponse(**INTERVIEW_TYPES_RESPONSE) diff --git a/tests/test_entities/test_production_calendar.py b/tests/test_entities/test_production_calendar.py index 6c1eb4f..815819e 100644 --- a/tests/test_entities/test_production_calendar.py +++ b/tests/test_entities/test_production_calendar.py @@ -110,7 +110,7 @@ async def test_get_non_working_days_in_period( ) -> None: httpx_mock.add_response( url=f"{VERSIONED_BASE_URL}/production_calendars/{CALENDAR_ID}/days/" - f"{DEADLINE_DATE.strftime('%Y-%m-%d')}?verbose=true", + f"{DEADLINE_DATE.strftime('%Y-%m-%d')}?verbose=false", json=NON_WORKING_DAYS_GET_RESPONSE, ) api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) diff --git a/tests/test_entities/test_recommendation.py b/tests/test_entities/test_recommendation.py new file mode 100644 index 0000000..4987db8 --- /dev/null +++ b/tests/test_entities/test_recommendation.py @@ -0,0 +1,62 @@ +from datetime import datetime + +from pytest_httpx import HTTPXMock + +from huntflow_api_client import HuntflowAPI +from huntflow_api_client.entities.recommendation import Recommendation +from huntflow_api_client.models.consts import RecommendationProcessingStatus +from huntflow_api_client.models.response.recommendation import RecommendationListResponse +from huntflow_api_client.tokens.proxy import HuntflowTokenProxy +from tests.api import BASE_URL, VERSIONED_BASE_URL + +ACCOUNT_ID = 1 +VACANCY_ID = 2 +RECOMMENDATION_LIST_RESPONSE = { + "items": [ + { + "id": 3, + "vacancy_id": VACANCY_ID, + "applicant_id": 4, + "rank": 5, + "created_at": datetime.now().isoformat(), + "updated_at": datetime.now().isoformat(), + "resolved_by_user": 6, + "status": "TAKEN", + }, + ], + "next_page_cursor": "any", +} + + +async def test_list_recommendation( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=( + f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/recommendations/{VACANCY_ID}" + f"?count=1&processing_status=PROCESSED" + ), + json=RECOMMENDATION_LIST_RESPONSE, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + recommendations = Recommendation(api_client) + + response = await recommendations.list( + ACCOUNT_ID, + VACANCY_ID, + count=1, + processing_status=RecommendationProcessingStatus.PROCESSED, + ) + assert response == RecommendationListResponse.model_validate(RECOMMENDATION_LIST_RESPONSE) + + next_page_cursor = "cursor" + httpx_mock.add_response( + url=( + f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/recommendations/{VACANCY_ID}" + f"?next_page_cursor={next_page_cursor}&processing_status=ALL" + ), + json=RECOMMENDATION_LIST_RESPONSE, + ) + response = await recommendations.list(ACCOUNT_ID, VACANCY_ID, next_page_cursor=next_page_cursor) + assert response == RecommendationListResponse.model_validate(RECOMMENDATION_LIST_RESPONSE) diff --git a/tests/test_entities/test_survey_type_a.py b/tests/test_entities/test_survey_type_a.py new file mode 100644 index 0000000..4be42c4 --- /dev/null +++ b/tests/test_entities/test_survey_type_a.py @@ -0,0 +1,109 @@ +from pytest_httpx import HTTPXMock + +from huntflow_api_client import HuntflowAPI +from huntflow_api_client.entities.survey_type_a import SurveyTypeA +from huntflow_api_client.models.response.survey import ( + SurveyAnswerTypeAResponse, + SurveySchemasTypeAListResponse, + SurveySchemaTypeAResponse, +) +from huntflow_api_client.tokens.proxy import HuntflowTokenProxy +from tests.api import BASE_URL, VERSIONED_BASE_URL + +ACCOUNT_ID = 1 + +SURVEY_FEEDBACK_SCHEMAS_LIST_RESPONSE = { + "items": [ + { + "id": 1, + "name": "test_survey", + "type": "type_a", + "active": True, + "created": "2020-01-01T00:00:00+03:00", + "updated": "2020-01-01T00:00:00+03:00", + }, + ], +} + +SURVEY_FEEDBACK_SCHEMA_RESPONSE = { + "id": 1, + "name": "test_survey", + "type": "type_a", + "active": True, + "created": "2020-01-01T00:00:00+03:00", + "updated": "2020-01-01T00:00:00+03:00", + "schema": {}, + "ui_schema": {}, +} + +SURVEY_ANSWER_RESPONSE = { + "id": 1, + "created": "2020-01-01T00:00:00+03:00", + "survey": { + "id": 1, + "name": "test_survey", + "type": "type_a", + "active": True, + "created": "2020-01-01T00:00:00+03:00", + "updated": "2020-01-01T00:00:00+03:00", + "schema": {}, + "ui_schema": {}, + }, + "respondent": {"account_id": 1, "name": "John Joe"}, + "data": {}, +} + + +async def test_list( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/surveys/type_a?active=true", + json=SURVEY_FEEDBACK_SCHEMAS_LIST_RESPONSE, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + feedback = SurveyTypeA(api_client) + + response = await feedback.list(ACCOUNT_ID) + assert response == SurveySchemasTypeAListResponse.model_validate( + SURVEY_FEEDBACK_SCHEMAS_LIST_RESPONSE, + ) + + +async def test_get( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + survey_id = 1 + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/surveys/type_a/{survey_id}", + json=SURVEY_FEEDBACK_SCHEMA_RESPONSE, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + feedback = SurveyTypeA(api_client) + + response = await feedback.get(ACCOUNT_ID, survey_id) + assert response == SurveySchemaTypeAResponse.model_validate( + SURVEY_FEEDBACK_SCHEMA_RESPONSE, + ) + + +async def test_get_applicant_answer( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + survey_id = 1 + answer_id = 2 + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/surveys/type_a/" + f"{survey_id}/answers/{answer_id}", + json=SURVEY_ANSWER_RESPONSE, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + feedback = SurveyTypeA(api_client) + + response = await feedback.get_applicant_answer(ACCOUNT_ID, survey_id, answer_id) + assert response == SurveyAnswerTypeAResponse.model_validate( + SURVEY_ANSWER_RESPONSE, + ) diff --git a/tests/test_entities/test_user_settings.py b/tests/test_entities/test_user_settings.py index 8fe693c..cdc5943 100644 --- a/tests/test_entities/test_user_settings.py +++ b/tests/test_entities/test_user_settings.py @@ -4,26 +4,30 @@ from huntflow_api_client import HuntflowAPI from huntflow_api_client.entities.user_settings import UserSettings +from huntflow_api_client.models.common import StatusResponse +from huntflow_api_client.models.request.user_settings import ( + ExchangeEmailAccountRequest, + OtherEmailAccountRequest, +) from huntflow_api_client.models.response.user_settings import ( CalendarAccountsListResponse, + EmailAccount, EmailAccountsListResponse, ) from huntflow_api_client.tokens.proxy import HuntflowTokenProxy from tests.api import BASE_URL, VERSIONED_BASE_URL -GET_USER_EMAIL_ACCOUNTS_RESPONSE: Dict[str, Any] = { - "items": [ - { - "id": 10, - "name": "test@example.com", - "email": "test@example.com", - "receive": False, - "send": False, - "last_sync": "2020-01-01T00:00:00+03:00", - }, - ], +EMAIL_ACCOUNT: Dict[str, Any] = { + "id": 10, + "name": "test@example.com", + "email": "test@example.com", + "receive": False, + "send": False, + "last_sync": "2020-01-01T00:00:00+03:00", } +GET_USER_EMAIL_ACCOUNTS_RESPONSE: Dict[str, Any] = {"items": [EMAIL_ACCOUNT]} + GET_USER_CALENDAR_ACCOUNTS_RESPONSE: Dict[str, Any] = { "items": [ { @@ -43,6 +47,30 @@ ], } +EXCHANGE_EMAIL_ACCOUNT_CREATE_REQUEST: Dict[str, Any] = { + "access_type": "DEFAULT", + "email": "mail@mail.ru", + "ews_url": "https://your-company.org/ews/exchange.asmx", + "password": "string", + "user": "string", +} + +ACCOUNT_EMAIL_ID = 1 + +IMAP_EMAIL_ACCOUNT_CREATE_REQUEST: Dict[str, Any] = { + "email": "mail@mail.ru", + "password": "string", + "inbound_host": "imap.your-company.org", + "inbound_port": 1, + "inbound_ssl": False, + "outbound_host": "smtp.your-company.org", + "outbound_port": 1, + "outbound_ssl": False, + "type": "IMAP", +} + +DELETE_ACCOUNT_EMAIL_RESPONSE: Dict[str, Any] = {"status": True} + async def test_get_email_accounts( httpx_mock: HTTPXMock, @@ -72,3 +100,86 @@ async def test_get_calendar_accounts( response = await settings.get_calendar_accounts() assert response == CalendarAccountsListResponse(**GET_USER_CALENDAR_ACCOUNTS_RESPONSE) + + +async def test_create_exchange_email_account( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/email_accounts/exchange", + json=EMAIL_ACCOUNT, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + + api_request = ExchangeEmailAccountRequest(**EXCHANGE_EMAIL_ACCOUNT_CREATE_REQUEST) + settings = UserSettings(api_client) + + response = await settings.create_exchange_email_account(api_request) + assert response == EmailAccount(**EMAIL_ACCOUNT) + + +async def test_update_exchange_email_account( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/email_accounts/exchange/{ACCOUNT_EMAIL_ID}", + json=EMAIL_ACCOUNT, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + + api_request = ExchangeEmailAccountRequest(**EXCHANGE_EMAIL_ACCOUNT_CREATE_REQUEST) + settings = UserSettings(api_client) + + response = await settings.update_exchange_email_account(api_request, ACCOUNT_EMAIL_ID) + assert response == EmailAccount(**EMAIL_ACCOUNT) + + +async def test_create_other_email_account( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/email_accounts/other", + json=EMAIL_ACCOUNT, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + + api_request = OtherEmailAccountRequest(**IMAP_EMAIL_ACCOUNT_CREATE_REQUEST) + settings = UserSettings(api_client) + + response = await settings.create_other_email_account(api_request) + assert response == EmailAccount(**EMAIL_ACCOUNT) + + +async def test_update_other_email_account( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/email_accounts/other/{ACCOUNT_EMAIL_ID}", + json=EMAIL_ACCOUNT, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + + api_request = OtherEmailAccountRequest(**IMAP_EMAIL_ACCOUNT_CREATE_REQUEST) + settings = UserSettings(api_client) + + response = await settings.update_other_email_account(api_request, ACCOUNT_EMAIL_ID) + assert response == EmailAccount(**EMAIL_ACCOUNT) + + +async def test_delete_email_account( + httpx_mock: HTTPXMock, + token_proxy: HuntflowTokenProxy, +) -> None: + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/email_accounts/{ACCOUNT_EMAIL_ID}", + json=DELETE_ACCOUNT_EMAIL_RESPONSE, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + settings = UserSettings(api_client) + + response = await settings.delete_email_account(ACCOUNT_EMAIL_ID) + assert response == StatusResponse(**DELETE_ACCOUNT_EMAIL_RESPONSE) diff --git a/tests/test_entities/test_users_management.py b/tests/test_entities/test_users_management.py index 7340819..72cbf75 100644 --- a/tests/test_entities/test_users_management.py +++ b/tests/test_entities/test_users_management.py @@ -37,6 +37,34 @@ }, ], } +GET_USERS_RESPONSE_TWO_MEMBERS: Dict[str, Any] = { + "page": 1, + "count": 30, + "total_pages": 1, + "total_items": 2, + "items": [ + { + "id": "some_foreign_id_1", + "name": "John Doe", + "email": "mail@gmail.com", + "type": "owner", + "head_id": "user-032044", + "division_ids": ["division-154", "division-871"], + "permissions": ["string"], + "meta": {}, + }, + { + "id": "some_foreign_id_2", + "name": "Nick Smith", + "email": "mail@example.com", + "type": "manager", + "head_id": "user-1234", + "division_ids": ["division-123", "division-321"], + "permissions": ["string"], + "meta": {}, + }, + ], +} GET_USER_BY_FOREIGN_RESPONSE: Dict[str, Any] = { "id": FOREIGN_USER_ID, "name": "John Doe", @@ -75,15 +103,33 @@ async def test_get_users_with_foreign( token_proxy: HuntflowTokenProxy, ) -> None: httpx_mock.add_response( - url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/users/foreign?count=30&page=1", + url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/users/" + f"foreign?count=30&page=1&member_types=owner", json=GET_USERS_RESPONSE, ) api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) users_management = UsersManagement(api_client) - response = await users_management.get_users_with_foreign(account_id=ACCOUNT_ID) + response = await users_management.get_users_with_foreign( + account_id=ACCOUNT_ID, + member_types=[MemberType.owner], + ) assert response == ForeignUsersListResponse(**GET_USERS_RESPONSE) + httpx_mock.add_response( + url=f"{VERSIONED_BASE_URL}/accounts/{ACCOUNT_ID}/users/" + f"foreign?count=30&page=1&member_types=owner&member_types=manager", + json=GET_USERS_RESPONSE_TWO_MEMBERS, + ) + api_client = HuntflowAPI(BASE_URL, token_proxy=token_proxy) + users_management = UsersManagement(api_client) + + response = await users_management.get_users_with_foreign( + account_id=ACCOUNT_ID, + member_types=[MemberType.owner, MemberType.manager], + ) + assert response == ForeignUsersListResponse(**GET_USERS_RESPONSE_TWO_MEMBERS) + async def test_get_user_by_foreign( httpx_mock: HTTPXMock, diff --git a/tests/test_entities/test_webhooks.py b/tests/test_entities/test_webhooks.py index 3dee175..cc9cf43 100644 --- a/tests/test_entities/test_webhooks.py +++ b/tests/test_entities/test_webhooks.py @@ -22,6 +22,7 @@ "created": "2023-05-04T17:21:14+03:00", "active": True, "webhook_events": ["APPLICANT"], + "type": "USER", }, ], } @@ -33,6 +34,7 @@ "created": "2023-05-04T17:24:28+03:00", "active": True, "webhook_events": ["APPLICANT"], + "type": "USER", }