From 7b7d8ca20c4af60c0748f331440396e5db887474 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:40:55 +0000 Subject: [PATCH 1/2] chore: sync repo --- .devcontainer/Dockerfile | 8 + .devcontainer/devcontainer.json | 43 + .fern/metadata.json | 19 - .fernignore | 6 - .github/workflows/ci.yml | 143 +- .github/workflows/publish-pypi.yml | 17 +- .github/workflows/release-doctor.yml | 19 + .gitignore | 21 +- .python-version | 1 + .release-please-manifest.json | 3 + .stats.yml | 4 + .vscode/settings.json | 3 + Brewfile | 2 + CHANGELOG.md | 223 + CONTRIBUTING.md | 154 +- README.md | 307 +- api.md | 197 + bin/check-release-environment | 17 + bin/publish-pypi | 11 + changelog.md | 9 - examples/.keep | 4 + poetry.lock | 1469 ----- pyproject.toml | 302 +- reference.md | 4171 ------------ release-please-config.json | 66 + requirements-dev.lock | 110 + requirements.txt | 4 - scripts/bootstrap | 30 + scripts/format | 14 + scripts/lint | 22 + scripts/test | 38 + scripts/utils/ruffen-docs.py | 167 + scripts/utils/upload-artifact.sh | 27 + src/vitable_connect/__init__.py | 712 +- src/vitable_connect/_base_client.py | 2153 +++++++ src/vitable_connect/_client.py | 870 +++ src/vitable_connect/_compat.py | 226 + src/vitable_connect/_constants.py | 14 + src/vitable_connect/_default_clients.py | 32 - src/vitable_connect/_exceptions.py | 108 + src/vitable_connect/_files.py | 173 + src/vitable_connect/_models.py | 958 +++ src/vitable_connect/_qs.py | 149 + src/vitable_connect/_resource.py | 43 + src/vitable_connect/_response.py | 835 +++ src/vitable_connect/_streaming.py | 338 + src/vitable_connect/_types.py | 274 + src/vitable_connect/_utils/__init__.py | 64 + src/vitable_connect/_utils/_compat.py | 45 + src/vitable_connect/_utils/_datetime_parse.py | 136 + src/vitable_connect/_utils/_json.py | 35 + src/vitable_connect/_utils/_logs.py | 25 + src/vitable_connect/_utils/_path.py | 127 + src/vitable_connect/_utils/_proxy.py | 65 + src/vitable_connect/_utils/_reflection.py | 42 + .../_utils/_resources_proxy.py | 24 + src/vitable_connect/_utils/_streams.py | 12 + src/vitable_connect/_utils/_sync.py | 58 + src/vitable_connect/_utils/_transform.py | 457 ++ src/vitable_connect/_utils/_typing.py | 156 + src/vitable_connect/_utils/_utils.py | 433 ++ src/vitable_connect/_version.py | 4 + src/vitable_connect/auth/__init__.py | 4 - src/vitable_connect/auth/client.py | 141 - src/vitable_connect/auth/raw_client.py | 338 - src/vitable_connect/client.py | 395 -- src/vitable_connect/core/__init__.py | 132 - src/vitable_connect/core/api_error.py | 23 - src/vitable_connect/core/client_wrapper.py | 147 - src/vitable_connect/core/datetime_utils.py | 70 - src/vitable_connect/core/file.py | 67 - src/vitable_connect/core/force_multipart.py | 18 - src/vitable_connect/core/http_client.py | 940 --- src/vitable_connect/core/http_response.py | 63 - src/vitable_connect/core/http_sse/__init__.py | 42 - src/vitable_connect/core/http_sse/_api.py | 455 -- .../core/http_sse/_decoders.py | 74 - .../core/http_sse/_exceptions.py | 7 - src/vitable_connect/core/http_sse/_models.py | 17 - src/vitable_connect/core/jsonable_encoder.py | 133 - src/vitable_connect/core/logging.py | 107 - src/vitable_connect/core/pagination.py | 82 - src/vitable_connect/core/parse_error.py | 36 - .../core/pydantic_utilities.py | 486 -- src/vitable_connect/core/query_encoder.py | 58 - .../core/remove_none_from_dict.py | 11 - src/vitable_connect/core/request_options.py | 40 - src/vitable_connect/core/serialization.py | 347 - src/vitable_connect/employees/__init__.py | 4 - src/vitable_connect/employees/client.py | 456 -- src/vitable_connect/employees/raw_client.py | 1021 --- src/vitable_connect/employers/__init__.py | 4 - src/vitable_connect/employers/client.py | 2267 ------- src/vitable_connect/employers/raw_client.py | 5733 ----------------- src/vitable_connect/enrollments/__init__.py | 4 - src/vitable_connect/enrollments/client.py | 361 -- src/vitable_connect/enrollments/raw_client.py | 949 --- src/vitable_connect/environment.py | 8 - src/vitable_connect/errors/__init__.py | 62 - .../errors/bad_gateway_error.py | 11 - .../errors/bad_request_error.py | 11 - src/vitable_connect/errors/conflict_error.py | 11 - src/vitable_connect/errors/forbidden_error.py | 11 - .../errors/internal_server_error.py | 11 - src/vitable_connect/errors/not_found_error.py | 11 - .../errors/too_many_requests_error.py | 11 - .../errors/unauthorized_error.py | 11 - .../errors/unprocessable_entity_error.py | 11 - src/vitable_connect/groups/__init__.py | 34 - src/vitable_connect/groups/client.py | 432 -- .../groups/members/__init__.py | 34 - src/vitable_connect/groups/members/client.py | 63 - .../groups/members/raw_client.py | 13 - .../groups/members/sync/__init__.py | 4 - .../groups/members/sync/client.py | 260 - .../groups/members/sync/raw_client.py | 618 -- src/vitable_connect/groups/raw_client.py | 1238 ---- src/vitable_connect/lib/.keep | 4 + src/vitable_connect/members/__init__.py | 4 - src/vitable_connect/members/client.py | 715 -- src/vitable_connect/members/raw_client.py | 2254 ------- src/vitable_connect/organizations/__init__.py | 4 - src/vitable_connect/organizations/client.py | 195 - .../organizations/raw_client.py | 585 -- src/vitable_connect/pagination.py | 77 + src/vitable_connect/plans/__init__.py | 4 - src/vitable_connect/plans/client.py | 143 - src/vitable_connect/plans/raw_client.py | 340 - src/vitable_connect/resources/__init__.py | 131 + src/vitable_connect/resources/auth.py | 198 + src/vitable_connect/resources/employees.py | 491 ++ src/vitable_connect/resources/employers.py | 2245 +++++++ src/vitable_connect/resources/enrollments.py | 444 ++ .../resources/groups/__init__.py | 33 + .../resources/groups/groups.py | 509 ++ .../resources/groups/members/__init__.py | 33 + .../resources/groups/members/members.py | 102 + .../resources/groups/members/sync.py | 276 + src/vitable_connect/resources/members.py | 935 +++ .../resources/organizations.py | 281 + src/vitable_connect/resources/plans.py | 196 + .../resources/webhook_events.py | 454 ++ src/vitable_connect/types/__init__.py | 655 +- src/vitable_connect/types/access_method.py | 5 - .../types/access_token_response.py | 38 - .../types/additional_access_method.py | 5 - src/vitable_connect/types/address.py | 60 - src/vitable_connect/types/address_request.py | 52 - .../types/auth_issue_access_token_params.py | 32 + .../types/auth_issue_access_token_response.py | 35 + src/vitable_connect/types/benefit_family.py | 5 - .../types/benefit_family_param_item.py | 5 - .../types/benefit_lifecycle_stage_item.py | 7 - .../types/benefit_plan_network.py | 73 - .../types/benefit_plan_year_detail.py | 144 - .../types/benefit_plan_year_id.py | 6 - .../types/benefit_plan_year_list.py | 131 - src/vitable_connect/types/benefit_status.py | 5 - src/vitable_connect/types/benefit_summary.py | 60 - src/vitable_connect/types/bound_entity.py | 31 - .../types/bound_entity_response.py | 31 - .../types/bound_entity_type.py | 5 - src/vitable_connect/types/card_type.py | 5 - src/vitable_connect/types/category.py | 5 - .../types/census_sync_detail_response.py | 27 - .../census_sync_employee_address_request.py | 53 - .../types/census_sync_employee_request.py | 71 - .../types/census_sync_response.py | 28 - .../types/classification_correction_source.py | 5 - .../types/compensation_type.py | 5 - .../types/contribution_class_compensation.py | 5 - .../types/contribution_class_employment.py | 7 - .../types/contribution_class_location.py | 5 - .../types/contribution_range.py | 27 - .../types/contribution_strategy.py | 34 - .../types/contribution_tier.py | 70 - src/vitable_connect/types/coverage_tier.py | 5 - .../types/create_organization_request_type.py | 8 - src/vitable_connect/types/created_after.py | 5 - src/vitable_connect/types/created_before.py | 5 - src/vitable_connect/types/deduction_detail.py | 64 - .../types/deduction_frequency.py | 5 - .../types/digital_benefit_card.py | 115 - .../types/election_status_item.py | 5 - .../types/eligibility_policy.py | 52 - .../types/eligibility_policy_rule.py | 52 - .../eligibility_rule_compensation_type.py | 5 - .../types/eligibility_rule_employee_class.py | 8 - .../types/eligible_geographical_location.py | 31 - .../eligible_geographical_location_type.py | 5 - src/vitable_connect/types/employee.py | 214 +- src/vitable_connect/types/employee_address.py | 52 - .../types/employee_address_input.py | 55 - src/vitable_connect/types/employee_class.py | 10 +- src/vitable_connect/types/employee_id.py | 6 - .../types/employee_list_enrollments_params.py | 15 + .../types/employee_list_response.py | 33 - .../types/employee_response.py | 27 - .../types/employee_retrieve_response.py | 12 + src/vitable_connect/types/employee_search.py | 6 - src/vitable_connect/types/employee_status.py | 5 - .../types/employee_update_params.py | 79 + .../types/employee_update_response.py | 12 + src/vitable_connect/types/employer.py | 128 +- src/vitable_connect/types/employer_address.py | 56 - .../types/employer_address_input.py | 52 - .../types/employer_benefit_lifecycle_stage.py | 28 - .../employer_benefit_plan_year_response.py | 27 - ...ployer_benefit_plan_years_list_response.py | 23 - src/vitable_connect/types/employer_contact.py | 31 - .../types/employer_create_params.py | 50 + ...sure_payroll_integration_email_response.py | 15 + src/vitable_connect/types/employer_hris.py | 23 - .../types/employer_hris_response.py | 27 - .../types/employer_hris_status.py | 32 - src/vitable_connect/types/employer_id.py | 6 - .../types/employer_invoice_pdf.py | 22 - .../types/employer_invoice_pdf_response.py | 27 - .../types/employer_invoices_list_response.py | 33 - ...st_benefit_plan_year_enrollments_params.py | 29 + ..._benefit_plan_year_enrollments_response.py | 88 + ...ployer_list_benefit_plan_years_response.py | 129 + .../types/employer_list_employees_params.py | 21 + .../employer_list_hris_providers_response.py | 24 + .../types/employer_list_invoices_params.py | 16 + .../types/employer_list_invoices_response.py | 38 + .../types/employer_list_params.py | 48 + ...ist_payroll_deduction_statements_params.py | 15 + ...t_payroll_deduction_statements_response.py | 51 + .../types/employer_list_response.py | 140 + ...yroll_deduction_statement_list_response.py | 33 - .../types/employer_response.py | 28 +- ...yer_retrieve_benefit_plan_year_response.py | 335 + .../types/employer_retrieve_hris_response.py | 41 + .../employer_retrieve_invoice_pdf_response.py | 16 + ..._retrieve_payroll_access_setup_response.py | 20 + src/vitable_connect/types/employer_search.py | 6 - .../types/employer_settings.py | 28 - .../types/employer_settings_response.py | 27 - .../employer_submit_census_sync_params.py | 192 + .../employer_submit_census_sync_response.py | 19 + ...oyer_submit_payroll_access_setup_params.py | 78 + ...er_submit_payroll_access_setup_response.py | 20 + .../types/employer_update_params.py | 41 + .../types/employer_update_settings_params.py | 17 + .../employer_update_settings_response.py | 24 + src/vitable_connect/types/enrollment.py | 153 +- .../types/enrollment_detail.py | 102 - src/vitable_connect/types/enrollment_id.py | 6 - .../types/enrollment_list_response.py | 33 - src/vitable_connect/types/enrollment_rate.py | 32 - .../types/enrollment_rate_summary.py | 32 - .../types/enrollment_reissue_params.py | 22 + .../types/enrollment_reissue_response.py | 16 + .../types/enrollment_response.py | 27 - .../types/enrollment_retrieve_response.py | 133 + .../types/enrollment_status.py | 8 +- .../types/enrollment_terminate_params.py | 22 + src/vitable_connect/types/error_response.py | 43 - src/vitable_connect/types/event_id.py | 3 - src/vitable_connect/types/event_name.py | 20 - src/vitable_connect/types/gender.py | 7 - src/vitable_connect/types/grant_type.py | 5 - src/vitable_connect/types/group.py | 76 +- .../types/group_create_params.py | 13 + src/vitable_connect/types/group_id.py | 6 - .../types/group_list_params.py | 15 + .../types/group_list_response.py | 33 - .../group_member_sync_detail_response.py | 27 - .../types/group_member_sync_failure.py | 33 - .../types/group_member_sync_member_request.py | 59 - .../types/group_member_sync_request_detail.py | 44 - ...oup_member_sync_request_detail_response.py | 27 - .../types/group_member_sync_response.py | 33 - .../types/group_member_sync_results.py | 33 - src/vitable_connect/types/group_response.py | 27 +- .../types/group_update_params.py | 14 + src/vitable_connect/types/groups/__init__.py | 3 + .../types/groups/members/__init__.py | 7 + .../groups/members/sync_retrieve_response.py | 47 + .../groups/members/sync_submit_params.py | 45 + .../groups/members/sync_submit_response.py | 21 + src/vitable_connect/types/household_member.py | 68 - .../types/household_member_relationship.py | 5 - .../types/household_member_type.py | 5 - .../types/household_members_response.py | 27 - src/vitable_connect/types/hris_connection.py | 43 - src/vitable_connect/types/hris_status_item.py | 5 - .../types/ichra_contribution_class.py | 92 - src/vitable_connect/types/idp_provider.py | 5 - .../types/include_cancelled.py | 6 - src/vitable_connect/types/invoice.py | 37 - src/vitable_connect/types/invoice_id.py | 6 - .../types/invoices_cursor_pagination.py | 22 - src/vitable_connect/types/limit.py | 6 - .../list_webhook_event_deliveries_response.py | 23 - src/vitable_connect/types/marital_status.py | 5 - src/vitable_connect/types/member.py | 143 - src/vitable_connect/types/member_address.py | 116 - .../types/member_dependents_response.py | 27 - .../member_digital_benefit_cards_response.py | 27 - .../types/member_employments_response.py | 27 - .../types/member_enrollment.py | 182 - .../types/member_enrollments_response.py | 27 - src/vitable_connect/types/member_id.py | 6 - .../types/member_legal_dependent.py | 72 - .../types/member_list_dependents_response.py | 51 + .../types/member_list_employments_response.py | 14 + .../types/member_list_enrollments_response.py | 170 + .../types/member_list_id_cards_response.py | 161 + src/vitable_connect/types/member_list_item.py | 52 - .../types/member_list_params.py | 21 + ...mber_list_qualifying_life_events_params.py | 18 + ...er_list_qualifying_life_events_response.py | 78 + .../types/member_list_response.py | 186 +- .../types/member_qualifying_life_event.py | 76 - ...ber_qualifying_life_event_list_response.py | 33 - src/vitable_connect/types/member_response.py | 27 - .../member_retrieve_household_response.py | 49 + .../types/member_retrieve_response.py | 250 + src/vitable_connect/types/member_search.py | 6 - src/vitable_connect/types/member_status.py | 5 - .../types/missing_employee_resolution.py | 5 - src/vitable_connect/types/name_suffix.py | 5 - src/vitable_connect/types/offset.py | 8 - src/vitable_connect/types/operation.py | 5 - src/vitable_connect/types/organization.py | 59 - .../types/organization_create_params.py | 22 + .../types/organization_create_response.py | 40 + .../types/organization_employer.py | 110 - .../organization_employer_list_response.py | 33 - .../types/organization_hris_provider.py | 27 - .../organization_hris_providers_response.py | 27 - .../types/organization_list_response.py | 49 + .../types/organization_type.py | 8 - .../types/organizations_list_response.py | 32 - src/vitable_connect/types/page.py | 6 - src/vitable_connect/types/pagination.py | 46 +- .../types/payroll_access_setup_status.py | 28 - .../payroll_access_setup_status_response.py | 27 - .../types/payroll_deduction_statement.py | 76 - .../types/payroll_integration_email.py | 22 - .../payroll_integration_email_response.py | 27 - src/vitable_connect/types/plan.py | 27 - src/vitable_connect/types/plan_list_params.py | 15 + .../types/plan_list_response.py | 34 +- .../types/plan_year_enrollment.py | 99 - .../plan_year_enrollment_election_status.py | 5 - .../plan_year_enrollment_list_response.py | 33 - .../plan_year_enrollment_policy_status.py | 7 - .../types/preferred_language.py | 5 - src/vitable_connect/types/product_code.py | 24 - .../types/qualifying_life_event_status.py | 5 - .../types/qualifying_life_event_type.py | 30 - .../types/reissue_enrollment.py | 22 - .../types/reissue_enrollment_response.py | 27 - src/vitable_connect/types/relationship.py | 5 - .../types/remaining_employee_action.py | 5 - src/vitable_connect/types/request_id.py | 3 - src/vitable_connect/types/resource_id.py | 3 - src/vitable_connect/types/resource_type.py | 7 - src/vitable_connect/types/search.py | 8 - src/vitable_connect/types/sex_at_birth.py | 5 - src/vitable_connect/types/state.py | 71 - src/vitable_connect/types/status.py | 5 - .../types/tax_classification.py | 5 - src/vitable_connect/types/termination_rule.py | 5 - .../types/update_employer_address_input.py | 52 - src/vitable_connect/types/waiting_period.py | 5 - src/vitable_connect/types/webhook_delivery.py | 63 - src/vitable_connect/types/webhook_event.py | 55 +- .../webhook_event_list_deliveries_response.py | 41 + .../types/webhook_event_list_params.py | 62 + .../types/webhook_event_list_response.py | 33 - .../types/webhook_event_response.py | 27 - .../types/webhook_event_retrieve_response.py | 12 + src/vitable_connect/version.py | 3 - .../webhook_events/__init__.py | 4 - src/vitable_connect/webhook_events/client.py | 373 -- .../webhook_events/raw_client.py | 951 --- src/vitable_connect_api/lib/.keep | 4 + src/vitable_partner_api/lib/.keep | 4 + tests/__init__.py | 1 + tests/api_resources/__init__.py | 1 + tests/api_resources/groups/__init__.py | 1 + .../api_resources/groups/members/__init__.py | 1 + .../api_resources/groups/members/test_sync.py | 333 + tests/api_resources/test_auth.py | 116 + tests/api_resources/test_employees.py | 356 + tests/api_resources/test_employers.py | 2071 ++++++ tests/api_resources/test_enrollments.py | 323 + tests/api_resources/test_groups.py | 361 ++ tests/api_resources/test_members.py | 720 +++ tests/api_resources/test_organizations.py | 166 + tests/api_resources/test_plans.py | 99 + tests/api_resources/test_webhook_events.py | 282 + tests/conftest.py | 91 +- tests/custom/test_client.py | 7 - tests/sample_file.txt | 1 + tests/test_aiohttp_autodetect.py | 116 - tests/test_client.py | 2018 ++++++ tests/test_extract_files.py | 91 + tests/test_files.py | 148 + tests/test_models.py | 1017 +++ tests/test_qs.py | 78 + tests/test_required_args.py | 111 + tests/test_response.py | 277 + tests/test_streaming.py | 252 + tests/test_transform.py | 460 ++ tests/test_utils/test_datetime_parse.py | 110 + tests/test_utils/test_json.py | 126 + tests/test_utils/test_path.py | 89 + tests/test_utils/test_proxy.py | 34 + tests/test_utils/test_typing.py | 73 + tests/utils.py | 167 + tests/utils/__init__.py | 2 - tests/utils/assets/models/__init__.py | 21 - tests/utils/assets/models/circle.py | 11 - tests/utils/assets/models/color.py | 7 - .../assets/models/object_with_defaults.py | 15 - .../models/object_with_optional_field.py | 35 - tests/utils/assets/models/shape.py | 28 - tests/utils/assets/models/square.py | 11 - .../assets/models/undiscriminated_shape.py | 10 - tests/utils/test_http_client.py | 853 --- tests/utils/test_query_encoding.py | 36 - tests/utils/test_serialization.py | 72 - uv.lock | 1829 ++++++ 428 files changed, 31381 insertions(+), 37334 deletions(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json delete mode 100644 .fern/metadata.json create mode 100644 .github/workflows/release-doctor.yml create mode 100644 .python-version create mode 100644 .release-please-manifest.json create mode 100644 .stats.yml create mode 100644 .vscode/settings.json create mode 100644 Brewfile create mode 100644 CHANGELOG.md create mode 100644 api.md create mode 100644 bin/check-release-environment create mode 100644 bin/publish-pypi delete mode 100644 changelog.md create mode 100644 examples/.keep delete mode 100644 poetry.lock delete mode 100644 reference.md create mode 100644 release-please-config.json create mode 100644 requirements-dev.lock delete mode 100644 requirements.txt create mode 100755 scripts/bootstrap create mode 100755 scripts/format create mode 100755 scripts/lint create mode 100755 scripts/test create mode 100644 scripts/utils/ruffen-docs.py create mode 100755 scripts/utils/upload-artifact.sh create mode 100644 src/vitable_connect/_base_client.py create mode 100644 src/vitable_connect/_client.py create mode 100644 src/vitable_connect/_compat.py create mode 100644 src/vitable_connect/_constants.py delete mode 100644 src/vitable_connect/_default_clients.py create mode 100644 src/vitable_connect/_exceptions.py create mode 100644 src/vitable_connect/_files.py create mode 100644 src/vitable_connect/_models.py create mode 100644 src/vitable_connect/_qs.py create mode 100644 src/vitable_connect/_resource.py create mode 100644 src/vitable_connect/_response.py create mode 100644 src/vitable_connect/_streaming.py create mode 100644 src/vitable_connect/_types.py create mode 100644 src/vitable_connect/_utils/__init__.py create mode 100644 src/vitable_connect/_utils/_compat.py create mode 100644 src/vitable_connect/_utils/_datetime_parse.py create mode 100644 src/vitable_connect/_utils/_json.py create mode 100644 src/vitable_connect/_utils/_logs.py create mode 100644 src/vitable_connect/_utils/_path.py create mode 100644 src/vitable_connect/_utils/_proxy.py create mode 100644 src/vitable_connect/_utils/_reflection.py create mode 100644 src/vitable_connect/_utils/_resources_proxy.py create mode 100644 src/vitable_connect/_utils/_streams.py create mode 100644 src/vitable_connect/_utils/_sync.py create mode 100644 src/vitable_connect/_utils/_transform.py create mode 100644 src/vitable_connect/_utils/_typing.py create mode 100644 src/vitable_connect/_utils/_utils.py create mode 100644 src/vitable_connect/_version.py delete mode 100644 src/vitable_connect/auth/__init__.py delete mode 100644 src/vitable_connect/auth/client.py delete mode 100644 src/vitable_connect/auth/raw_client.py delete mode 100644 src/vitable_connect/client.py delete mode 100644 src/vitable_connect/core/__init__.py delete mode 100644 src/vitable_connect/core/api_error.py delete mode 100644 src/vitable_connect/core/client_wrapper.py delete mode 100644 src/vitable_connect/core/datetime_utils.py delete mode 100644 src/vitable_connect/core/file.py delete mode 100644 src/vitable_connect/core/force_multipart.py delete mode 100644 src/vitable_connect/core/http_client.py delete mode 100644 src/vitable_connect/core/http_response.py delete mode 100644 src/vitable_connect/core/http_sse/__init__.py delete mode 100644 src/vitable_connect/core/http_sse/_api.py delete mode 100644 src/vitable_connect/core/http_sse/_decoders.py delete mode 100644 src/vitable_connect/core/http_sse/_exceptions.py delete mode 100644 src/vitable_connect/core/http_sse/_models.py delete mode 100644 src/vitable_connect/core/jsonable_encoder.py delete mode 100644 src/vitable_connect/core/logging.py delete mode 100644 src/vitable_connect/core/pagination.py delete mode 100644 src/vitable_connect/core/parse_error.py delete mode 100644 src/vitable_connect/core/pydantic_utilities.py delete mode 100644 src/vitable_connect/core/query_encoder.py delete mode 100644 src/vitable_connect/core/remove_none_from_dict.py delete mode 100644 src/vitable_connect/core/request_options.py delete mode 100644 src/vitable_connect/core/serialization.py delete mode 100644 src/vitable_connect/employees/__init__.py delete mode 100644 src/vitable_connect/employees/client.py delete mode 100644 src/vitable_connect/employees/raw_client.py delete mode 100644 src/vitable_connect/employers/__init__.py delete mode 100644 src/vitable_connect/employers/client.py delete mode 100644 src/vitable_connect/employers/raw_client.py delete mode 100644 src/vitable_connect/enrollments/__init__.py delete mode 100644 src/vitable_connect/enrollments/client.py delete mode 100644 src/vitable_connect/enrollments/raw_client.py delete mode 100644 src/vitable_connect/environment.py delete mode 100644 src/vitable_connect/errors/__init__.py delete mode 100644 src/vitable_connect/errors/bad_gateway_error.py delete mode 100644 src/vitable_connect/errors/bad_request_error.py delete mode 100644 src/vitable_connect/errors/conflict_error.py delete mode 100644 src/vitable_connect/errors/forbidden_error.py delete mode 100644 src/vitable_connect/errors/internal_server_error.py delete mode 100644 src/vitable_connect/errors/not_found_error.py delete mode 100644 src/vitable_connect/errors/too_many_requests_error.py delete mode 100644 src/vitable_connect/errors/unauthorized_error.py delete mode 100644 src/vitable_connect/errors/unprocessable_entity_error.py delete mode 100644 src/vitable_connect/groups/__init__.py delete mode 100644 src/vitable_connect/groups/client.py delete mode 100644 src/vitable_connect/groups/members/__init__.py delete mode 100644 src/vitable_connect/groups/members/client.py delete mode 100644 src/vitable_connect/groups/members/raw_client.py delete mode 100644 src/vitable_connect/groups/members/sync/__init__.py delete mode 100644 src/vitable_connect/groups/members/sync/client.py delete mode 100644 src/vitable_connect/groups/members/sync/raw_client.py delete mode 100644 src/vitable_connect/groups/raw_client.py create mode 100644 src/vitable_connect/lib/.keep delete mode 100644 src/vitable_connect/members/__init__.py delete mode 100644 src/vitable_connect/members/client.py delete mode 100644 src/vitable_connect/members/raw_client.py delete mode 100644 src/vitable_connect/organizations/__init__.py delete mode 100644 src/vitable_connect/organizations/client.py delete mode 100644 src/vitable_connect/organizations/raw_client.py create mode 100644 src/vitable_connect/pagination.py delete mode 100644 src/vitable_connect/plans/__init__.py delete mode 100644 src/vitable_connect/plans/client.py delete mode 100644 src/vitable_connect/plans/raw_client.py create mode 100644 src/vitable_connect/resources/__init__.py create mode 100644 src/vitable_connect/resources/auth.py create mode 100644 src/vitable_connect/resources/employees.py create mode 100644 src/vitable_connect/resources/employers.py create mode 100644 src/vitable_connect/resources/enrollments.py create mode 100644 src/vitable_connect/resources/groups/__init__.py create mode 100644 src/vitable_connect/resources/groups/groups.py create mode 100644 src/vitable_connect/resources/groups/members/__init__.py create mode 100644 src/vitable_connect/resources/groups/members/members.py create mode 100644 src/vitable_connect/resources/groups/members/sync.py create mode 100644 src/vitable_connect/resources/members.py create mode 100644 src/vitable_connect/resources/organizations.py create mode 100644 src/vitable_connect/resources/plans.py create mode 100644 src/vitable_connect/resources/webhook_events.py delete mode 100644 src/vitable_connect/types/access_method.py delete mode 100644 src/vitable_connect/types/access_token_response.py delete mode 100644 src/vitable_connect/types/additional_access_method.py delete mode 100644 src/vitable_connect/types/address.py delete mode 100644 src/vitable_connect/types/address_request.py create mode 100644 src/vitable_connect/types/auth_issue_access_token_params.py create mode 100644 src/vitable_connect/types/auth_issue_access_token_response.py delete mode 100644 src/vitable_connect/types/benefit_family.py delete mode 100644 src/vitable_connect/types/benefit_family_param_item.py delete mode 100644 src/vitable_connect/types/benefit_lifecycle_stage_item.py delete mode 100644 src/vitable_connect/types/benefit_plan_network.py delete mode 100644 src/vitable_connect/types/benefit_plan_year_detail.py delete mode 100644 src/vitable_connect/types/benefit_plan_year_id.py delete mode 100644 src/vitable_connect/types/benefit_plan_year_list.py delete mode 100644 src/vitable_connect/types/benefit_status.py delete mode 100644 src/vitable_connect/types/benefit_summary.py delete mode 100644 src/vitable_connect/types/bound_entity.py delete mode 100644 src/vitable_connect/types/bound_entity_response.py delete mode 100644 src/vitable_connect/types/bound_entity_type.py delete mode 100644 src/vitable_connect/types/card_type.py delete mode 100644 src/vitable_connect/types/category.py delete mode 100644 src/vitable_connect/types/census_sync_detail_response.py delete mode 100644 src/vitable_connect/types/census_sync_employee_address_request.py delete mode 100644 src/vitable_connect/types/census_sync_employee_request.py delete mode 100644 src/vitable_connect/types/census_sync_response.py delete mode 100644 src/vitable_connect/types/classification_correction_source.py delete mode 100644 src/vitable_connect/types/compensation_type.py delete mode 100644 src/vitable_connect/types/contribution_class_compensation.py delete mode 100644 src/vitable_connect/types/contribution_class_employment.py delete mode 100644 src/vitable_connect/types/contribution_class_location.py delete mode 100644 src/vitable_connect/types/contribution_range.py delete mode 100644 src/vitable_connect/types/contribution_strategy.py delete mode 100644 src/vitable_connect/types/contribution_tier.py delete mode 100644 src/vitable_connect/types/coverage_tier.py delete mode 100644 src/vitable_connect/types/create_organization_request_type.py delete mode 100644 src/vitable_connect/types/created_after.py delete mode 100644 src/vitable_connect/types/created_before.py delete mode 100644 src/vitable_connect/types/deduction_detail.py delete mode 100644 src/vitable_connect/types/deduction_frequency.py delete mode 100644 src/vitable_connect/types/digital_benefit_card.py delete mode 100644 src/vitable_connect/types/election_status_item.py delete mode 100644 src/vitable_connect/types/eligibility_policy.py delete mode 100644 src/vitable_connect/types/eligibility_policy_rule.py delete mode 100644 src/vitable_connect/types/eligibility_rule_compensation_type.py delete mode 100644 src/vitable_connect/types/eligibility_rule_employee_class.py delete mode 100644 src/vitable_connect/types/eligible_geographical_location.py delete mode 100644 src/vitable_connect/types/eligible_geographical_location_type.py delete mode 100644 src/vitable_connect/types/employee_address.py delete mode 100644 src/vitable_connect/types/employee_address_input.py delete mode 100644 src/vitable_connect/types/employee_id.py create mode 100644 src/vitable_connect/types/employee_list_enrollments_params.py delete mode 100644 src/vitable_connect/types/employee_list_response.py delete mode 100644 src/vitable_connect/types/employee_response.py create mode 100644 src/vitable_connect/types/employee_retrieve_response.py delete mode 100644 src/vitable_connect/types/employee_search.py delete mode 100644 src/vitable_connect/types/employee_status.py create mode 100644 src/vitable_connect/types/employee_update_params.py create mode 100644 src/vitable_connect/types/employee_update_response.py delete mode 100644 src/vitable_connect/types/employer_address.py delete mode 100644 src/vitable_connect/types/employer_address_input.py delete mode 100644 src/vitable_connect/types/employer_benefit_lifecycle_stage.py delete mode 100644 src/vitable_connect/types/employer_benefit_plan_year_response.py delete mode 100644 src/vitable_connect/types/employer_benefit_plan_years_list_response.py delete mode 100644 src/vitable_connect/types/employer_contact.py create mode 100644 src/vitable_connect/types/employer_create_params.py create mode 100644 src/vitable_connect/types/employer_ensure_payroll_integration_email_response.py delete mode 100644 src/vitable_connect/types/employer_hris.py delete mode 100644 src/vitable_connect/types/employer_hris_response.py delete mode 100644 src/vitable_connect/types/employer_hris_status.py delete mode 100644 src/vitable_connect/types/employer_id.py delete mode 100644 src/vitable_connect/types/employer_invoice_pdf.py delete mode 100644 src/vitable_connect/types/employer_invoice_pdf_response.py delete mode 100644 src/vitable_connect/types/employer_invoices_list_response.py create mode 100644 src/vitable_connect/types/employer_list_benefit_plan_year_enrollments_params.py create mode 100644 src/vitable_connect/types/employer_list_benefit_plan_year_enrollments_response.py create mode 100644 src/vitable_connect/types/employer_list_benefit_plan_years_response.py create mode 100644 src/vitable_connect/types/employer_list_employees_params.py create mode 100644 src/vitable_connect/types/employer_list_hris_providers_response.py create mode 100644 src/vitable_connect/types/employer_list_invoices_params.py create mode 100644 src/vitable_connect/types/employer_list_invoices_response.py create mode 100644 src/vitable_connect/types/employer_list_params.py create mode 100644 src/vitable_connect/types/employer_list_payroll_deduction_statements_params.py create mode 100644 src/vitable_connect/types/employer_list_payroll_deduction_statements_response.py create mode 100644 src/vitable_connect/types/employer_list_response.py delete mode 100644 src/vitable_connect/types/employer_payroll_deduction_statement_list_response.py create mode 100644 src/vitable_connect/types/employer_retrieve_benefit_plan_year_response.py create mode 100644 src/vitable_connect/types/employer_retrieve_hris_response.py create mode 100644 src/vitable_connect/types/employer_retrieve_invoice_pdf_response.py create mode 100644 src/vitable_connect/types/employer_retrieve_payroll_access_setup_response.py delete mode 100644 src/vitable_connect/types/employer_search.py delete mode 100644 src/vitable_connect/types/employer_settings.py delete mode 100644 src/vitable_connect/types/employer_settings_response.py create mode 100644 src/vitable_connect/types/employer_submit_census_sync_params.py create mode 100644 src/vitable_connect/types/employer_submit_census_sync_response.py create mode 100644 src/vitable_connect/types/employer_submit_payroll_access_setup_params.py create mode 100644 src/vitable_connect/types/employer_submit_payroll_access_setup_response.py create mode 100644 src/vitable_connect/types/employer_update_params.py create mode 100644 src/vitable_connect/types/employer_update_settings_params.py create mode 100644 src/vitable_connect/types/employer_update_settings_response.py delete mode 100644 src/vitable_connect/types/enrollment_detail.py delete mode 100644 src/vitable_connect/types/enrollment_id.py delete mode 100644 src/vitable_connect/types/enrollment_list_response.py delete mode 100644 src/vitable_connect/types/enrollment_rate.py delete mode 100644 src/vitable_connect/types/enrollment_rate_summary.py create mode 100644 src/vitable_connect/types/enrollment_reissue_params.py create mode 100644 src/vitable_connect/types/enrollment_reissue_response.py delete mode 100644 src/vitable_connect/types/enrollment_response.py create mode 100644 src/vitable_connect/types/enrollment_retrieve_response.py create mode 100644 src/vitable_connect/types/enrollment_terminate_params.py delete mode 100644 src/vitable_connect/types/error_response.py delete mode 100644 src/vitable_connect/types/event_id.py delete mode 100644 src/vitable_connect/types/event_name.py delete mode 100644 src/vitable_connect/types/gender.py delete mode 100644 src/vitable_connect/types/grant_type.py create mode 100644 src/vitable_connect/types/group_create_params.py delete mode 100644 src/vitable_connect/types/group_id.py create mode 100644 src/vitable_connect/types/group_list_params.py delete mode 100644 src/vitable_connect/types/group_list_response.py delete mode 100644 src/vitable_connect/types/group_member_sync_detail_response.py delete mode 100644 src/vitable_connect/types/group_member_sync_failure.py delete mode 100644 src/vitable_connect/types/group_member_sync_member_request.py delete mode 100644 src/vitable_connect/types/group_member_sync_request_detail.py delete mode 100644 src/vitable_connect/types/group_member_sync_request_detail_response.py delete mode 100644 src/vitable_connect/types/group_member_sync_response.py delete mode 100644 src/vitable_connect/types/group_member_sync_results.py create mode 100644 src/vitable_connect/types/group_update_params.py create mode 100644 src/vitable_connect/types/groups/__init__.py create mode 100644 src/vitable_connect/types/groups/members/__init__.py create mode 100644 src/vitable_connect/types/groups/members/sync_retrieve_response.py create mode 100644 src/vitable_connect/types/groups/members/sync_submit_params.py create mode 100644 src/vitable_connect/types/groups/members/sync_submit_response.py delete mode 100644 src/vitable_connect/types/household_member.py delete mode 100644 src/vitable_connect/types/household_member_relationship.py delete mode 100644 src/vitable_connect/types/household_member_type.py delete mode 100644 src/vitable_connect/types/household_members_response.py delete mode 100644 src/vitable_connect/types/hris_connection.py delete mode 100644 src/vitable_connect/types/hris_status_item.py delete mode 100644 src/vitable_connect/types/ichra_contribution_class.py delete mode 100644 src/vitable_connect/types/idp_provider.py delete mode 100644 src/vitable_connect/types/include_cancelled.py delete mode 100644 src/vitable_connect/types/invoice.py delete mode 100644 src/vitable_connect/types/invoice_id.py delete mode 100644 src/vitable_connect/types/invoices_cursor_pagination.py delete mode 100644 src/vitable_connect/types/limit.py delete mode 100644 src/vitable_connect/types/list_webhook_event_deliveries_response.py delete mode 100644 src/vitable_connect/types/marital_status.py delete mode 100644 src/vitable_connect/types/member.py delete mode 100644 src/vitable_connect/types/member_address.py delete mode 100644 src/vitable_connect/types/member_dependents_response.py delete mode 100644 src/vitable_connect/types/member_digital_benefit_cards_response.py delete mode 100644 src/vitable_connect/types/member_employments_response.py delete mode 100644 src/vitable_connect/types/member_enrollment.py delete mode 100644 src/vitable_connect/types/member_enrollments_response.py delete mode 100644 src/vitable_connect/types/member_id.py delete mode 100644 src/vitable_connect/types/member_legal_dependent.py create mode 100644 src/vitable_connect/types/member_list_dependents_response.py create mode 100644 src/vitable_connect/types/member_list_employments_response.py create mode 100644 src/vitable_connect/types/member_list_enrollments_response.py create mode 100644 src/vitable_connect/types/member_list_id_cards_response.py delete mode 100644 src/vitable_connect/types/member_list_item.py create mode 100644 src/vitable_connect/types/member_list_params.py create mode 100644 src/vitable_connect/types/member_list_qualifying_life_events_params.py create mode 100644 src/vitable_connect/types/member_list_qualifying_life_events_response.py delete mode 100644 src/vitable_connect/types/member_qualifying_life_event.py delete mode 100644 src/vitable_connect/types/member_qualifying_life_event_list_response.py delete mode 100644 src/vitable_connect/types/member_response.py create mode 100644 src/vitable_connect/types/member_retrieve_household_response.py create mode 100644 src/vitable_connect/types/member_retrieve_response.py delete mode 100644 src/vitable_connect/types/member_search.py delete mode 100644 src/vitable_connect/types/member_status.py delete mode 100644 src/vitable_connect/types/missing_employee_resolution.py delete mode 100644 src/vitable_connect/types/name_suffix.py delete mode 100644 src/vitable_connect/types/offset.py delete mode 100644 src/vitable_connect/types/operation.py delete mode 100644 src/vitable_connect/types/organization.py create mode 100644 src/vitable_connect/types/organization_create_params.py create mode 100644 src/vitable_connect/types/organization_create_response.py delete mode 100644 src/vitable_connect/types/organization_employer.py delete mode 100644 src/vitable_connect/types/organization_employer_list_response.py delete mode 100644 src/vitable_connect/types/organization_hris_provider.py delete mode 100644 src/vitable_connect/types/organization_hris_providers_response.py create mode 100644 src/vitable_connect/types/organization_list_response.py delete mode 100644 src/vitable_connect/types/organization_type.py delete mode 100644 src/vitable_connect/types/organizations_list_response.py delete mode 100644 src/vitable_connect/types/page.py delete mode 100644 src/vitable_connect/types/payroll_access_setup_status.py delete mode 100644 src/vitable_connect/types/payroll_access_setup_status_response.py delete mode 100644 src/vitable_connect/types/payroll_deduction_statement.py delete mode 100644 src/vitable_connect/types/payroll_integration_email.py delete mode 100644 src/vitable_connect/types/payroll_integration_email_response.py delete mode 100644 src/vitable_connect/types/plan.py create mode 100644 src/vitable_connect/types/plan_list_params.py delete mode 100644 src/vitable_connect/types/plan_year_enrollment.py delete mode 100644 src/vitable_connect/types/plan_year_enrollment_election_status.py delete mode 100644 src/vitable_connect/types/plan_year_enrollment_list_response.py delete mode 100644 src/vitable_connect/types/plan_year_enrollment_policy_status.py delete mode 100644 src/vitable_connect/types/preferred_language.py delete mode 100644 src/vitable_connect/types/product_code.py delete mode 100644 src/vitable_connect/types/qualifying_life_event_status.py delete mode 100644 src/vitable_connect/types/qualifying_life_event_type.py delete mode 100644 src/vitable_connect/types/reissue_enrollment.py delete mode 100644 src/vitable_connect/types/reissue_enrollment_response.py delete mode 100644 src/vitable_connect/types/relationship.py delete mode 100644 src/vitable_connect/types/remaining_employee_action.py delete mode 100644 src/vitable_connect/types/request_id.py delete mode 100644 src/vitable_connect/types/resource_id.py delete mode 100644 src/vitable_connect/types/resource_type.py delete mode 100644 src/vitable_connect/types/search.py delete mode 100644 src/vitable_connect/types/sex_at_birth.py delete mode 100644 src/vitable_connect/types/state.py delete mode 100644 src/vitable_connect/types/status.py delete mode 100644 src/vitable_connect/types/tax_classification.py delete mode 100644 src/vitable_connect/types/termination_rule.py delete mode 100644 src/vitable_connect/types/update_employer_address_input.py delete mode 100644 src/vitable_connect/types/waiting_period.py delete mode 100644 src/vitable_connect/types/webhook_delivery.py create mode 100644 src/vitable_connect/types/webhook_event_list_deliveries_response.py create mode 100644 src/vitable_connect/types/webhook_event_list_params.py delete mode 100644 src/vitable_connect/types/webhook_event_list_response.py delete mode 100644 src/vitable_connect/types/webhook_event_response.py create mode 100644 src/vitable_connect/types/webhook_event_retrieve_response.py delete mode 100644 src/vitable_connect/version.py delete mode 100644 src/vitable_connect/webhook_events/__init__.py delete mode 100644 src/vitable_connect/webhook_events/client.py delete mode 100644 src/vitable_connect/webhook_events/raw_client.py create mode 100644 src/vitable_connect_api/lib/.keep create mode 100644 src/vitable_partner_api/lib/.keep create mode 100644 tests/__init__.py create mode 100644 tests/api_resources/__init__.py create mode 100644 tests/api_resources/groups/__init__.py create mode 100644 tests/api_resources/groups/members/__init__.py create mode 100644 tests/api_resources/groups/members/test_sync.py create mode 100644 tests/api_resources/test_auth.py create mode 100644 tests/api_resources/test_employees.py create mode 100644 tests/api_resources/test_employers.py create mode 100644 tests/api_resources/test_enrollments.py create mode 100644 tests/api_resources/test_groups.py create mode 100644 tests/api_resources/test_members.py create mode 100644 tests/api_resources/test_organizations.py create mode 100644 tests/api_resources/test_plans.py create mode 100644 tests/api_resources/test_webhook_events.py delete mode 100644 tests/custom/test_client.py create mode 100644 tests/sample_file.txt delete mode 100644 tests/test_aiohttp_autodetect.py create mode 100644 tests/test_client.py create mode 100644 tests/test_extract_files.py create mode 100644 tests/test_files.py create mode 100644 tests/test_models.py create mode 100644 tests/test_qs.py create mode 100644 tests/test_required_args.py create mode 100644 tests/test_response.py create mode 100644 tests/test_streaming.py create mode 100644 tests/test_transform.py create mode 100644 tests/test_utils/test_datetime_parse.py create mode 100644 tests/test_utils/test_json.py create mode 100644 tests/test_utils/test_path.py create mode 100644 tests/test_utils/test_proxy.py create mode 100644 tests/test_utils/test_typing.py create mode 100644 tests/utils.py delete mode 100644 tests/utils/__init__.py delete mode 100644 tests/utils/assets/models/__init__.py delete mode 100644 tests/utils/assets/models/circle.py delete mode 100644 tests/utils/assets/models/color.py delete mode 100644 tests/utils/assets/models/object_with_defaults.py delete mode 100644 tests/utils/assets/models/object_with_optional_field.py delete mode 100644 tests/utils/assets/models/shape.py delete mode 100644 tests/utils/assets/models/square.py delete mode 100644 tests/utils/assets/models/undiscriminated_shape.py delete mode 100644 tests/utils/test_http_client.py delete mode 100644 tests/utils/test_query_encoding.py delete mode 100644 tests/utils/test_serialization.py create mode 100644 uv.lock diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..62c2d13 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,8 @@ +ARG VARIANT="3.9" +FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} + +USER vscode + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +RUN echo "[[ -d .venv ]] && source .venv/bin/activate || export PATH=\$PATH" >> /home/vscode/.bashrc diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..e01283d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,43 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/debian +{ + "name": "Debian", + "build": { + "dockerfile": "Dockerfile", + "context": ".." + }, + + "postStartCommand": "uv sync --all-extras", + + "customizations": { + "vscode": { + "extensions": [ + "ms-python.python" + ], + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "python.pythonPath": ".venv/bin/python", + "python.defaultInterpreterPath": ".venv/bin/python", + "python.typeChecking": "basic", + "terminal.integrated.env.linux": { + "PATH": "${env:PATH}" + } + } + } + }, + "features": { + "ghcr.io/devcontainers/features/node:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.fern/metadata.json b/.fern/metadata.json deleted file mode 100644 index eaef8ab..0000000 --- a/.fern/metadata.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "cliVersion": "5.112.1", - "generatorName": "fernapi/fern-python-sdk", - "generatorVersion": "5.29.3", - "generatorConfig": { - "package_name": "vitable_connect", - "client_class_name": "VitableConnect", - "offset_semantics": "page-index", - "pydantic_config": { - "union_naming": "v1" - } - }, - "originGitCommit": "e710ee21f093ef71854fc1636c39553fa760d026", - "originGitCommitIsDirty": false, - "invokedBy": "ci", - "requestedVersion": "AUTO", - "ciProvider": "github", - "sdkVersion": "1.0.1" -} \ No newline at end of file diff --git a/.fernignore b/.fernignore index fa4239f..1808d93 100644 --- a/.fernignore +++ b/.fernignore @@ -1,10 +1,4 @@ # Files Fern must not overwrite or delete when regenerating this repo. LICENSE SECURITY.md -changelog.md -# Owned publishing: the PyPI trusted publisher registration is pinned to -# publish-pypi.yml + the `prod` environment and we lack owner access on the -# PyPI project to change it. ci.yml is Fern's copy with its publish job -# (environment `pypi`) stripped so it doesn't fail OIDC on every tag. -.github/workflows/ci.yml .github/workflows/publish-pypi.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3929ff..dc9a787 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,76 +1,95 @@ -# Fern's generated ci.yml, minus its publish job: that job publishes via a -# trusted publisher registered for ci.yml + environment `pypi`, which we can't -# create (no owner access on the PyPI project). Publishing happens in -# publish-pypi.yml instead, matching the existing registration. -# This file is listed in .fernignore so Fern regeneration preserves it. -name: ci - -on: [push] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false +name: CI +on: + push: + branches: + - '**' + - '!integrated/**' + - '!stl-preview-head/**' + - '!stl-preview-base/**' + - '!generated' + - '!codegen/**' + - 'codegen/stl/**' + pull_request: + branches-ignore: + - 'stl-preview-head/**' + - 'stl-preview-base/**' jobs: - compile: - runs-on: ubuntu-latest + lint: + timeout-minutes: 10 + name: lint + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') steps: - - name: Checkout repo - uses: actions/checkout@v4 - - name: Set up python - uses: actions/setup-python@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: - python-version: "3.10" - - name: Bootstrap poetry - run: | - curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + version: '0.10.2' + - name: Install dependencies - run: poetry install - - name: Compile - run: poetry run mypy . - test: - runs-on: ubuntu-latest + run: uv sync --all-extras + + - name: Run lints + run: ./scripts/lint + + build: + if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata') + timeout-minutes: 10 + name: build + permissions: + contents: read + id-token: write + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - name: Checkout repo - uses: actions/checkout@v4 - - name: Set up python - uses: actions/setup-python@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: - python-version: "3.10" - - name: Bootstrap poetry - run: | - curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 + version: '0.10.2' + - name: Install dependencies - run: poetry install + run: uv sync --all-extras - - name: Test - run: poetry run pytest -rP -n auto . + - name: Run build + run: uv build - - name: Install aiohttp extra - run: poetry install --extras aiohttp + - name: Get GitHub OIDC Token + if: |- + github.repository == 'stainless-sdks/vitable-connect-python' && + !startsWith(github.ref, 'refs/heads/stl/') + id: github-oidc + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: core.setOutput('github_token', await core.getIDToken()); - - name: Test (aiohttp) - run: poetry run pytest -rP -n auto -m aiohttp . + - name: Upload tarball + if: |- + github.repository == 'stainless-sdks/vitable-connect-python' && + !startsWith(github.ref, 'refs/heads/stl/') + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + run: ./scripts/utils/upload-artifact.sh - build: - name: Build distribution - runs-on: ubuntu-latest + test: + timeout-minutes: 10 + name: test + runs-on: ${{ startsWith(github.repository, 'stainless-sdks/') && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - name: Checkout repo - uses: actions/checkout@v4 - - name: Set up python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - name: Bootstrap poetry - run: | - curl -sSL https://install.python-poetry.org | python - -y --version 1.5.1 - - name: Install dependencies - run: poetry install - - name: Build package - run: poetry build - - name: Store the distribution packages - uses: actions/upload-artifact@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install uv + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 with: - name: python-package-distributions - path: dist/ + version: '0.10.2' + + - name: Bootstrap + run: ./scripts/bootstrap + + - name: Run tests + run: ./scripts/test diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 65e4209..686c49c 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,20 +1,13 @@ -# Publishes the Fern-generated SDK to PyPI via OIDC trusted publishing. The -# PyPI project trusts this repo + this workflow file + the `prod` environment; -# do not rename any of the three without updating the registration. +# Publishes the Fern-generated SDK to PyPI via OIDC trusted publishing (the +# PyPI project trusts this repo + workflow file + `prod` environment; no +# token). Triggered when a GitHub release is published, or manually to +# re-publish after a failure. # -# Triggers on tag pushes because Fern (github mode: release) creates tags, not -# GitHub releases. This file is listed in .fernignore so Fern regeneration -# preserves it. +# This file is listed in .fernignore so Fern regeneration preserves it. name: Publish PyPI on: workflow_dispatch: - push: - tags: - # Fern tags bare semver (1.0.1); v-prefixed kept for manual tags. - - "[0-9]*" - - "v[0-9]*" - release: types: [published] diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml new file mode 100644 index 0000000..a3c3301 --- /dev/null +++ b/.github/workflows/release-doctor.yml @@ -0,0 +1,19 @@ +name: Release Doctor +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + release_doctor: + name: release doctor + runs-on: ubuntu-latest + if: github.repository == 'Vitable-Inc/vitable-connect-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Check release environment + run: | + bash ./bin/check-release-environment diff --git a/.gitignore b/.gitignore index d2e4ca8..3824f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,16 @@ -.mypy_cache/ -.ruff_cache/ -__pycache__/ -dist/ -poetry.toml +.prism.log +.stdy.log +_dev + +__pycache__ +.mypy_cache + +dist + +.venv +.idea + +.env +.envrc +codegen.log +Brewfile.lock.json diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..43077b2 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.9.18 diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..6d78745 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.9.0" +} \ No newline at end of file diff --git a/.stats.yml b/.stats.yml new file mode 100644 index 0000000..19c28af --- /dev/null +++ b/.stats.yml @@ -0,0 +1,4 @@ +configured_endpoints: 45 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/vitable/vitable-connect-c3a225d6201981c24180c285dd0d8a6ed97544f9e4e6a734130ead4231063f5e.yml +openapi_spec_hash: 05748169a4747ace4fcb717d9c87458c +config_hash: 0b4d62a9a1e0cc71998bcfc3d15c4db7 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5b01030 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.importFormat": "relative", +} diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000..c43041c --- /dev/null +++ b/Brewfile @@ -0,0 +1,2 @@ +brew "uv" + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3311c5f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,223 @@ +# Changelog + +## 0.9.0 (2026-08-20) + +Full Changelog: [v0.8.0...v0.9.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.8.0...v0.9.0) + +### Features + +* **api:** api update ([7412f2e](https://github.com/Vitable-Inc/vitable-connect-python/commit/7412f2e62688341085b2d241cddbea3cdf1d1da7)) +* **api:** api update ([2509ddc](https://github.com/Vitable-Inc/vitable-connect-python/commit/2509ddcb6ff5eb1d439bfb9a56e54b95e60a1d9d)) +* **api:** api update ([cd74e20](https://github.com/Vitable-Inc/vitable-connect-python/commit/cd74e20f7384394276ff090d91171f8f24527c3e)) +* **api:** api update ([11e1453](https://github.com/Vitable-Inc/vitable-connect-python/commit/11e1453884a7e36e30bd1e9b631be3ff973440eb)) +* **api:** api update ([a0f88e6](https://github.com/Vitable-Inc/vitable-connect-python/commit/a0f88e6af1065f98e613a1ac333795d28fb91970)) +* **api:** api update ([6ca7ac5](https://github.com/Vitable-Inc/vitable-connect-python/commit/6ca7ac5bbf038dcfcb120fcf78090f40635c8788)) +* **api:** api update ([6de883d](https://github.com/Vitable-Inc/vitable-connect-python/commit/6de883dae776c613f6d315e52c4fb681a57a4eb8)) +* **api:** api update ([abe2e5c](https://github.com/Vitable-Inc/vitable-connect-python/commit/abe2e5c77f7dbc7108edc2c6004c0fbebf16da5b)) +* **api:** api update ([7a08626](https://github.com/Vitable-Inc/vitable-connect-python/commit/7a08626418c51b6c2ebd92b907c30d9a4ede81bc)) +* **api:** api update ([1275787](https://github.com/Vitable-Inc/vitable-connect-python/commit/1275787206e5ab26299cb68641ec8ed5e068ef61)) +* **api:** api update ([8fa7020](https://github.com/Vitable-Inc/vitable-connect-python/commit/8fa70208d70951f8b6f9e696f7206d9609b251d2)) +* **api:** configure all public v1 endpoints and drop dangling Type model ref ([6dc6ff2](https://github.com/Vitable-Inc/vitable-connect-python/commit/6dc6ff2b67ace879553a99526e479912c2b3ff41)) +* **stlc:** configurable CI runner and private-production-repo support in workflow templates ([e6d3d00](https://github.com/Vitable-Inc/vitable-connect-python/commit/e6d3d00dd86edcf327d4d207ec518cce9c0c7dbc)) + + +### Bug Fixes + +* **client:** bind IdentityProviderBearer to an identity_token client option ([de364d8](https://github.com/Vitable-Inc/vitable-connect-python/commit/de364d88d90d7b1e8c7b7815ad5904bb6de0dbba)) +* **internal:** resolve build failures ([d424005](https://github.com/Vitable-Inc/vitable-connect-python/commit/d424005af9acf931ea77e97fb139718237e55ede)) + + +### Refactors + +* **api:** drop the IdP session endpoints from the SDK surface ([2a24109](https://github.com/Vitable-Inc/vitable-connect-python/commit/2a24109cd514c9ff714ee167641b36f4cccc3a8e)) + +## 0.8.0 (2026-06-18) + +Full Changelog: [v0.7.0...v0.8.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.7.0...v0.8.0) + +### Features + +* **api:** api update ([33d4265](https://github.com/Vitable-Inc/vitable-connect-python/commit/33d426524714ca7cc4c1ca204b990add997ed3f3)) +* **api:** api update ([1152e00](https://github.com/Vitable-Inc/vitable-connect-python/commit/1152e00b0ff6aaf7bea6bffd6d1f6009f1b398ec)) +* **api:** api update ([4ca546b](https://github.com/Vitable-Inc/vitable-connect-python/commit/4ca546b8a409cb40c849443b490133a3478761cf)) +* **api:** api update ([72d79d6](https://github.com/Vitable-Inc/vitable-connect-python/commit/72d79d6b7db386c4d71707f516e6cb758d06a847)) + + +### Bug Fixes + +* **auth:** prioritize first auth header ([188eb11](https://github.com/Vitable-Inc/vitable-connect-python/commit/188eb113fc26e5e2cddb83267ab7c330c059ce67)) + +## 0.7.0 (2026-05-15) + +Full Changelog: [v0.6.0...v0.7.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.6.0...v0.7.0) + +### Features + +* **api:** api update ([be238bd](https://github.com/Vitable-Inc/vitable-connect-python/commit/be238bdfa91087968a214e85c1b1f2e3ae3956ba)) +* **api:** api update ([d2cf53f](https://github.com/Vitable-Inc/vitable-connect-python/commit/d2cf53f72ca61c63bea8aa21512153d65dac5267)) +* **api:** manual updates ([d3b3b22](https://github.com/Vitable-Inc/vitable-connect-python/commit/d3b3b226b1f3604bb0bd209e1fd8d5ce6e568346)) +* **internal/types:** support eagerly validating pydantic iterators ([a5abdda](https://github.com/Vitable-Inc/vitable-connect-python/commit/a5abddaf5c4d164c0207fd9df357a2f663fd50e3)) +* support setting headers via env ([3ca51fa](https://github.com/Vitable-Inc/vitable-connect-python/commit/3ca51fa4ae7ec750ee3a82fa9f8551de2d73c705)) + + +### Bug Fixes + +* **client:** add missing f-string prefix in file type error message ([004c0b9](https://github.com/Vitable-Inc/vitable-connect-python/commit/004c0b9a537063e354d79079b9927559be504392)) +* use correct field name format for multipart file arrays ([6e86a19](https://github.com/Vitable-Inc/vitable-connect-python/commit/6e86a1953203a86f6cf66ede8e4e296de047eb8e)) + + +### Chores + +* **internal:** more robust bootstrap script ([0a150fe](https://github.com/Vitable-Inc/vitable-connect-python/commit/0a150fea1af9f9c0e41ef27d8b3dd87b16e20900)) +* **internal:** reformat pyproject.toml ([3547294](https://github.com/Vitable-Inc/vitable-connect-python/commit/3547294e7a4bb987f8b7ef66aebd612bdbfb22f5)) + +## 0.6.0 (2026-04-18) + +Full Changelog: [v0.5.0...v0.6.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.5.0...v0.6.0) + +### Features + +* **api:** api update ([ce09b27](https://github.com/Vitable-Inc/vitable-connect-python/commit/ce09b27923e4d550edf0090b17de2471a2b9487e)) + + +### Bug Fixes + +* **client:** preserve hardcoded query params when merging with user params ([a146ca8](https://github.com/Vitable-Inc/vitable-connect-python/commit/a146ca8b4396978eb085d9e35669b8de9b88eea6)) +* ensure file data are only sent as 1 parameter ([4f40996](https://github.com/Vitable-Inc/vitable-connect-python/commit/4f409963112717020cd8f0cd8a9cfd3938e56fc8)) + + +### Performance Improvements + +* **client:** optimize file structure copying in multipart requests ([ef0dd2c](https://github.com/Vitable-Inc/vitable-connect-python/commit/ef0dd2c111eb6c05a486150ea8b6243768b0d7d9)) + +## 0.5.0 (2026-03-27) + +Full Changelog: [v0.4.0...v0.5.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.4.0...v0.5.0) + +### Features + +* **internal:** implement indices array format for query and form serialization ([20179b1](https://github.com/Vitable-Inc/vitable-connect-python/commit/20179b1aa30918ed29ef03f6e577546a893f9a7a)) + +## 0.4.0 (2026-03-25) + +Full Changelog: [v0.3.0...v0.4.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.3.0...v0.4.0) + +### Features + +* **api:** api update ([cbeb23a](https://github.com/Vitable-Inc/vitable-connect-python/commit/cbeb23af9f69f3d6601a572ca1ad477e3a0a7604)) + + +### Chores + +* **ci:** skip lint on metadata-only changes ([ae2ea7a](https://github.com/Vitable-Inc/vitable-connect-python/commit/ae2ea7a1799256a5647a1361e5290f1ac0a3053f)) + +## 0.3.0 (2026-03-24) + +Full Changelog: [v0.2.0...v0.3.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.2.0...v0.3.0) + +### Features + +* **api:** api update ([79cb703](https://github.com/Vitable-Inc/vitable-connect-python/commit/79cb7035cf622d851378bd2ddef1522aa47a0982)) + + +### Chores + +* **internal:** update gitignore ([46e8e0a](https://github.com/Vitable-Inc/vitable-connect-python/commit/46e8e0a89c1202cbd204fac742eee4418bf29957)) + +## 0.2.0 (2026-03-20) + +Full Changelog: [v0.1.2...v0.2.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.1.2...v0.2.0) + +### Features + +* **api:** api update ([2d17eba](https://github.com/Vitable-Inc/vitable-connect-python/commit/2d17ebab7f730aa1a9a9cba0044d7bd1f05ef3d7)) +* **api:** manual updates ([61e9959](https://github.com/Vitable-Inc/vitable-connect-python/commit/61e9959f83e9d2de1c974e9d04349efba0c03589)) +* **api:** manual updates ([311d294](https://github.com/Vitable-Inc/vitable-connect-python/commit/311d294e50fb62fde627e0e0619064ff05018cb7)) +* **api:** manual updates ([81fa03e](https://github.com/Vitable-Inc/vitable-connect-python/commit/81fa03e7036aeb56052384dfbd31de5f554d2d74)) + + +### Bug Fixes + +* **deps:** bump minimum typing-extensions version ([4f315a4](https://github.com/Vitable-Inc/vitable-connect-python/commit/4f315a4488437d128364f288ae1a2e526aceb639)) +* **pydantic:** do not pass `by_alias` unless set ([7d698a1](https://github.com/Vitable-Inc/vitable-connect-python/commit/7d698a13ec079a12c7681ce236abf4aa9ea765fa)) +* sanitize endpoint path params ([5d252df](https://github.com/Vitable-Inc/vitable-connect-python/commit/5d252df080bd65d8227cff938a47fc2c0be43c64)) + + +### Chores + +* **internal:** tweak CI branches ([96b210d](https://github.com/Vitable-Inc/vitable-connect-python/commit/96b210d9f4ace8c61d48775f1cf840e8fcb18d44)) +* update SDK settings ([b5d7c3a](https://github.com/Vitable-Inc/vitable-connect-python/commit/b5d7c3a4a61e8b7beb84a7c2c184d1c4237b324a)) +* update SDK settings ([5749036](https://github.com/Vitable-Inc/vitable-connect-python/commit/57490368bcb31217dc0c9ca08d7757f1c3f03400)) +* update SDK settings ([f29a924](https://github.com/Vitable-Inc/vitable-connect-python/commit/f29a9244a92c5205cd978c2adc176b9428c0be4f)) + +## 0.1.2 (2026-03-15) + +Full Changelog: [v0.1.1...v0.1.2](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.1.1...v0.1.2) + +### Chores + +* update SDK settings ([edacf2c](https://github.com/Vitable-Inc/vitable-connect-python/commit/edacf2c85b5c78a54c3691223d181da0c4b2d27e)) +* update SDK settings ([5462ee2](https://github.com/Vitable-Inc/vitable-connect-python/commit/5462ee2a354f9d756ba2e03cdca95ab44850e352)) + +## 0.1.1 (2026-03-15) + +Full Changelog: [v0.1.0...v0.1.1](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.1.0...v0.1.1) + +### Chores + +* update SDK settings ([cd3b35f](https://github.com/Vitable-Inc/vitable-connect-python/commit/cd3b35f6f860a3e41f868098eaed4cfe39f9812d)) +* update SDK settings ([375f1e8](https://github.com/Vitable-Inc/vitable-connect-python/commit/375f1e81bf537e90bd4d54d41e433dbf8560a2e4)) + +## 0.1.0 (2026-03-15) + +Full Changelog: [v0.0.1...v0.1.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.0.1...v0.1.0) + +### Features + +* **api:** manual updates ([667eed2](https://github.com/Vitable-Inc/vitable-connect-python/commit/667eed26c19537612e1d945523aa9b782fa0d83d)) +* **api:** manual updates ([ee1b326](https://github.com/Vitable-Inc/vitable-connect-python/commit/ee1b326e854727d73d0bec48c97d4dc680d06cfb)) +* **api:** manual updates ([c6773ab](https://github.com/Vitable-Inc/vitable-connect-python/commit/c6773abcca47609d1b5a7a9ccbeaffb64bf78194)) +* **api:** manual updates ([24510e4](https://github.com/Vitable-Inc/vitable-connect-python/commit/24510e4c0305594da91455e032d359e8ff41e7a6)) +* **api:** manual updates for 0.0.1 ([b6507fe](https://github.com/Vitable-Inc/vitable-connect-python/commit/b6507fee183f47cef7327a71f8b4949686cf0053)) + + +### Bug Fixes + +* ensure streams are always closed ([7fcce2a](https://github.com/Vitable-Inc/vitable-connect-python/commit/7fcce2af30d77a8fe4cfc387ac553e3a14aa1cb2)) +* **types:** allow pyright to infer TypedDict types within SequenceNotStr ([01c6584](https://github.com/Vitable-Inc/vitable-connect-python/commit/01c6584018f20aef86cf2a2a0ee2ac811898a574)) +* use async_to_httpx_files in patch method ([4aa431e](https://github.com/Vitable-Inc/vitable-connect-python/commit/4aa431ec74d320c83e718980a9a74322223696b9)) + + +### Chores + +* add missing docstrings ([7486482](https://github.com/Vitable-Inc/vitable-connect-python/commit/7486482e46d7b866a10328c7537769f44249c37b)) +* **ci:** bump uv version ([74f7244](https://github.com/Vitable-Inc/vitable-connect-python/commit/74f724414d1805a0dfc1eab672b7e9fb21c48b66)) +* **ci:** skip uploading artifacts on stainless-internal branches ([24d02a6](https://github.com/Vitable-Inc/vitable-connect-python/commit/24d02a6f8bdcb6f2de398c51c673c078a5ccc87a)) +* configure new SDK language ([ccd61da](https://github.com/Vitable-Inc/vitable-connect-python/commit/ccd61dad2038a70f73ec60220e89675532ed5e84)) +* **deps:** mypy 1.18.1 has a regression, pin to 1.17 ([39137b7](https://github.com/Vitable-Inc/vitable-connect-python/commit/39137b70fb97c358e6c44769544088f70e7ccd6a)) +* **docs:** use environment variables for authentication in code snippets ([a3caad5](https://github.com/Vitable-Inc/vitable-connect-python/commit/a3caad5c7ef028623891bf3f6a6850db2c6aba97)) +* format all `api.md` files ([55cca23](https://github.com/Vitable-Inc/vitable-connect-python/commit/55cca23dd91a01c58dda3b1a3f37f8c438819d00)) +* **internal:** add `--fix` argument to lint script ([040488a](https://github.com/Vitable-Inc/vitable-connect-python/commit/040488a8f0fbbacb81dbed57d823b6f421deb8de)) +* **internal:** add missing files argument to base client ([94f5cc6](https://github.com/Vitable-Inc/vitable-connect-python/commit/94f5cc6a2609b47ccd42ecb9a4226494719dfe54)) +* **internal:** add request options to SSE classes ([e77e21d](https://github.com/Vitable-Inc/vitable-connect-python/commit/e77e21d0d5ec96ab7f8503cbc157b386157d8f54)) +* **internal:** bump dependencies ([79fc12e](https://github.com/Vitable-Inc/vitable-connect-python/commit/79fc12e83874b482cf239ee27f08c82d981ee950)) +* **internal:** codegen related update ([f23f0ff](https://github.com/Vitable-Inc/vitable-connect-python/commit/f23f0ff38f78f92553e8e7035bf69c6507abe9a1)) +* **internal:** codegen related update ([d2aa4f5](https://github.com/Vitable-Inc/vitable-connect-python/commit/d2aa4f53016ac31f848e46e9eadcd313ea5e38be)) +* **internal:** fix lint error on Python 3.14 ([d4044ca](https://github.com/Vitable-Inc/vitable-connect-python/commit/d4044caa72fceda0c4fe1523461ae2164bbc98fb)) +* **internal:** make `test_proxy_environment_variables` more resilient ([92eae8a](https://github.com/Vitable-Inc/vitable-connect-python/commit/92eae8a46d8d2214190500bb982459399f3c5e7f)) +* **internal:** make `test_proxy_environment_variables` more resilient to env ([e2cbec5](https://github.com/Vitable-Inc/vitable-connect-python/commit/e2cbec575768adbf8f3ec70ad14f89d5600389b5)) +* **internal:** refactor authentication internals ([5050fe1](https://github.com/Vitable-Inc/vitable-connect-python/commit/5050fe1d1bc945a5f73f7e89390730179c9ed806)) +* **internal:** remove mock server code ([a611093](https://github.com/Vitable-Inc/vitable-connect-python/commit/a611093dbe8d79e6d87e5535b503391fb72240e2)) +* speedup initial import ([5c171ba](https://github.com/Vitable-Inc/vitable-connect-python/commit/5c171ba86cde9e105740eef77b56a2d4775dfa4e)) +* update lockfile ([46fd22c](https://github.com/Vitable-Inc/vitable-connect-python/commit/46fd22c853059f80119d94490b13bd2b97289af1)) +* update mock server docs ([2c8915c](https://github.com/Vitable-Inc/vitable-connect-python/commit/2c8915c6ad19db22f33e7cd52ea729615e009e58)) +* update SDK settings ([4bde5c3](https://github.com/Vitable-Inc/vitable-connect-python/commit/4bde5c377461c595665bdbd3e13cf8a3c837d0b2)) +* update SDK settings ([299f91c](https://github.com/Vitable-Inc/vitable-connect-python/commit/299f91c1cba012878dd76f104358521f0ca6a29f)) +* update SDK settings ([e4ab81a](https://github.com/Vitable-Inc/vitable-connect-python/commit/e4ab81a88c4304bd917e8a2884f25162629b9278)) +* update SDK settings ([25bf803](https://github.com/Vitable-Inc/vitable-connect-python/commit/25bf803a5aea7fc821e4bf517ba18416dfab6039)) +* update SDK settings ([fca5cc6](https://github.com/Vitable-Inc/vitable-connect-python/commit/fca5cc63014e2d7c1eb74f93c951e29a1875855c)) +* update SDK settings ([c331e7e](https://github.com/Vitable-Inc/vitable-connect-python/commit/c331e7e60ebd406a89efbcb955600071a79d7580)) +* update SDK settings ([7ff81a7](https://github.com/Vitable-Inc/vitable-connect-python/commit/7ff81a719f5846cadf2fdea414bdaf232d22d8bf)) +* update SDK settings ([0203a28](https://github.com/Vitable-Inc/vitable-connect-python/commit/0203a283b17e4ddaceb1c630a89e832e0d057856)) +* update SDK settings ([ecb955c](https://github.com/Vitable-Inc/vitable-connect-python/commit/ecb955c5da5fe3f860b9a9471ee2bd279e5a2ce2)) +* update SDK settings ([667632c](https://github.com/Vitable-Inc/vitable-connect-python/commit/667632ccdf1a6d62ecc13a21b7abb26fb2944b88)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index af948ce..d86e412 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,125 +1,121 @@ -# Contributing +## Setting up the environment -Thanks for your interest in contributing to this SDK! This document provides guidelines for contributing to the project. +### With `uv` -## Getting Started +We use [uv](https://docs.astral.sh/uv/) to manage dependencies because it will automatically provision a Python environment with the expected Python version. To set it up, run: -### Prerequisites - -- Python 3.9+ -- pip -- poetry - -### Installation +```sh +$ ./scripts/bootstrap +``` -Install the project dependencies: +Or [install uv manually](https://docs.astral.sh/uv/getting-started/installation/) and run: -```bash -poetry install +```sh +$ uv sync --all-extras ``` -### Building +You can then run scripts using `uv run python script.py` or by manually activating the virtual environment: -Build the project: +```sh +# manually activate - https://docs.python.org/3/library/venv.html#how-venvs-work +$ source .venv/bin/activate -```bash -poetry build +# now you can omit the `uv run` prefix +$ python script.py ``` -### Testing +### Without `uv` -Run the test suite: +Alternatively if you don't want to install `uv`, you can stick with the standard `pip` setup by ensuring you have the Python version specified in `.python-version`, create a virtual environment however you desire and then install dependencies using this command: -```bash -poetry run pytest +```sh +$ pip install -r requirements-dev.lock ``` -### Linting and Formatting +## Modifying/Adding code -Check code style: +Most of the SDK is generated code. Modifications to code will be persisted between generations, but may +result in merge conflicts between manual patches and changes from the generator. The generator will never +modify the contents of the `src/vitable_connect/lib/` and `examples/` directories. -```bash -poetry run ruff check . -poetry run ruff format . -``` +## Adding and running examples -### Type Checking +All files in the `examples/` directory are not modified by the generator and can be freely edited or added to. -Run the type checker: +```py +# add an example to examples/.py -```bash -poetry run mypy . +#!/usr/bin/env -S uv run python +… ``` -## About Generated Code - -**Important**: Most files in this SDK are automatically generated by [Fern](https://buildwithfern.com) from the API definition. Direct modifications to generated files will be overwritten the next time the SDK is generated. - -### Generated Files +```sh +$ chmod +x examples/.py +# run the example against your api +$ ./examples/.py +``` -The following directories contain generated code: -- `src/` - API client classes and types -- Most Python files in the project +## Using the repository from source -### How to Customize +If you’d like to use the repository from source, you can either install from git or link to a cloned repository: -If you need to customize the SDK, you have two options: +To install via git: -#### Option 1: Use `.fernignore` +```sh +$ pip install git+ssh://git@github.com/Vitable-Inc/vitable-connect-python.git +``` -For custom code that should persist across SDK regenerations: +Alternatively, you can build from source and install the wheel file: -1. Create a `.fernignore` file in the project root -2. Add file patterns for files you want to preserve (similar to `.gitignore` syntax) -3. Add your custom code to those files +Building this package will create two files in the `dist/` directory, a `.tar.gz` containing the source files and a `.whl` that can be used to install the package efficiently. -Files listed in `.fernignore` will not be overwritten when the SDK is regenerated. +To create a distributable version of the library, all you have to do is run this command: -For more information, see the [Fern documentation on custom code](https://buildwithfern.com/learn/sdks/overview/custom-code). +```sh +$ uv build +# or +$ python -m build +``` -#### Option 2: Contribute to the Generator +Then to install: -If you want to change how code is generated for all users of this SDK: +```sh +$ pip install ./path-to-wheel-file.whl +``` -1. The Python SDK generator lives in the [Fern repository](https://github.com/fern-api/fern) -2. Generator code is located at `generators/python-v2/` -3. Follow the [Fern contributing guidelines](https://github.com/fern-api/fern/blob/main/CONTRIBUTING.md) -4. Submit a pull request with your changes to the generator +## Running tests -This approach is best for: -- Bug fixes in generated code -- New features that would benefit all users -- Improvements to code generation patterns +```sh +$ ./scripts/test +``` -## Making Changes +## Linting and formatting -### Workflow +This repository uses [ruff](https://github.com/astral-sh/ruff) and +[black](https://github.com/psf/black) to format the code in the repository. -1. Create a new branch for your changes -2. Make your modifications -3. Run tests to ensure nothing breaks: `poetry run pytest` -4. Run linting and formatting: `poetry run ruff check .` and `poetry run ruff format .` -5. Run type checking: `poetry run mypy .` -6. Build the project: `poetry build` -7. Commit your changes with a clear commit message -8. Push your branch and create a pull request +To lint: -### Commit Messages +```sh +$ ./scripts/lint +``` -Write clear, descriptive commit messages that explain what changed and why. +To format and fix all ruff issues automatically: -### Code Style +```sh +$ ./scripts/format +``` -This project uses automated code formatting and linting. Run `poetry run ruff format .` and `poetry run ruff check .` before committing to ensure your code meets the project's style guidelines. +## Publishing and releases -## Questions or Issues? +Changes made to this repository via the automated release PR pipeline should publish to PyPI automatically. If +the changes aren't made through the automated pipeline, you may want to make releases manually. -If you have questions or run into issues: +### Publish with a GitHub workflow -1. Check the [Fern documentation](https://buildwithfern.com) -2. Search existing [GitHub issues](https://github.com/fern-api/fern/issues) -3. Open a new issue if your question hasn't been addressed +You can release to package managers by using [the `Publish PyPI` GitHub action](https://www.github.com/Vitable-Inc/vitable-connect-python/actions/workflows/publish-pypi.yml). This requires a setup organization or repository secret to be set up. -## License +### Publish manually -By contributing to this project, you agree that your contributions will be licensed under the same license as the project. +If you need to manually release a package, you can run the `bin/publish-pypi` script with a `PYPI_TOKEN` set on +the environment. diff --git a/README.md b/README.md index c204dde..c2acb4b 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,13 @@ -# Vitable Connect Python Library - -[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2FVitable-Inc%2Fvitable-connect-python) -[![pypi](https://img.shields.io/pypi/v/vitable-connect)](https://pypi.python.org/pypi/vitable-connect) - -The Vitable Connect Python library provides convenient access to the Vitable Connect APIs from Python. - -## Table of Contents - -- [Documentation](#documentation) -- [Installation](#installation) -- [Reference](#reference) -- [Usage](#usage) -- [Async Usage](#async-usage) -- [Using Types](#using-types) -- [Environments](#environments) -- [Async Client](#async-client) -- [Exception Handling](#exception-handling) -- [Pagination](#pagination) -- [Nested Params](#nested-params) -- [Handling Errors](#handling-errors) -- [Advanced](#advanced) - - [Access Raw Response Data](#access-raw-response-data) - - [Retries](#retries) - - [Timeouts](#timeouts) - - [Custom Client](#custom-client) -- [Versioning](#versioning) -- [Requirements](#requirements) -- [Contributing](#contributing) +# Vitable Connect Python API library + + +[![PyPI version](https://img.shields.io/pypi/v/vitable_connect.svg?label=pypi%20(stable))](https://pypi.org/project/vitable_connect/) + +The Vitable Connect Python library provides convenient access to the Vitable Connect REST API from any Python 3.9+ +application. The library includes type definitions for all request params and response fields, +and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). + +It is generated with [Stainless](https://www.stainless.com/). ## Documentation @@ -35,29 +16,35 @@ The REST API documentation can be found on [vitablehealth.com](https://vitablehe ## Installation ```sh -pip install vitable-connect +# install from PyPI +pip install vitable_connect ``` -## Reference - -A full reference for this library is available [here](https://github.com/Vitable-Inc/vitable-connect-python/blob/HEAD/./reference.md). - ## Usage -Instantiate and use the client with the following: +The full API of this library can be found in [api.md](api.md). ```python +import os from vitable_connect import VitableConnect client = VitableConnect( - api_key="", + api_key=os.environ.get("VITABLE_CONNECT_API_KEY"), # This is the default and can be omitted + # defaults to "production". + environment="environment_1", ) -client.auth.issue_access_token( +response = client.auth.issue_access_token( grant_type="client_credentials", ) +print(response.access_token) ``` +While you can provide an `api_key` keyword argument, +we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) +to add `VITABLE_CONNECT_API_KEY="My API Key"` to your `.env` file +so that your API Key is not stored in source control. + ## Async usage Simply import `AsyncVitableConnect` instead of `VitableConnect` and use `await` with each API call: @@ -129,82 +116,75 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. -## Environments +## Pagination + +List methods in the Vitable Connect API are paginated. -This SDK allows you to configure different environments for API requests. +This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: ```python from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment -client = VitableConnect( - environment=VitableConnectEnvironment.PRODUCTION, -) -``` +client = VitableConnect() -## Async Client +all_employees = [] +# Automatically fetches more pages as needed. +for employee in client.employees.list_enrollments( + employee_id="empl_abc123def456", +): + # Do something with employee here + all_employees.append(employee) +print(all_employees) +``` -The SDK also exports an `async` client so that you can make non-blocking calls to our API. Note that if you are constructing an Async httpx client class to pass into this client, use `httpx.AsyncClient()` instead of `httpx.Client()` (e.g. for the `httpx_client` parameter of this client). +Or, asynchronously: ```python import asyncio - from vitable_connect import AsyncVitableConnect -client = AsyncVitableConnect( - api_key="", -) +client = AsyncVitableConnect() async def main() -> None: - await client.auth.issue_access_token( - grant_type="client_credentials", - ) + all_employees = [] + # Iterate through items across all pages, issuing requests as needed. + async for employee in client.employees.list_enrollments( + employee_id="empl_abc123def456", + ): + all_employees.append(employee) + print(all_employees) asyncio.run(main()) ``` -## Exception Handling - -When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error -will be thrown. +Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: ```python -from vitable_connect.core.api_error import ApiError +first_page = await client.employees.list_enrollments( + employee_id="empl_abc123def456", +) +if first_page.has_next_page(): + print(f"will fetch next page using these details: {first_page.next_page_info()}") + next_page = await first_page.get_next_page() + print(f"number of items we just fetched: {len(next_page.data)}") -try: - client.auth.issue_access_token(...) -except ApiError as e: - print(e.status_code) - print(e.body) +# Remove `await` for non-async usage. ``` -## Pagination - -Paginated requests will return a `SyncPager` or `AsyncPager`, which can be used as generators for the underlying object. +Or just work directly with the returned data: ```python -from vitable_connect import VitableConnect - -client = VitableConnect( - api_key="", -) - -client.employees.list_enrollments( +first_page = await client.employees.list_enrollments( employee_id="empl_abc123def456", - limit=20, - page=1, ) -``` -```python -# You can also iterate through pages and access the typed response per page -pager = client.employees.list_enrollments(...) -for page in pager.iter_pages(): - print(page.response) # access the typed response for each page - for item in page: - print(item) +print(f"page number: {first_page.pagination.page}") # => "page number: 1" +for employee in first_page.data: + print(employee.id) + +# Remove `await` for non-async usage. ``` ## Nested params @@ -323,84 +303,145 @@ Note that requests that time out are [retried twice by default](#retries). ## Advanced -### Access Raw Response Data +### Logging -The SDK provides access to raw response data, including headers, through the `.with_raw_response` property. -The `.with_raw_response` property returns a "raw" client that can be used to access the `.headers` and `.data` attributes. +We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. -```python -from vitable_connect import VitableConnect +You can enable logging by setting the environment variable `VITABLE_CONNECT_LOG` to `info`. -client = VitableConnect(...) -response = client.auth.with_raw_response.issue_access_token(...) -print(response.headers) # access the response headers -print(response.status_code) # access the response status code -print(response.data) # access the underlying object +```shell +$ export VITABLE_CONNECT_LOG=info ``` -### Retries +Or to `debug` for more verbose logging. + +### How to tell whether `None` means `null` or missing + +In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: -The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long -as the request is deemed retryable and the number of retry attempts has not grown larger than the configured -retry limit (default: 2). +```py +if response.my_field is None: + if 'my_field' not in response.model_fields_set: + print('Got json like {}, without a "my_field" key present at all.') + else: + print('Got json like {"my_field": null}.') +``` -Which status codes are retried depends on the `retryStatusCodes` generator configuration: +### Accessing raw response data (e.g. headers) -**`legacy`** (current default): retries on -- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) -- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) -- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) -- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses) (All server errors, including 500) +The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., -**`recommended`**: retries on -- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) -- [409](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409) (Conflict) -- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) -- [502](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) (Bad Gateway) -- [503](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) (Service Unavailable) -- [504](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) (Gateway Timeout) +```py +from vitable_connect import VitableConnect -Use the `max_retries` request option to configure this behavior. +client = VitableConnect() +response = client.auth.with_raw_response.issue_access_token( + grant_type="client_credentials", +) +print(response.headers.get('X-My-Header')) -```python -client.auth.issue_access_token(..., request_options={ - "max_retries": 1 -}) +auth = response.parse() # get the object that `auth.issue_access_token()` would have returned +print(auth.access_token) ``` -### Timeouts +These methods return an [`APIResponse`](https://github.com/Vitable-Inc/vitable-connect-python/tree/main/src/vitable_connect/_response.py) object. + +The async client returns an [`AsyncAPIResponse`](https://github.com/Vitable-Inc/vitable-connect-python/tree/main/src/vitable_connect/_response.py) with the same structure, the only difference being `await`able methods for reading the response content. + +#### `.with_streaming_response` + +The above interface eagerly reads the full response body when you make the request, which may not always be what you want. -The SDK defaults to a 60 second timeout. You can configure this with a timeout option at the client or request level. +To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. ```python -from vitable_connect import VitableConnect +with client.auth.with_streaming_response.issue_access_token( + grant_type="client_credentials", +) as response: + print(response.headers.get("X-My-Header")) + + for line in response.iter_lines(): + print(line) +``` + +The context manager is required so that the response will reliably be closed. + +### Making custom/undocumented requests -client = VitableConnect(..., timeout=20.0) +This library is typed for convenient access to the documented API. -# Override timeout for a specific method -client.auth.issue_access_token(..., request_options={ - "timeout": 1 -}) +If you need to access undocumented endpoints, params, or response properties, the library can still be used. + +#### Undocumented endpoints + +To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other +http verbs. Options on the client will be respected (such as retries) when making this request. + +```py +import httpx + +response = client.post( + "/foo", + cast_to=httpx.Response, + body={"my_param": True}, +) + +print(response.headers.get("x-foo")) ``` -### Custom Client +#### Undocumented request params + +If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request +options. + +#### Undocumented response properties + +To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You +can also get all the extra fields on the Pydantic model as a dict with +[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). -You can override the `httpx` client to customize it for your use-case. Some common use-cases include support for proxies -and transports. +### Configuring the HTTP client + +You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: + +- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) +- Custom [transports](https://www.python-httpx.org/advanced/transports/) +- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality ```python import httpx -from vitable_connect import VitableConnect +from vitable_connect import VitableConnect, DefaultHttpxClient client = VitableConnect( - ..., - httpx_client=httpx.Client( + # Or use the `VITABLE_CONNECT_BASE_URL` env var + base_url="http://my.test.server.example.com:8083", + http_client=DefaultHttpxClient( proxy="http://my.test.proxy.example.com", transport=httpx.HTTPTransport(local_address="0.0.0.0"), ), ) ``` +You can also customize the client on a per-request basis by using `with_options()`: + +```python +client.with_options(http_client=DefaultHttpxClient(...)) +``` + +### Managing HTTP resources + +By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. + +```py +from vitable_connect import VitableConnect + +with VitableConnect() as client: + # make requests here + ... + +# HTTP client is now closed +``` + ## Versioning This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: @@ -430,10 +471,4 @@ Python 3.9 or higher. ## Contributing -While we value open-source contributions to this SDK, this library is generated programmatically. -Additions made directly to this library would have to be moved over to our generation code, -otherwise they would be overwritten upon the next generated release. Feel free to open a PR as -a proof of concept, but know that we will not be able to merge it as-is. We suggest opening -an issue first to discuss with us! - -On the other hand, contributions to the README are always very welcome! +See [the contributing documentation](./CONTRIBUTING.md). diff --git a/api.md b/api.md new file mode 100644 index 0000000..cc6b1c7 --- /dev/null +++ b/api.md @@ -0,0 +1,197 @@ +# Auth + +Types: + +```python +from vitable_connect.types import AuthIssueAccessTokenResponse +``` + +Methods: + +- client.auth.issue_access_token(\*\*params) -> AuthIssueAccessTokenResponse + +# Employees + +Types: + +```python +from vitable_connect.types import ( + Employee, + EmployeeClass, + Pagination, + EmployeeRetrieveResponse, + EmployeeUpdateResponse, +) +``` + +Methods: + +- client.employees.retrieve(employee_id) -> EmployeeRetrieveResponse +- client.employees.update(employee_id, \*\*params) -> EmployeeUpdateResponse +- client.employees.list_enrollments(employee_id, \*\*params) -> SyncPageNumberPage[Enrollment] + +# Employers + +Types: + +```python +from vitable_connect.types import ( + Employer, + EmployerResponse, + EmployerListResponse, + EmployerEnsurePayrollIntegrationEmailResponse, + EmployerListBenefitPlanYearEnrollmentsResponse, + EmployerListBenefitPlanYearsResponse, + EmployerListHRISProvidersResponse, + EmployerListInvoicesResponse, + EmployerListPayrollDeductionStatementsResponse, + EmployerRetrieveBenefitPlanYearResponse, + EmployerRetrieveHRISResponse, + EmployerRetrieveInvoicePdfResponse, + EmployerRetrievePayrollAccessSetupResponse, + EmployerSubmitCensusSyncResponse, + EmployerSubmitPayrollAccessSetupResponse, + EmployerUpdateSettingsResponse, +) +``` + +Methods: + +- client.employers.create(\*\*params) -> EmployerResponse +- client.employers.retrieve(employer_id) -> EmployerResponse +- client.employers.update(employer_id, \*\*params) -> EmployerResponse +- client.employers.list(\*\*params) -> SyncPageNumberPage[EmployerListResponse] +- client.employers.ensure_payroll_integration_email(employer_id) -> EmployerEnsurePayrollIntegrationEmailResponse +- client.employers.list_benefit_plan_year_enrollments(benefit_plan_year_id, \*, employer_id, \*\*params) -> SyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse] +- client.employers.list_benefit_plan_years(employer_id) -> EmployerListBenefitPlanYearsResponse +- client.employers.list_employees(employer_id, \*\*params) -> SyncPageNumberPage[Employee] +- client.employers.list_hris_providers() -> EmployerListHRISProvidersResponse +- client.employers.list_invoices(employer_id, \*\*params) -> EmployerListInvoicesResponse +- client.employers.list_payroll_deduction_statements(employer_id, \*\*params) -> SyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse] +- client.employers.retrieve_benefit_plan_year(benefit_plan_year_id, \*, employer_id) -> EmployerRetrieveBenefitPlanYearResponse +- client.employers.retrieve_hris(employer_id) -> EmployerRetrieveHRISResponse +- client.employers.retrieve_invoice_pdf(invoice_id, \*, employer_id) -> EmployerRetrieveInvoicePdfResponse +- client.employers.retrieve_payroll_access_setup(employer_id) -> EmployerRetrievePayrollAccessSetupResponse +- client.employers.submit_census_sync(employer_id, \*\*params) -> EmployerSubmitCensusSyncResponse +- client.employers.submit_payroll_access_setup(employer_id, \*\*params) -> EmployerSubmitPayrollAccessSetupResponse +- client.employers.update_settings(employer_id, \*\*params) -> EmployerUpdateSettingsResponse + +# Enrollments + +Types: + +```python +from vitable_connect.types import ( + Enrollment, + EnrollmentStatus, + EnrollmentRetrieveResponse, + EnrollmentReissueResponse, +) +``` + +Methods: + +- client.enrollments.retrieve(enrollment_id) -> EnrollmentRetrieveResponse +- client.enrollments.reissue(enrollment_id, \*\*params) -> EnrollmentReissueResponse +- client.enrollments.terminate(enrollment_id, \*\*params) -> None + +# WebhookEvents + +Types: + +```python +from vitable_connect.types import ( + WebhookEvent, + WebhookEventRetrieveResponse, + WebhookEventListDeliveriesResponse, +) +``` + +Methods: + +- client.webhook_events.retrieve(event_id) -> WebhookEventRetrieveResponse +- client.webhook_events.list(\*\*params) -> SyncPageNumberPage[WebhookEvent] +- client.webhook_events.list_deliveries(event_id) -> WebhookEventListDeliveriesResponse + +# Groups + +Types: + +```python +from vitable_connect.types import Group, GroupResponse +``` + +Methods: + +- client.groups.create(\*\*params) -> GroupResponse +- client.groups.retrieve(group_id) -> GroupResponse +- client.groups.update(group_id, \*\*params) -> GroupResponse +- client.groups.list(\*\*params) -> SyncPageNumberPage[Group] + +## Members + +### Sync + +Types: + +```python +from vitable_connect.types.groups.members import SyncRetrieveResponse, SyncSubmitResponse +``` + +Methods: + +- client.groups.members.sync.retrieve(request_id, \*, group_id) -> SyncRetrieveResponse +- client.groups.members.sync.submit(group_id, \*\*params) -> SyncSubmitResponse + +# Members + +Types: + +```python +from vitable_connect.types import ( + MemberRetrieveResponse, + MemberListResponse, + MemberListDependentsResponse, + MemberListEmploymentsResponse, + MemberListEnrollmentsResponse, + MemberListIDCardsResponse, + MemberListQualifyingLifeEventsResponse, + MemberRetrieveHouseholdResponse, +) +``` + +Methods: + +- client.members.retrieve(member_id) -> MemberRetrieveResponse +- client.members.list(\*\*params) -> SyncPageNumberPage[MemberListResponse] +- client.members.list_dependents(member_id) -> MemberListDependentsResponse +- client.members.list_employments(member_id) -> MemberListEmploymentsResponse +- client.members.list_enrollments(member_id) -> MemberListEnrollmentsResponse +- client.members.list_id_cards(member_id) -> MemberListIDCardsResponse +- client.members.list_qualifying_life_events(member_id, \*\*params) -> SyncPageNumberPage[MemberListQualifyingLifeEventsResponse] +- client.members.retrieve_household(member_id) -> MemberRetrieveHouseholdResponse + +# Organizations + +Types: + +```python +from vitable_connect.types import OrganizationCreateResponse, OrganizationListResponse +``` + +Methods: + +- client.organizations.create(\*\*params) -> OrganizationCreateResponse +- client.organizations.list() -> OrganizationListResponse + +# Plans + +Types: + +```python +from vitable_connect.types import PlanListResponse +``` + +Methods: + +- client.plans.list(\*\*params) -> SyncPageNumberPage[PlanListResponse] diff --git a/bin/check-release-environment b/bin/check-release-environment new file mode 100644 index 0000000..1e951e9 --- /dev/null +++ b/bin/check-release-environment @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +errors=() + +lenErrors=${#errors[@]} + +if [[ lenErrors -gt 0 ]]; then + echo -e "Found the following errors in the release environment:\n" + + for error in "${errors[@]}"; do + echo -e "- $error\n" + done + + exit 1 +fi + +echo "The environment is ready to push releases!" diff --git a/bin/publish-pypi b/bin/publish-pypi new file mode 100644 index 0000000..5895700 --- /dev/null +++ b/bin/publish-pypi @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -eux +rm -rf dist +mkdir -p dist +uv build +if [ -n "${PYPI_TOKEN:-}" ]; then + uv publish --token=$PYPI_TOKEN +else + uv publish +fi diff --git a/changelog.md b/changelog.md deleted file mode 100644 index 307a9c8..0000000 --- a/changelog.md +++ /dev/null @@ -1,9 +0,0 @@ -## 1.0.1 - 2026-09-04 -* chore: update User-Agent header to SDK version placeholder -* Replace the hardcoded version string in the User-Agent header with the -* Fern-managed placeholder value. This is an internal SDK regeneration -* artifact and has no effect on public API behavior. -* Key changes: -* Updated `User-Agent` header value from `vitable-connect/1.0.0` to `vitable-connect/0.0.0-fern-placeholder` in `BaseClientWrapper` -* 🌿 Generated with Fern - diff --git a/examples/.keep b/examples/.keep new file mode 100644 index 0000000..d8c73e9 --- /dev/null +++ b/examples/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store example files demonstrating usage of this SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 4d7420a..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1469 +0,0 @@ -# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand. - -[[package]] -name = "aiohappyeyeballs" -version = "2.7.1" -description = "Happy Eyeballs for asyncio" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, - {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, -] - -[[package]] -name = "aiohttp" -version = "3.14.3" -description = "Async http client/server framework (asyncio)" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, - {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, - {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, - {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, - {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, - {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, - {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, - {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, - {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, - {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, - {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, - {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, - {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, - {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, - {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, - {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, - {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, - {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, - {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, - {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, - {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, - {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, - {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, - {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, - {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, - {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, - {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, - {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, - {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, - {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, - {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, - {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, - {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, - {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, - {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, - {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, - {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, - {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, - {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, - {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, - {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, - {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, - {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, - {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, - {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, - {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, - {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, - {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, - {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, - {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, - {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, - {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, - {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, - {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, - {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, - {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, - {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, - {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, - {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, - {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, - {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, - {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, - {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, - {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, - {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, - {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, - {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, - {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, - {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, - {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, - {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, - {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, - {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, - {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, - {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, - {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, - {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, - {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, - {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, - {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, - {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, - {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, - {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, - {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, - {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, - {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, - {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, - {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, - {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, - {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, - {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, - {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, - {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, - {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, - {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, - {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, - {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, - {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, - {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, - {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, - {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, - {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, - {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, - {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, - {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, - {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, - {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, - {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, - {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, - {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, - {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, - {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, - {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, - {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, - {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, - {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, - {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, - {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, - {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.4.0" -async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\" and sys_platform != \"android\" and sys_platform != \"ios\"", "aiodns (>=3.3.0) ; sys_platform != \"android\" and sys_platform != \"ios\"", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\" and sys_platform != \"android\" and sys_platform != \"ios\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] - -[[package]] -name = "aiosignal" -version = "1.4.0" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, - {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} - -[[package]] -name = "annotated-types" -version = "0.8.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, - {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, -] - -[[package]] -name = "anyio" -version = "4.15.0" -description = "High-level concurrency and networking framework on top of asyncio or Trio" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "anyio-4.15.0-py3-none-any.whl", hash = "sha256:7ecd9937369ffce8bba0b5ccb9b3a9507b101b0ed50256aecfbab27e6c2acb99"}, - {file = "anyio-4.15.0.tar.gz", hash = "sha256:b5c620ed540725e2579c31b17bb995b3bf02c9281c9cace04c7d186380bab85e"}, -] - -[package.dependencies] -exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} -idna = ">=2.8" -typing_extensions = {version = ">=4.16.0", markers = "python_version < \"3.15\""} - -[package.extras] -trio = ["trio (>=0.32.0)"] - -[[package]] -name = "async-timeout" -version = "5.0.1" -description = "Timeout context manager for asyncio programs" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "python_version == \"3.10\" and extra == \"aiohttp\"" -files = [ - {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, - {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, -] - -[[package]] -name = "attrs" -version = "26.1.0" -description = "Classes Without Boilerplate" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"}, - {file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"}, -] - -[[package]] -name = "backports-asyncio-runner" -version = "1.2.0" -description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." -optional = false -python-versions = "<3.11,>=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, - {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, -] - -[[package]] -name = "certifi" -version = "2026.7.22" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, - {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.1.2" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, - {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "frozenlist" -version = "1.8.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, - {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, - {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, - {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, - {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, - {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, - {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, - {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, - {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, - {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, - {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, - {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, - {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, - {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, - {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, - {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, - {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, - {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, - {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, - {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, - {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, - {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, - {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, - {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, - {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, - {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, - {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, - {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, - {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, - {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, - {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, - {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, - {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, - {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, - {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, - {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, - {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, - {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, - {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, - {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, - {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, - {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, - {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, - {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, - {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, - {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, - {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, - {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, -] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "httpx-aiohttp" -version = "0.1.12" -description = "Aiohttp transport for HTTPX" -optional = true -python-versions = ">=3.8" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141"}, - {file = "httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c"}, -] - -[package.dependencies] -aiohttp = ">=3.10.0,<4" -httpx = ">=0.27.0" - -[[package]] -name = "idna" -version = "3.19" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4"}, - {file = "idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15"}, -] - -[package.extras] -all = ["coverage (>=7.10.0)", "hypothesis (>=6.141.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.16.0)", "ty (>=0.0.37)"] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "multidict" -version = "6.7.1" -description = "multidict implementation" -optional = true -python-versions = ">=3.9" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, - {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, - {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, - {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, - {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, - {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, - {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, - {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, - {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, - {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, - {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, - {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, - {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, - {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, - {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, - {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, - {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, - {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, - {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, - {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, - {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, - {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, - {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, - {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, - {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, - {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, - {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, - {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, - {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, - {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, - {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, - {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, - {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, - {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, - {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, - {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, - {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, - {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, - {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, - {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, - {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, - {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, - {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, - {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, - {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, - {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, - {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, - {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, - {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, - {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, - {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, -] - -[package.dependencies] -typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} - -[[package]] -name = "mypy" -version = "1.13.0" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6607e0f1dd1fb7f0aca14d936d13fd19eba5e17e1cd2a14f808fa5f8f6d8f60a"}, - {file = "mypy-1.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a21be69bd26fa81b1f80a61ee7ab05b076c674d9b18fb56239d72e21d9f4c80"}, - {file = "mypy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b2353a44d2179846a096e25691d54d59904559f4232519d420d64da6828a3a7"}, - {file = "mypy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0730d1c6a2739d4511dc4253f8274cdd140c55c32dfb0a4cf8b7a43f40abfa6f"}, - {file = "mypy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5fc54dbb712ff5e5a0fca797e6e0aa25726c7e72c6a5850cfd2adbc1eb0a372"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:581665e6f3a8a9078f28d5502f4c334c0c8d802ef55ea0e7276a6e409bc0d82d"}, - {file = "mypy-1.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ddb5b9bf82e05cc9a627e84707b528e5c7caaa1c55c69e175abb15a761cec2d"}, - {file = "mypy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20c7ee0bc0d5a9595c46f38beb04201f2620065a93755704e141fcac9f59db2b"}, - {file = "mypy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3790ded76f0b34bc9c8ba4def8f919dd6a46db0f5a6610fb994fe8efdd447f73"}, - {file = "mypy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:51f869f4b6b538229c1d1bcc1dd7d119817206e2bc54e8e374b3dfa202defcca"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5c7051a3461ae84dfb5dd15eff5094640c61c5f22257c8b766794e6dd85e72d5"}, - {file = "mypy-1.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:39bb21c69a5d6342f4ce526e4584bc5c197fd20a60d14a8624d8743fffb9472e"}, - {file = "mypy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:164f28cb9d6367439031f4c81e84d3ccaa1e19232d9d05d37cb0bd880d3f93c2"}, - {file = "mypy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a4c1bfcdbce96ff5d96fc9b08e3831acb30dc44ab02671eca5953eadad07d6d0"}, - {file = "mypy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:a0affb3a79a256b4183ba09811e3577c5163ed06685e4d4b46429a271ba174d2"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a7b44178c9760ce1a43f544e595d35ed61ac2c3de306599fa59b38a6048e1aa7"}, - {file = "mypy-1.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d5092efb8516d08440e36626f0153b5006d4088c1d663d88bf79625af3d1d62"}, - {file = "mypy-1.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2904956dac40ced10931ac967ae63c5089bd498542194b436eb097a9f77bc8"}, - {file = "mypy-1.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:7bfd8836970d33c2105562650656b6846149374dc8ed77d98424b40b09340ba7"}, - {file = "mypy-1.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9f73dba9ec77acb86457a8fc04b5239822df0c14a082564737833d2963677dbc"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:100fac22ce82925f676a734af0db922ecfea991e1d7ec0ceb1e115ebe501301a"}, - {file = "mypy-1.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bcb0bb7f42a978bb323a7c88f1081d1b5dee77ca86f4100735a6f541299d8fb"}, - {file = "mypy-1.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bde31fc887c213e223bbfc34328070996061b0833b0a4cfec53745ed61f3519b"}, - {file = "mypy-1.13.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:07de989f89786f62b937851295ed62e51774722e5444a27cecca993fc3f9cd74"}, - {file = "mypy-1.13.0-cp38-cp38-win_amd64.whl", hash = "sha256:4bde84334fbe19bad704b3f5b78c4abd35ff1026f8ba72b29de70dda0916beb6"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0246bcb1b5de7f08f2826451abd947bf656945209b140d16ed317f65a17dc7dc"}, - {file = "mypy-1.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f5b7deae912cf8b77e990b9280f170381fdfbddf61b4ef80927edd813163732"}, - {file = "mypy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7029881ec6ffb8bc233a4fa364736789582c738217b133f1b55967115288a2bc"}, - {file = "mypy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3e38b980e5681f28f033f3be86b099a247b13c491f14bb8b1e1e134d23bb599d"}, - {file = "mypy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:a6789be98a2017c912ae6ccb77ea553bbaf13d27605d2ca20a76dfbced631b24"}, - {file = "mypy-1.13.0-py3-none-any.whl", hash = "sha256:9c250883f9fd81d212e0952c92dbfcc96fc237f4b7c92f56ac81fd48460b3e5a"}, - {file = "mypy-1.13.0.tar.gz", hash = "sha256:0291a61b6fbf3e6673e3405cfcc0e7650bebc7939659fdca2702958038bd835e"}, -] - -[package.dependencies] -mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "packaging" -version = "26.3" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"}, - {file = "packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79"}, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "propcache" -version = "0.5.2" -description = "Accelerated property cache" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, - {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, - {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, - {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, - {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, - {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, - {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, - {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, - {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, - {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, - {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, - {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, - {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, - {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, - {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, - {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, - {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, - {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, - {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, - {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, - {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, - {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, - {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, - {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, - {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, - {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, - {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, - {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, - {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, - {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, - {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, - {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, - {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, - {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, - {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, - {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, - {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, - {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, - {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, - {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, - {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, - {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, - {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, - {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, - {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, - {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, -] - -[[package]] -name = "pydantic" -version = "2.13.5" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73"}, - {file = "pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -pydantic-core = "2.46.5" -typing-extensions = ">=4.14.1" -typing-inspection = ">=0.4.2" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] - -[[package]] -name = "pydantic-core" -version = "2.46.5" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6"}, - {file = "pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615"}, - {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb"}, - {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b"}, - {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6"}, - {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793"}, - {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b"}, - {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461"}, - {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736"}, - {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3"}, - {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f"}, - {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1"}, - {file = "pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069"}, - {file = "pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d"}, - {file = "pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f"}, - {file = "pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f"}, - {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061"}, - {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be"}, - {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a"}, - {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b"}, - {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c"}, - {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee"}, - {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e"}, - {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2"}, - {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689"}, - {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec"}, - {file = "pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129"}, - {file = "pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c"}, - {file = "pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8"}, - {file = "pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d"}, - {file = "pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e"}, - {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29"}, - {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4"}, - {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a"}, - {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62"}, - {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2"}, - {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869"}, - {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5"}, - {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3"}, - {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b"}, - {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0"}, - {file = "pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b"}, - {file = "pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8"}, - {file = "pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084"}, - {file = "pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0"}, - {file = "pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff"}, - {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931"}, - {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f"}, - {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038"}, - {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f"}, - {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1"}, - {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761"}, - {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5"}, - {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e"}, - {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed"}, - {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519"}, - {file = "pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea"}, - {file = "pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5"}, - {file = "pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575"}, - {file = "pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355"}, - {file = "pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e"}, - {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3"}, - {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c"}, - {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21"}, - {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f"}, - {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f"}, - {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a"}, - {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821"}, - {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2"}, - {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47"}, - {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a"}, - {file = "pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074"}, - {file = "pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0"}, - {file = "pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290"}, - {file = "pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f"}, - {file = "pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed"}, - {file = "pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0"}, - {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655"}, - {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a"}, - {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d"}, - {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8"}, - {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf"}, - {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464"}, - {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64"}, - {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168"}, - {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e"}, - {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13"}, - {file = "pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7"}, - {file = "pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0"}, - {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2"}, - {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c"}, - {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47"}, - {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a"}, - {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942"}, - {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f"}, - {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433"}, - {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c"}, - {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f"}, - {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0"}, - {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4"}, - {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25"}, - {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6"}, - {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e"}, - {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda"}, - {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266"}, - {file = "pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"}, -] - -[package.dependencies] -typing-extensions = ">=4.14.1" - -[[package]] -name = "pygments" -version = "2.21.0" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9"}, - {file = "pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pytest" -version = "9.1.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"}, - {file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" -tomli = {version = ">=1", markers = "python_version < \"3.11\""} - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "1.4.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, - {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, -] - -[package.dependencies] -backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} -pytest = ">=8.4,<10" -typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-xdist" -version = "3.8.0" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, - {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "ruff" -version = "0.11.5" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, - {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, - {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"}, - {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"}, - {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"}, - {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"}, - {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"}, -] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["dev"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "tomli" -version = "2.4.1" -description = "A lil' TOML parser" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -markers = "python_version == \"3.10\"" -files = [ - {file = "tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30"}, - {file = "tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076"}, - {file = "tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c"}, - {file = "tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc"}, - {file = "tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049"}, - {file = "tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e"}, - {file = "tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a"}, - {file = "tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9"}, - {file = "tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585"}, - {file = "tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1"}, - {file = "tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917"}, - {file = "tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9"}, - {file = "tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54"}, - {file = "tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897"}, - {file = "tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d"}, - {file = "tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5"}, - {file = "tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd"}, - {file = "tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36"}, - {file = "tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf"}, - {file = "tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662"}, - {file = "tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15"}, - {file = "tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba"}, - {file = "tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6"}, - {file = "tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7"}, - {file = "tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4"}, - {file = "tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d"}, - {file = "tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c"}, - {file = "tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f"}, - {file = "tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8"}, - {file = "tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26"}, - {file = "tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396"}, - {file = "tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe"}, - {file = "tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f"}, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20260807" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "types_python_dateutil-2.9.0.20260807-py3-none-any.whl", hash = "sha256:54aa3707350ed7a9cc0776fd2f6739679d6967d11b40150985e81edcb86df4db"}, - {file = "types_python_dateutil-2.9.0.20260807.tar.gz", hash = "sha256:e0b8a90d464c8684c66b7b8e4556d9074afdddcc56ca45323f0987134f9e7034"}, -] - -[[package]] -name = "typing-extensions" -version = "4.16.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, - {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.4" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, - {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, -] - -[package.dependencies] -typing-extensions = ">=4.15.0" - -[[package]] -name = "urllib3" -version = "2.7.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.10" -groups = ["dev"] -files = [ - {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, - {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, -] - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "yarl" -version = "1.24.5" -description = "Yet another URL library" -optional = true -python-versions = ">=3.10" -groups = ["main"] -markers = "extra == \"aiohttp\"" -files = [ - {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, - {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, - {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, - {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, - {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, - {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, - {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, - {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, - {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, - {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, - {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, - {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, - {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, - {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, - {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, - {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, - {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, - {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, - {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, - {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, - {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, - {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, - {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, - {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, - {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, - {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, - {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, - {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, - {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, - {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, - {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, - {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, - {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, - {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.1" - -[extras] -aiohttp = ["aiohttp", "httpx-aiohttp"] - -[metadata] -lock-version = "2.1" -python-versions = "^3.10" -content-hash = "4eaeff06fb8e558535aa3729ed6f99c2398eaaba665bcdf9c096bb08d9cb5fc6" diff --git a/pyproject.toml b/pyproject.toml index ffceae4..d4b93dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,97 +1,255 @@ [project] -name = "vitable-connect" -dynamic = ["version"] +name = "vitable_connect" +version = "0.9.0" +description = "The official Python library for the vitable-connect API" +dynamic = ["readme"] +license = "Apache-2.0" +authors = [ +{ name = "Vitable Connect", email = "dev@vitablehealth.com" }, +] -[tool.poetry] -name = "vitable-connect" -version = "1.0.1" -description = "" -readme = "README.md" -authors = [] -keywords = [] +dependencies = [ + "httpx>=0.23.0, <1", + "pydantic>=1.9.0, <3", + "typing-extensions>=4.14, <5", + "anyio>=3.5.0, <5", + "distro>=1.7.0, <2", + "sniffio", +] +requires-python = ">= 3.9" classifiers = [ - "Intended Audience :: Developers", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.14", - "Programming Language :: Python :: 3.15", - "Operating System :: OS Independent", - "Operating System :: POSIX", - "Operating System :: MacOS", - "Operating System :: POSIX :: Linux", - "Operating System :: Microsoft :: Windows", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed" + "Typing :: Typed", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Operating System :: OS Independent", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: POSIX :: Linux", + "Operating System :: Microsoft :: Windows", + "Topic :: Software Development :: Libraries :: Python Modules", + "License :: OSI Approved :: Apache Software License" +] + +[project.urls] +Homepage = "https://github.com/Vitable-Inc/vitable-connect-python" +Repository = "https://github.com/Vitable-Inc/vitable-connect-python" + +[project.optional-dependencies] +aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.9"] + +[tool.uv] +managed = true +required-version = ">=0.9" +conflicts = [ + [ + { group = "pydantic-v1" }, + { group = "pydantic-v2" }, + ], +] + +[dependency-groups] +# version pins are in uv.lock +dev = [ + "pyright==1.1.399", + "mypy==1.17", + "respx", + "pytest", + "pytest-asyncio", + "ruff", + "time-machine", + "dirty-equals>=0.6.0", + "importlib-metadata>=6.7.0", + "rich>=13.7.1", + "pytest-xdist>=3.6.1", ] -packages = [ - { include = "vitable_connect", from = "src"} +pydantic-v1 = [ + "pydantic>=1.9.0,<2", +] +pydantic-v2 = [ + "pydantic~=2.0 ; python_full_version < '3.14'", + "pydantic~=2.12 ; python_full_version >= '3.14'", ] -[tool.poetry.urls] -Repository = 'https://github.com/Vitable-Inc/vitable-connect-python' - -[tool.poetry.dependencies] -python = "^3.10" -aiohttp = { version = ">=3.14.1,<4", optional = true, python = ">=3.10"} -httpx = ">=0.21.2" -httpx-aiohttp = { version = "^0.1.8", optional = true, python = ">=3.10"} -pydantic = ">= 1.9.2" -pydantic-core = ">=2.18.2,<3.0.0" -typing_extensions = ">= 4.0.0" - -[tool.poetry.group.dev.dependencies] -mypy = "==1.13.0" -pytest = "^9.0.3" -pytest-asyncio = "^1.0.0" -pytest-xdist = "^3.6.1" -python-dateutil = "^2.9.0" -types-python-dateutil = "^2.9.0.20240316" -urllib3 = ">=2.6.3,<3.0.0" -ruff = "==0.11.5" +[build-system] +requires = ["hatchling==1.26.3", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[tool.hatch.build] +include = [ + "src/*" +] + +[tool.hatch.build.targets.wheel] +packages = ["src/vitable_connect"] + +[tool.hatch.build.targets.sdist] +# Basically everything except hidden files/directories (such as .github, .devcontainers, .python-version, etc) +include = [ + "/*.toml", + "/*.json", + "/*.lock", + "/*.md", + "/mypy.ini", + "/noxfile.py", + "bin/*", + "examples/*", + "src/*", + "tests/*", +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]] +path = "README.md" + +[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]] +# replace relative links with absolute links +pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)' +replacement = '[\1](https://github.com/Vitable-Inc/vitable-connect-python/tree/main/\g<2>)' [tool.pytest.ini_options] -testpaths = [ "tests" ] +testpaths = ["tests"] +addopts = "--tb=short -n auto" +xfail_strict = true asyncio_mode = "auto" -norecursedirs = [ "src" ] -markers = [ - "aiohttp: tests that require httpx_aiohttp to be installed", +asyncio_default_fixture_loop_scope = "session" +filterwarnings = [ + "error" ] +[tool.pyright] +# this enables practically every flag given by pyright. +# there are a couple of flags that are still disabled by +# default in strict mode as they are experimental and niche. +typeCheckingMode = "strict" +pythonVersion = "3.9" + +exclude = [ + "_dev", + ".venv", + ".nox", + ".git", +] + +reportImplicitOverride = true +reportOverlappingOverload = false + +reportImportCycles = false +reportPrivateUsage = false + [tool.mypy] -plugins = ["pydantic.mypy"] +pretty = true +show_error_codes = true + +# Exclude _files.py because mypy isn't smart enough to apply +# the correct type narrowing and as this is an internal module +# it's fine to just use Pyright. +# +# We also exclude our `tests` as mypy doesn't always infer +# types correctly and Pyright will still catch any type errors. +exclude = ["src/vitable_connect/_files.py", "_dev/.*.py", "tests/.*"] + +strict_equality = true +implicit_reexport = true +check_untyped_defs = true +no_implicit_optional = true + +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true + +# Turn these options off as it could cause conflicts +# with the Pyright options. +warn_unused_ignores = false +warn_redundant_casts = false + +disallow_any_generics = true +disallow_untyped_defs = true +disallow_untyped_calls = true +disallow_subclassing_any = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +cache_fine_grained = true + +# By default, mypy reports an error if you assign a value to the result +# of a function call that doesn't return anything. We do this in our test +# cases: +# ``` +# result = ... +# assert result is None +# ``` +# Changing this codegen to make mypy happy would increase complexity +# and would not be worth it. +disable_error_code = "func-returns-value,overload-cannot-match" + +# https://github.com/python/mypy/issues/12162 +[[tool.mypy.overrides]] +module = "black.files.*" +ignore_errors = true +ignore_missing_imports = true + [tool.ruff] line-length = 120 +output-format = "grouped" +target-version = "py38" + +[tool.ruff.format] +docstring-code-format = true [tool.ruff.lint] select = [ - "E", # pycodestyle errors - "F", # pyflakes - "I", # isort + # isort + "I", + # bugbear rules + "B", + # remove unused imports + "F401", + # check for missing future annotations + "FA102", + # bare except statements + "E722", + # unused arguments + "ARG", + # print statements + "T201", + "T203", + # misuse of typing.TYPE_CHECKING + "TC004", + # import rules + "TID251", ] ignore = [ - "E402", # Module level import not at top of file - "E501", # Line too long - "E711", # Comparison to `None` should be `cond is not None` - "E712", # Avoid equality comparisons to `True`; use `if ...:` checks - "E721", # Use `is` and `is not` for type comparisons, or `isinstance()` for insinstance checks - "E722", # Do not use bare `except` - "E731", # Do not assign a `lambda` expression, use a `def` - "F821", # Undefined name - "F841" # Local variable ... is assigned to but never used + # mutable defaults + "B006", +] +unfixable = [ + # disable auto fix for print statements + "T201", + "T203", ] -[tool.ruff.lint.isort] -section-order = ["future", "standard-library", "third-party", "first-party"] +extend-safe-fixes = ["FA102"] -[build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"functools.lru_cache".msg = "This function does not retain type information for the wrapped function's arguments; The `lru_cache` function from `_utils` should be used instead" + +[tool.ruff.lint.isort] +length-sort = true +length-sort-straight = true +combine-as-imports = true +extra-standard-library = ["typing_extensions"] +known-first-party = ["vitable_connect", "tests"] -[tool.poetry.extras] -aiohttp=["aiohttp", "httpx-aiohttp"] +[tool.ruff.lint.per-file-ignores] +"bin/**.py" = ["T201", "T203"] +"scripts/**.py" = ["T201", "T203"] +"tests/**.py" = ["T201", "T203"] +"examples/**.py" = ["T201", "T203"] diff --git a/reference.md b/reference.md deleted file mode 100644 index b3a420f..0000000 --- a/reference.md +++ /dev/null @@ -1,4171 +0,0 @@ -# Reference -## Auth -
client.auth.issue_access_token(...) -> AccessTokenResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Issues a short-lived access token from the authenticated API key. Access tokens can optionally be bound to a specific employer or employee for scoped access. Tokens expire after 15 minutes. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.auth.issue_access_token( - grant_type="client_credentials", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**grant_type:** `GrantType` - -Token issuance flow. Currently only 'client_credentials' supported. - -* `client_credentials` - client_credentials - -
-
- -
-
- -**bound_entity:** `typing.Optional[BoundEntity]` — Optional entity to bind the token to for scoped access - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Employees -
client.employees.get(...) -> EmployeeResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves detailed information for a specific employee by ID. Returns employee details including personal information, employment status, classification and compensation-type effective dates, compensation type, and payroll deductions from the most recent statement period. Deductions reflect a snapshot of the current period and are replaced when a new statement is generated. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employees.get( - employee_id="empl_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employee_id:** `EmployeeId` — Unique employee identifier (empl_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employees.update(...) -> EmployeeResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Updates employee personal, contact, address, and employment fields. This endpoint currently supports email, phone, gender, address, employee_class, start_date, and compensation_type. effective_date is required and applies to employee_class and compensation_type when those fields are included in the request. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment -import datetime - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employees.update( - employee_id="empl_abc123def456", - employee_class="Full Time", - start_date=datetime.date.fromisoformat("2023-01-15"), - compensation_type="Salary", - effective_date=datetime.date.fromisoformat("2023-03-01"), -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employee_id:** `EmployeeId` — Unique employee identifier (empl_*) - -
-
- -
-
- -**effective_date:** `datetime.date` — Past or present date applied to each tracked employment field included in this request - -
-
- -
-
- -**email:** `typing.Optional[str]` — Email address - -
-
- -
-
- -**phone:** `typing.Optional[str]` — Phone number - -
-
- -
-
- -**gender:** `typing.Optional[Gender]` - -Gender identity - -* `Male` - Male -* `Female` - Female -* `Transgender` - Transgender -* `Non-binary` - Non-binary -* `Prefer not to respond` - Prefer not to respond - -
-
- -
-
- -**address:** `typing.Optional[EmployeeAddressInput]` — Employee's residential address - -
-
- -
-
- -**employee_class:** `typing.Optional[EmployeeClass]` - -Employment classification - -* `Full Time` - Full Time -* `Part Time` - Part Time -* `Temporary` - Temporary -* `Intern` - Intern -* `Seasonal` - Seasonal -* `Individual Contractor` - Individual Contractor - -
-
- -
-
- -**start_date:** `typing.Optional[datetime.date]` — Employment start date - -
-
- -
-
- -**compensation_type:** `typing.Optional[CompensationType]` - -Employee compensation type - -* `Salary` - Salary -* `Hourly` - Hourly - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employees.list_enrollments(...) -> EnrollmentListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a paginated list of benefit enrollments for an employee. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employees.list_enrollments( - employee_id="empl_abc123def456", - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employee_id:** `EmployeeId` — Unique employee identifier (empl_*) - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Items per page (default: 20, max: 100) - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number (default: 1) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Employers -
client.employers.list(...) -> OrganizationEmployerListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns the caller's employer book — every employer with its computed columns (enrollment-rate summary, benefit-family tags, HRIS connection, benefit-lifecycle stage) merged with the employer's flat CRM fields (legal name, EIN, contact, address, timestamps). The book is derived from the authenticated principal: one organization's employers, or every organization's for a caller whose reach is not a single organization. Supports search by display name, legal name, or exact EIN, employer id or contact email, benefit-family/lifecycle/HRIS filters, and page/limit pagination. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.list( - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**benefit_family:** `typing.Optional[typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]]]` — Filter to employers with at least one active benefit in these families. - -
-
- -
-
- -**benefit_lifecycle_stage:** `typing.Optional[typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]]]` — Filter to employers in one of these computed benefit-lifecycle stages. - -
-
- -
-
- -**hris_provider:** `typing.Optional[typing.Union[str, typing.Sequence[str]]]` — Filter to employers whose HRIS connection is with one of these payroll providers (e.g. `ADP RUN`). Matched case-insensitively; free text, so read the available values from the HRIS-providers endpoint rather than assuming a fixed set. - -
-
- -
-
- -**hris_status:** `typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]]` — Filter to employers whose HRIS connection is in one of these statuses. - -
-
- -
-
- -**include_cancelled:** `typing.Optional[IncludeCancelled]` — Include cancelled employers (hidden by default unless their stage is explicitly requested). - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Items per page. - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number. - -
-
- -
-
- -**search:** `typing.Optional[Search]` — Employer filter. Matches the display name or the legal name case-insensitively as a substring, or one of these exactly: the EIN (with or without its dash), the employer id, or the contact email of one of the employer's non-disabled admins. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.create(...) -> EmployerResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Creates a new employer for the authenticated organization. Requires employer name, legal name, EIN, email, and address information. Returns the created employer with its assigned ID. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect, EmployerAddressInput -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.create( - name="NewCo Industries", - legal_name="NewCo Industries LLC", - ein="12-3456789", - email="hr@newco.com", - address=EmployerAddressInput( - address_line1="789 Business Blvd", - address_line2="Floor 5", - city="Seattle", - state="WA", - zipcode="98101", - ), - phone_number="2065550100", - reference_id="partner-emp-001", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` — Employer display name - -
-
- -
-
- -**legal_name:** `str` — Legal business name - -
-
- -
-
- -**ein:** `str` — Employer Identification Number (format: XX-XXXXXXX) - -
-
- -
-
- -**email:** `str` — Email address for billing and communications - -
-
- -
-
- -**address:** `EmployerAddressInput` — Employer address - -
-
- -
-
- -**phone_number:** `typing.Optional[str]` — Employer phone number (10-digit US format, e.g. 5551234567) - -
-
- -
-
- -**reference_id:** `typing.Optional[str]` — External reference ID for this employer - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.get(...) -> EmployerResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves detailed information for a specific employer by ID. The employer must belong to the authenticated organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.get( - employer_id="empr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.update(...) -> EmployerResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Updates an existing employer. All fields are optional — only provided fields are updated. PO Box addresses are rejected. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.update( - employer_id="empr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**name:** `typing.Optional[str]` — Employer display name - -
-
- -
-
- -**legal_name:** `typing.Optional[str]` — Legal business name - -
-
- -
-
- -**address:** `typing.Optional[UpdateEmployerAddressInput]` — Employer address - -
-
- -
-
- -**active:** `typing.Optional[bool]` — Whether the employer is active - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.list_benefit_plan_years(...) -> EmployerBenefitPlanYearsListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns the employer's benefit plan years (all years, or one when `year` is given), each with its benefits, offered states, benefit families, and the year-level enrollment roll-up. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.list_benefit_plan_years( - employer_id="empr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.get_benefit_plan_year(...) -> EmployerBenefitPlanYearResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns one benefit plan year in full — its benefit details plus the per-benefit enrollment rate and SPD link — addressed by its `benefit_plan_year_id`. The caller must be authorized for the employer; an unknown or unauthorized plan year returns 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.get_benefit_plan_year( - employer_id="empr_abc123def456", - benefit_plan_year_id="plyr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**benefit_plan_year_id:** `BenefitPlanYearId` — Unique benefit-plan-year identifier (plyr_*). - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.list_benefit_plan_year_enrollments(...) -> PlanYearEnrollmentListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns a paginated list of every member with an enrollment in one of an employer's plan years, any election status: what they elected, where their coverage stands, dependent count, carrier, plan, tier, and the plan's total monthly cost. The caller must be authorized for the employer `empr_<...>`; an unknown or unauthorized employer, or an unknown plan year `plyr_<...>`, returns 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.list_benefit_plan_year_enrollments( - employer_id="empr_abc123def456", - benefit_plan_year_id="plyr_abc123def456", - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*). - -
-
- -
-
- -**benefit_plan_year_id:** `BenefitPlanYearId` — Unique benefit-plan-year identifier (plyr_*). - -
-
- -
-
- -**election_status:** `typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]]` — Filter by election status. Repeat the parameter to match several. - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Items per page (default: 20, max: 100) - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number (default: 1) - -
-
- -
-
- -**search:** `typing.Optional[EmployerSearch]` — Case-insensitive search. Matches member name partially, and the `member_id` exactly — either your own reference id or the prefixed `grpmbr_<...>` id. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.submit_census_sync(...) -> CensusSyncDetailResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Submits a census sync payload for the specified employer. The employees in the payload will be queued for processing. Returns an accepted response with the timestamp of acceptance. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect, CensusSyncEmployeeRequest, CensusSyncEmployeeAddressRequest -from vitable_connect.environment import VitableConnectEnvironment -import datetime - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.submit_census_sync( - employer_id="empr_abc123def456", - employees=[ - CensusSyncEmployeeRequest( - reference_id="EMP-001", - first_name="Jane", - last_name="Doe", - date_of_birth=datetime.date.fromisoformat("1990-05-15"), - email="jane.doe@acme.com", - phone="4155550100", - address=CensusSyncEmployeeAddressRequest( - address_line1="123 Main Street", - address_line2="Apt 4B", - city="San Francisco", - state="CA", - zipcode="94102", - ), - start_date=datetime.date.fromisoformat("2024-01-15"), - employee_class="Full Time", - compensation_type="Salary", - ), - CensusSyncEmployeeRequest( - first_name="John", - last_name="Smith", - date_of_birth=datetime.date.fromisoformat("1985-11-20"), - email="john.smith@acme.com", - phone="4155550101", - start_date=datetime.date.fromisoformat("2024-03-01"), - employee_class="Part Time", - compensation_type="Hourly", - ) - ], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**employees:** `typing.List[CensusSyncEmployeeRequest]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.list_employees(...) -> EmployeeListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a paginated list of employees for a specific employer. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. Results are paginated using page and limit parameters and can be narrowed with a case-insensitive `search` (first name, last name, or email) and an `employment_status` filter (active or terminated). Each employee includes payroll deductions from the most recent statement period. When a new deduction statement is generated, previous period deductions are replaced. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.list_employees( - employer_id="empr_abc123def456", - limit=20, - page=1, - search="jane", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**employment_status:** `typing.Optional[EmployeeStatus]` — Filter by employment status (active or terminated) - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Items per page (default: 20, max: 100) - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number (default: 1) - -
-
- -
-
- -**search:** `typing.Optional[EmployeeSearch]` — Case-insensitive search across employee first name, last name, and email - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.get_hris(...) -> EmployerHrisResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns the employer's HRIS connection — provider, status, last sync, and synced row count — or null when the employer has no integration. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.get_hris( - employer_id="empr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.list_invoices(...) -> EmployerInvoicesListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns a cursor-paginated page of the employer's billing invoices, newest first. Pass the `next_offset` from a previous page as `offset` to fetch the next page. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.list_invoices( - employer_id="empr_abc123def456", - limit=20, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Maximum number of invoices per page - -
-
- -
-
- -**offset:** `typing.Optional[Offset]` — Opaque cursor from a previous page's next_offset - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.get_invoice_pdf(...) -> EmployerInvoicePdfResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns the time-limited PDF download link for a single invoice belonging to the employer's billing customer. `invoice_id` is the external Chargebee id (not a prefixed UUID). The caller must be authorized for the employer; an unknown or unauthorized employer or invoice returns 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.get_invoice_pdf( - employer_id="empr_abc123def456", - invoice_id="INV-00042", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**invoice_id:** `InvoiceId` — External Chargebee invoice id (not a prefixed UUID). - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.get_payroll_access_setup(...) -> PayrollAccessSetupStatusResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Return whether the employer has submitted payroll access setup. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.get_payroll_access_setup( - employer_id="empr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.submit_payroll_access_setup(...) -> PayrollAccessSetupStatusResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Submit the employer's payroll access setup answers. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.submit_payroll_access_setup( - employer_id="empr_abc123def456", - employees_in_payroll_acknowledged=True, - payroll_data_impacts_eligibility_acknowledged=True, - classifications_accurate=True, - all_benefit_eligible_employees_present=True, - is_controlled_group=True, - access_method="SELF_SETUP", - has_additional_payroll_system=True, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` - -
-
- -
-
- -**employees_in_payroll_acknowledged:** `bool` — Attestation that all benefit-eligible employees appear in the payroll system. - -
-
- -
-
- -**payroll_data_impacts_eligibility_acknowledged:** `bool` — Attestation that changes to payroll data affect benefit eligibility. - -
-
- -
-
- -**classifications_accurate:** `bool` — Attestation that employee classifications in payroll are accurate. Set `false` to report corrections in the fields below. - -
-
- -
-
- -**all_benefit_eligible_employees_present:** `bool` — Attestation that every benefit-eligible employee is present in payroll. - -
-
- -
-
- -**is_controlled_group:** `bool` — Whether this employer belongs to a controlled group of related entities. - -
-
- -
-
- -**access_method:** `AccessMethod` - -
-
- -
-
- -**has_additional_payroll_system:** `bool` — Whether a second payroll system is in use. When `true`, supply the `additional_*` fields below. - -
-
- -
-
- -**classification_correction_source:** `typing.Optional[ClassificationCorrectionSource]` — Where corrected classifications come from, when `classifications_accurate` is `false`. - -
-
- -
-
- -**misclassified_employee_names:** `typing.Optional[typing.List[str]]` — Names of employees whose payroll classification needs correcting. - -
-
- -
-
- -**remaining_employee_action:** `typing.Optional[RemainingEmployeeAction]` — How to handle employees still missing from the payroll system. - -
-
- -
-
- -**missing_employee_resolution:** `typing.Optional[MissingEmployeeResolution]` — How any missing employees will be added, when some are absent. - -
-
- -
-
- -**same_payroll_covers_other_eins:** `typing.Optional[bool]` — Whether this payroll system also covers other EINs in the controlled group. - -
-
- -
-
- -**login_url:** `typing.Optional[str]` — Sign-in URL for the payroll system. - -
-
- -
-
- -**username:** `typing.Optional[str]` — Username Vitable should use to access the payroll system. - -
-
- -
-
- -**phone:** `typing.Optional[str]` — Phone number used for payroll-system verification codes. - -
-
- -
-
- -**password:** `typing.Optional[str]` — Password Vitable should use to access the payroll system. - -
-
- -
-
- -**integration_confirmed:** `typing.Optional[bool]` — Whether the payroll integration has been confirmed as working. - -
-
- -
-
- -**additional_access_method:** `typing.Optional[AdditionalAccessMethod]` — How Vitable will access the second payroll system. - -
-
- -
-
- -**additional_login_url:** `typing.Optional[str]` — Sign-in URL for the second payroll system. - -
-
- -
-
- -**additional_username:** `typing.Optional[str]` — Username Vitable should use for the second payroll system. - -
-
- -
-
- -**additional_phone:** `typing.Optional[str]` — Phone number used for second payroll-system verification codes. - -
-
- -
-
- -**additional_password:** `typing.Optional[str]` — Password Vitable should use for the second payroll system. - -
-
- -
-
- -**additional_integration_confirmed:** `typing.Optional[bool]` — Whether the second payroll integration has been confirmed as working. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.list_payroll_deduction_statements(...) -> EmployerPayrollDeductionStatementListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns a paginated list of the employer's payroll-deduction statements, newest period first, each with its period, generation date, distinct employee count, total deduction, change-file link, and deduction frequency. Statements superseded by a later correction are excluded. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.list_payroll_deduction_statements( - employer_id="empr_abc123def456", - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Maximum number of statements per page - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number to retrieve (starts at 1) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.ensure_payroll_integration_email(...) -> PayrollIntegrationEmailResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Provision and return the employer's payroll integration email. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.ensure_payroll_integration_email( - employer_id="empr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.update_settings(...) -> EmployerSettingsResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Updates configuration settings for a specific employer. The employer must belong to the authenticated organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.update_settings( - employer_id="empr_abc123def456", - pay_frequency="bi_weekly", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**employer_id:** `EmployerId` — Unique employer identifier (empr_*) - -
-
- -
-
- -**pay_frequency:** `DeductionFrequency` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.employers.list_hris_providers() -> OrganizationHrisProvidersResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns the distinct HRIS/payroll providers across the same book `GET /v1/employers` returns, sorted for display. Use these as the values for the employers list's `hris_provider` filter — filter on `provider`, show `provider_label`. The stored providers are free text, so they cannot be enumerated in advance. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.employers.list_hris_providers() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Enrollments -
client.enrollments.get(...) -> EnrollmentResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a single enrollment: the employee and employer it belongs to, the benefit product, its status, the coverage period, the employee payroll deduction and employer contribution, and the enrolled plan's Summary of Benefits and Coverage document when one is on file. An enrollment the caller cannot reach is indistinguishable from one that does not exist. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.enrollments.get( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**enrollment_id:** `EnrollmentId` — Unique enrollment identifier (enrl_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.enrollments.reissue(...) -> ReissueEnrollmentResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Closes the targeted enrollment and creates a new unanswered enrollment for the same member and plan year. VPC never requires a qualifying life event; other products require an accepted, member-owned event outside open enrollment. User-backed callers must provide a reason; it is optional for organization API-key callers. Tenant mismatches return a non-disclosing 404 before the request body is validated. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.enrollments.reissue( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", - reason="Member needs a new election after a qualifying event.", - ticket_number="BPT-1234", - qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**enrollment_id:** `EnrollmentId` — Unique enrollment identifier (enrl_*) - -
-
- -
-
- -**reason:** `typing.Optional[str]` — Audit reason for the reissue; required for user-backed callers and optional for long-lived organization API-key callers - -
-
- -
-
- -**ticket_number:** `typing.Optional[str]` — Optional support or operational ticket number - -
-
- -
-
- -**qualifying_life_event_id:** `typing.Optional[str]` — Accepted member qualifying life event identifier (qle_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.enrollments.terminate(...) -
-
- -#### 📝 Description - -
-
- -
-
- -Terminates enrolled coverage immediately. An accepted qualifying life event owned by the enrollment member is required unless the plan is VPC or ICHRA. User-backed callers must provide a reason; it is optional for organization API-key callers. API keys may act across the caller organization's book. Tenant mismatches return the same non-disclosing 404 before the request body is validated. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.enrollments.terminate( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", - reason="Member requested coverage termination after a qualifying event.", - ticket_number="BPT-1234", - qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**enrollment_id:** `EnrollmentId` — Unique enrollment identifier (enrl_*) - -
-
- -
-
- -**reason:** `typing.Optional[str]` — Audit reason for the termination; required for user-backed callers and optional for long-lived organization API-key callers - -
-
- -
-
- -**ticket_number:** `typing.Optional[str]` — Optional support or operational ticket number - -
-
- -
-
- -**qualifying_life_event_id:** `typing.Optional[str]` — Accepted member qualifying life event identifier (qle_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Groups -
client.groups.list(...) -> GroupListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns a paginated list of groups belonging to the authenticated organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.groups.list( - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Items per page (default: 20, max: 100) - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number (default: 1) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.groups.create(...) -> GroupResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Creates a new group scoped to the authenticated organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.groups.create( - name="Tier 1", - external_reference_id="mol_seg_001", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` — Display name for the group. - -
-
- -
-
- -**external_reference_id:** `str` — Your own identifier for this group. Use it to correlate the group with a record in your system; it must be unique within your organization. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.groups.get(...) -> GroupResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a single group by its prefixed ID. Returns 404 if the group does not belong to the authenticated organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.groups.get( - group_id="grp_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**group_id:** `GroupId` — Unique group identifier (grp_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.groups.update(...) -> GroupResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Partially updates a group's name or external reference ID. Returns 404 if the group does not belong to the authenticated organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.groups.update( - group_id="grp_abc123def456", - name="Tier 1 (renamed)", - external_reference_id="mol_seg_001_v2", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**group_id:** `GroupId` — Unique group identifier (grp_*) - -
-
- -
-
- -**name:** `typing.Optional[str]` — New display name for the group. Omit to leave unchanged. - -
-
- -
-
- -**external_reference_id:** `typing.Optional[str]` — New external reference ID for the group. Omit to leave unchanged. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Members -
client.members.get(...) -> MemberResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a member's profile by ID — identity, demographics, address, contact details, tobacco status, and profile status. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.members.get( - member_id="mbr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**member_id:** `MemberId` — Unique member identifier (mbr_*) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.members.list_dependents(...) -> MemberDependentsResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Lists a member's active legal dependents — name, relationship, date of birth, age, and sex at birth. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.members.list_dependents( - member_id="mbr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**member_id:** `MemberId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.members.list_employments(...) -> MemberEmploymentsResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Lists a member's employment across every employer — the same employee record shape as the employer's employees list, plus the employer name. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all employments. A member not visible to the caller returns a 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.members.list_employments( - member_id="mbr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**member_id:** `MemberId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.members.list_enrollments(...) -> MemberEnrollmentsResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Lists a member's benefit enrollments across every employer — benefit type and product, employer, carrier, plan, tier, employee deduction, employer contribution and total premium, the individual enrollment coverage boundary (`coverage_end`), the separate pre-effective cancellation boundary (`cancelled_date`), and the distinct benefit plan-year boundary (`plan_year_coverage_end`) used to determine whether the plan year itself has ended, the date the enrollment record was created (`issued_date`, the value Ops labels Issued on, reported for every row whatever the member answered), the window the member could answer in -- which never opens before the enrollment was issued, so a row issued mid-open-enrollment starts its window on its issue date -- whether a qualifying life event would currently be required for reissue under the product/open-enrollment rule, enrollment/open-enrollment window, and two statuses: `election_status` (what the member answered) and `policy_status` (what became of their coverage, null unless they enrolled). Every row includes a stable enrollment ID and the exact employer and benefit plan-year IDs used to fetch that row's plan-year detail. The full list is returned across all states so the client derives active plans (effective and upcoming) and the enrollment history from those per-row statuses. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all enrollments. A member not visible to the caller returns a 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.members.list_enrollments( - member_id="mbr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**member_id:** `MemberId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.members.get_household(...) -> HouseholdMembersResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Lists a member's household as a per-participant table — the account holder plus each active household member, with name, relationship, member type, date of birth, and household-admin flag. Access is scoped to the authenticated principal; a member not visible to the caller (or with no household) returns a 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.members.get_household( - member_id="mbr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**member_id:** `MemberId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.members.list_id_cards(...) -> MemberDigitalBenefitCardsResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Lists a member's benefit ID cards — card type (medical, dental, vision, or rx), employer, plan, provider network, claims payer, carrier contact details, and the disclaimers printed on the card. Medical, dental and vision cards come from the member's active digital benefit cards; the rx card from the member's Ventegra pharmacy benefit (omitted when the member has no free-medication coverage), which carries no plan, network, or carrier details. Access is scoped to the authenticated principal, and an organization caller sees only cards from employers in its book; a member not visible to the caller returns a 404. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.members.list_id_cards( - member_id="mbr_abc123def456", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**member_id:** `MemberId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.members.list_qualifying_life_events(...) -> MemberQualifyingLifeEventListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Lists a member's qualifying life events, including events already used for another enrollment. Returns all statuses by default; pass the status query param to filter to one (e.g. approved). Events are ordered newest submission first with stable paging. Custom text is present only when submitted and is otherwise null. A member not visible to the caller returns a 404. API keys and unbound access tokens have organization-wide access. Employer-bound tokens require employment at the bound employer, and employee-bound tokens require the exact employee-member relationship. Organization or scope mismatches return a 404 before pagination is validated. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.members.list_qualifying_life_events( - member_id="mbr_abc123def456", - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**member_id:** `MemberId` — Unique member identifier (mbr_*) - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Items per page (default: 20, max: 100) - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number (default: 1) - -
-
- -
-
- -**status:** `typing.Optional[Status]` — Optional. Filter to a single QLE status; omit to return all statuses. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.members.list(...) -> MemberListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a paginated list of the members in the authenticated organization's book — identity, contact details, and address. The book covers members reached through an employer in the organization's book as well as members of a group it owns. Supports free-text search (name, email, phone number, or exact member id). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.members.list( - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Items per page (default: 20, max: 100) - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number (default: 1) - -
-
- -
-
- -**search:** `typing.Optional[MemberSearch]` — Case-insensitive search across member name, email, and phone number; exact match on member id (prefixed or raw uuid) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Organizations -
client.organizations.list() -> OrganizationsListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Lists the organizations the authenticated caller is an active member of (paginated). Returns an empty list when the caller belongs to no organizations. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.organizations.list() - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.organizations.create(...) -> Organization -
-
- -#### 📝 Description - -
-
- -
-
- -Onboards the authenticated user's partner Organization: creates the local Organization + the creator's admin membership atomically, then mirrors it to WorkOS (creates the WorkOS org and binds the creator as admin). 409 `organization_already_exists` when the user already has an organization (v0: one organization per user). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.organizations.create( - name="Acme Brokerage", - type="BROKERAGE", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**name:** `str` — Legal or trading name of the organization. - -
-
- -
-
- -**type:** `typing.Optional[CreateOrganizationRequestType]` — Category of organization being onboarded. - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Plans -
client.plans.list(...) -> PlanListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Returns a paginated list of benefit plans linked to the authenticated organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.plans.list( - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` — Items per page (default: 20, max: 100) - -
-
- -
-
- -**page:** `typing.Optional[Page]` — Page number (default: 1) - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Webhook Events -
client.webhook_events.list(...) -> WebhookEventListResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a paginated list of webhook events for the authenticated organization. Supports filtering by event name, resource type, resource ID, and date range. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.webhook_events.list( - limit=20, - page=1, -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**created_after:** `typing.Optional[CreatedAfter]` - -
-
- -
-
- -**created_before:** `typing.Optional[CreatedBefore]` - -
-
- -
-
- -**event_name:** `typing.Optional[EventName]` - -* `enrollment.accepted` - Enrollment Accepted -* `enrollment.terminated` - Enrollment Terminated -* `enrollment.termination_rescheduled` - Enrollment Termination Rescheduled -* `enrollment.elected` - Enrollment Elected -* `enrollment.granted` - Enrollment Granted -* `enrollment.waived` - Enrollment Waived -* `enrollment.started` - Enrollment Started -* `employee.eligibility_granted` - Employee Eligibility Granted -* `employee.eligibility_terminated` - Employee Eligibility Terminated -* `employee.deactivated` - Employee Deactivated -* `employee.deduction_created` - Employee Deduction Created - -
-
- -
-
- -**limit:** `typing.Optional[Limit]` - -
-
- -
-
- -**page:** `typing.Optional[Page]` - -
-
- -
-
- -**resource_id:** `typing.Optional[ResourceId]` - -
-
- -
-
- -**resource_type:** `typing.Optional[ResourceType]` - -* `enrollment` - Enrollment -* `employee` - Employee -* `employer` - Employer -* `dependent` - Dependent -* `plan_year` - Plan Year -* `payroll_deduction` - Payroll Deduction - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.webhook_events.get(...) -> WebhookEventResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a single webhook event by its prefixed ID. Returns 404 if the event does not exist or belongs to a different organization. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.webhook_events.get( - event_id="event_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**event_id:** `EventId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.webhook_events.list_deliveries(...) -> ListWebhookEventDeliveriesResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves all delivery attempts for a webhook event. Returns up to 100 deliveries. Each delivery includes a computed status field (Pending, In Progress, Delivered, or Failed). -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.webhook_events.list_deliveries( - event_id="event_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**event_id:** `EventId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -## Groups Members Sync -
client.groups.members.sync.submit(...) -> GroupMemberSyncDetailResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Submits a member sync payload for the specified group. Members in the payload will be queued for processing asynchronously. Returns HTTP 202 with the batch ID and acceptance timestamp. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect, GroupMemberSyncMemberRequest, AddressRequest -from vitable_connect.environment import VitableConnectEnvironment -import datetime - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.groups.members.sync.submit( - group_id="grp_abc123def456", - members=[ - GroupMemberSyncMemberRequest( - reference_id="EMP-001", - first_name="Jane", - last_name="Doe", - date_of_birth=datetime.date.fromisoformat("1990-05-15"), - phone="4155550100", - plan_id="pln_abc123def456", - address=AddressRequest( - address_line1="123 Main Street", - address_line2="Apt 4B", - city="San Francisco", - state="CA", - zipcode="94102", - ), - email="jane.doe@acme.com", - ) - ], -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**group_id:** `GroupId` - -
-
- -
-
- -**members:** `typing.List[GroupMemberSyncMemberRequest]` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- -
client.groups.members.sync.get(...) -> GroupMemberSyncRequestDetailResponse -
-
- -#### 📝 Description - -
-
- -
-
- -Retrieves a previously-submitted group member sync request by its `grpmsr_` ID. Returns the acceptance timestamp, completion timestamp (if processing has finished), and the per-member `results` once available. While processing is in flight, `completed_at` and `results` are `null`. -
-
-
-
- -#### 🔌 Usage - -
-
- -
-
- -```python -from vitable_connect import VitableConnect -from vitable_connect.environment import VitableConnectEnvironment - -client = VitableConnect( - api_key="", - environment=VitableConnectEnvironment.PRODUCTION, -) - -client.groups.members.sync.get( - group_id="grp_abc123def456", - request_id="request_id", -) - -``` -
-
-
-
- -#### ⚙️ Parameters - -
-
- -
-
- -**group_id:** `GroupId` - -
-
- -
-
- -**request_id:** `RequestId` - -
-
- -
-
- -**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. - -
-
-
-
- - -
-
-
- diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..c3536c4 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,66 @@ +{ + "packages": { + ".": {} + }, + "$schema": "https://raw.githubusercontent.com/stainless-api/release-please/main/schemas/config.json", + "include-v-in-tag": true, + "include-component-in-tag": false, + "versioning": "prerelease", + "prerelease": true, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "pull-request-header": "Automated Release PR", + "pull-request-title-pattern": "release: ${version}", + "changelog-sections": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "revert", + "section": "Reverts" + }, + { + "type": "chore", + "section": "Chores" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "style", + "section": "Styles" + }, + { + "type": "refactor", + "section": "Refactors" + }, + { + "type": "test", + "section": "Tests", + "hidden": true + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + } + ], + "release-type": "python", + "extra-files": [ + "src/vitable_connect/_version.py" + ] +} \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..4aef977 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,110 @@ +# This file was autogenerated by uv via the following command: +# uv export -o requirements-dev.lock --no-hashes +-e . +annotated-types==0.7.0 + # via pydantic +anyio==4.12.1 + # via + # httpx + # vitable-connect +backports-asyncio-runner==1.2.0 ; python_full_version < '3.11' + # via pytest-asyncio +certifi==2026.1.4 + # via + # httpcore + # httpx +colorama==0.4.6 ; sys_platform == 'win32' + # via pytest +dirty-equals==0.11 +distro==1.9.0 + # via vitable-connect +exceptiongroup==1.3.1 ; python_full_version < '3.11' + # via + # anyio + # pytest +execnet==2.1.2 + # via pytest-xdist +h11==0.16.0 + # via httpcore +httpcore==1.0.9 + # via httpx +httpx==0.28.1 + # via + # respx + # vitable-connect +idna==3.11 + # via + # anyio + # httpx +importlib-metadata==8.7.1 +iniconfig==2.1.0 ; python_full_version < '3.10' + # via pytest +iniconfig==2.3.0 ; python_full_version >= '3.10' + # via pytest +markdown-it-py==3.0.0 ; python_full_version < '3.10' + # via rich +markdown-it-py==4.0.0 ; python_full_version >= '3.10' + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy==1.17.0 +mypy-extensions==1.1.0 + # via mypy +nodeenv==1.10.0 + # via pyright +packaging==25.0 + # via pytest +pathspec==1.0.3 + # via mypy +pluggy==1.6.0 + # via pytest +pydantic==2.12.5 + # via vitable-connect +pydantic-core==2.41.5 + # via pydantic +pygments==2.19.2 + # via + # pytest + # rich +pyright==1.1.399 +pytest==8.4.2 ; python_full_version < '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest==9.0.2 ; python_full_version >= '3.10' + # via + # pytest-asyncio + # pytest-xdist +pytest-asyncio==1.2.0 ; python_full_version < '3.10' +pytest-asyncio==1.3.0 ; python_full_version >= '3.10' +pytest-xdist==3.8.0 +python-dateutil==2.9.0.post0 ; python_full_version < '3.10' + # via time-machine +respx==0.22.0 +rich==14.2.0 +ruff==0.14.13 +six==1.17.0 ; python_full_version < '3.10' + # via python-dateutil +sniffio==1.3.1 + # via vitable-connect +time-machine==2.19.0 ; python_full_version < '3.10' +time-machine==3.2.0 ; python_full_version >= '3.10' +tomli==2.4.0 ; python_full_version < '3.11' + # via + # mypy + # pytest +typing-extensions==4.15.0 + # via + # anyio + # exceptiongroup + # mypy + # pydantic + # pydantic-core + # pyright + # pytest-asyncio + # typing-inspection + # vitable-connect +typing-inspection==0.4.2 + # via pydantic +zipp==3.23.0 + # via importlib-metadata diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 443b1af..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -httpx>=0.21.2 -pydantic>= 1.9.2 -pydantic-core>=2.18.2,<3.0.0 -typing_extensions>= 4.0.0 diff --git a/scripts/bootstrap b/scripts/bootstrap new file mode 100755 index 0000000..5a23841 --- /dev/null +++ b/scripts/bootstrap @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then + brew bundle check >/dev/null 2>&1 || { + echo -n "==> Install Homebrew dependencies? (y/N): " + read -r response + case "$response" in + [yY][eE][sS]|[yY]) + brew bundle + ;; + *) + ;; + esac + echo + } +fi + +echo "==> Installing Python…" +uv python install + +echo "==> Installing Python dependencies…" +uv sync --all-extras + +echo "==> Exporting Python dependencies…" +# note: `--no-hashes` is required because of https://github.com/pypa/pip/issues/4995 +uv export -o requirements-dev.lock --no-hashes diff --git a/scripts/format b/scripts/format new file mode 100755 index 0000000..c8e1f69 --- /dev/null +++ b/scripts/format @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +echo "==> Running ruff" +uv run ruff format +uv run ruff check --fix . +# run formatting again to fix any inconsistencies when imports are stripped +uv run ruff format + +echo "==> Formatting docs" +uv run python scripts/utils/ruffen-docs.py README.md $(find . -type f -name api.md) diff --git a/scripts/lint b/scripts/lint new file mode 100755 index 0000000..8675467 --- /dev/null +++ b/scripts/lint @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + +if [ "$1" = "--fix" ]; then + echo "==> Running ruff with --fix" + uv run ruff check . --fix +else + echo "==> Running ruff" + uv run ruff check . +fi + +echo "==> Running pyright" +uv run pyright -p . + +echo "==> Running mypy" +uv run mypy . + +echo "==> Making sure it imports" +uv run python -c 'import vitable_connect' diff --git a/scripts/test b/scripts/test new file mode 100755 index 0000000..fe50ebb --- /dev/null +++ b/scripts/test @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +set -e + +cd "$(dirname "$0")/.." + + + +export DEFER_PYDANTIC_BUILD=false + +# Note that we need to specify the patch version here so that uv +# won't use unstable (alpha, beta, rc) releases for the tests +PY_VERSION_MIN=">=3.9.0" +PY_VERSION_MAX=">=3.14.0" + +function run_tests() { + echo "==> Running tests with Pydantic v2" + uv run --isolated --all-extras pytest "$@" + + # Skip Pydantic v1 tests on latest Python (not supported) + if [[ "$UV_PYTHON" != "$PY_VERSION_MAX" ]]; then + echo "==> Running tests with Pydantic v1" + uv run --isolated --all-extras --group=pydantic-v1 pytest "$@" + fi +} + +# If UV_PYTHON is already set in the environment, just run the command once +if [[ -n "$UV_PYTHON" ]]; then + run_tests "$@" +else + # If UV_PYTHON is not set, run the command for min and max versions + + echo "==> Running tests for Python $PY_VERSION_MIN" + UV_PYTHON="$PY_VERSION_MIN" run_tests "$@" + + echo "==> Running tests for Python $PY_VERSION_MAX" + UV_PYTHON="$PY_VERSION_MAX" run_tests "$@" +fi diff --git a/scripts/utils/ruffen-docs.py b/scripts/utils/ruffen-docs.py new file mode 100644 index 0000000..0cf2bd2 --- /dev/null +++ b/scripts/utils/ruffen-docs.py @@ -0,0 +1,167 @@ +# fork of https://github.com/asottile/blacken-docs adapted for ruff +from __future__ import annotations + +import re +import sys +import argparse +import textwrap +import contextlib +import subprocess +from typing import Match, Optional, Sequence, Generator, NamedTuple, cast + +MD_RE = re.compile( + r"(?P^(?P *)```\s*python\n)" r"(?P.*?)" r"(?P^(?P=indent)```\s*$)", + re.DOTALL | re.MULTILINE, +) +MD_PYCON_RE = re.compile( + r"(?P^(?P *)```\s*pycon\n)" r"(?P.*?)" r"(?P^(?P=indent)```.*$)", + re.DOTALL | re.MULTILINE, +) +PYCON_PREFIX = ">>> " +PYCON_CONTINUATION_PREFIX = "..." +PYCON_CONTINUATION_RE = re.compile( + rf"^{re.escape(PYCON_CONTINUATION_PREFIX)}( |$)", +) +DEFAULT_LINE_LENGTH = 100 + + +class CodeBlockError(NamedTuple): + offset: int + exc: Exception + + +def format_str( + src: str, +) -> tuple[str, Sequence[CodeBlockError]]: + errors: list[CodeBlockError] = [] + + @contextlib.contextmanager + def _collect_error(match: Match[str]) -> Generator[None, None, None]: + try: + yield + except Exception as e: + errors.append(CodeBlockError(match.start(), e)) + + def _md_match(match: Match[str]) -> str: + code = textwrap.dedent(match["code"]) + with _collect_error(match): + code = format_code_block(code) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + def _pycon_match(match: Match[str]) -> str: + code = "" + fragment = cast(Optional[str], None) + + def finish_fragment() -> None: + nonlocal code + nonlocal fragment + + if fragment is not None: + with _collect_error(match): + fragment = format_code_block(fragment) + fragment_lines = fragment.splitlines() + code += f"{PYCON_PREFIX}{fragment_lines[0]}\n" + for line in fragment_lines[1:]: + # Skip blank lines to handle Black adding a blank above + # functions within blocks. A blank line would end the REPL + # continuation prompt. + # + # >>> if True: + # ... def f(): + # ... pass + # ... + if line: + code += f"{PYCON_CONTINUATION_PREFIX} {line}\n" + if fragment_lines[-1].startswith(" "): + code += f"{PYCON_CONTINUATION_PREFIX}\n" + fragment = None + + indentation = None + for line in match["code"].splitlines(): + orig_line, line = line, line.lstrip() + if indentation is None and line: + indentation = len(orig_line) - len(line) + continuation_match = PYCON_CONTINUATION_RE.match(line) + if continuation_match and fragment is not None: + fragment += line[continuation_match.end() :] + "\n" + else: + finish_fragment() + if line.startswith(PYCON_PREFIX): + fragment = line[len(PYCON_PREFIX) :] + "\n" + else: + code += orig_line[indentation:] + "\n" + finish_fragment() + return code + + def _md_pycon_match(match: Match[str]) -> str: + code = _pycon_match(match) + code = textwrap.indent(code, match["indent"]) + return f"{match['before']}{code}{match['after']}" + + src = MD_RE.sub(_md_match, src) + src = MD_PYCON_RE.sub(_md_pycon_match, src) + return src, errors + + +def format_code_block(code: str) -> str: + return subprocess.check_output( + [ + sys.executable, + "-m", + "ruff", + "format", + "--stdin-filename=script.py", + f"--line-length={DEFAULT_LINE_LENGTH}", + ], + encoding="utf-8", + input=code, + ) + + +def format_file( + filename: str, + skip_errors: bool, +) -> int: + with open(filename, encoding="UTF-8") as f: + contents = f.read() + new_contents, errors = format_str(contents) + for error in errors: + lineno = contents[: error.offset].count("\n") + 1 + print(f"{filename}:{lineno}: code block parse error {error.exc}") + if errors and not skip_errors: + return 1 + if contents != new_contents: + print(f"{filename}: Rewriting...") + with open(filename, "w", encoding="UTF-8") as f: + f.write(new_contents) + return 0 + else: + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument( + "-l", + "--line-length", + type=int, + default=DEFAULT_LINE_LENGTH, + ) + parser.add_argument( + "-S", + "--skip-string-normalization", + action="store_true", + ) + parser.add_argument("-E", "--skip-errors", action="store_true") + parser.add_argument("filenames", nargs="*") + args = parser.parse_args(argv) + + retv = 0 + for filename in args.filenames: + retv |= format_file(filename, skip_errors=args.skip_errors) + return retv + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/utils/upload-artifact.sh b/scripts/utils/upload-artifact.sh new file mode 100755 index 0000000..2460b48 --- /dev/null +++ b/scripts/utils/upload-artifact.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -exuo pipefail + +FILENAME=$(basename dist/*.whl) + +RESPONSE=$(curl -X POST "$URL?filename=$FILENAME" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + +SIGNED_URL=$(echo "$RESPONSE" | jq -r '.url') + +if [[ "$SIGNED_URL" == "null" ]]; then + echo -e "\033[31mFailed to get signed URL.\033[0m" + exit 1 +fi + +UPLOAD_RESPONSE=$(curl -v -X PUT \ + -H "Content-Type: binary/octet-stream" \ + --data-binary "@dist/$FILENAME" "$SIGNED_URL" 2>&1) + +if echo "$UPLOAD_RESPONSE" | grep -q "HTTP/[0-9.]* 200"; then + echo -e "\033[32mUploaded build to Stainless storage.\033[0m" + echo -e "\033[32mInstallation: pip install 'https://pkg.stainless.com/s/vitable-connect-python/$SHA/$FILENAME'\033[0m" +else + echo -e "\033[31mFailed to upload artifact.\033[0m" + exit 1 +fi diff --git a/src/vitable_connect/__init__.py b/src/vitable_connect/__init__.py index 48d1ae4..6dd7acf 100644 --- a/src/vitable_connect/__init__.py +++ b/src/vitable_connect/__init__.py @@ -1,626 +1,104 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .types import ( - AccessMethod, - AccessTokenResponse, - AdditionalAccessMethod, - Address, - AddressRequest, - BenefitFamily, - BenefitFamilyParamItem, - BenefitLifecycleStageItem, - BenefitPlanNetwork, - BenefitPlanYearDetail, - BenefitPlanYearId, - BenefitPlanYearList, - BenefitStatus, - BenefitSummary, - BoundEntity, - BoundEntityResponse, - BoundEntityType, - CardType, - Category, - CensusSyncDetailResponse, - CensusSyncEmployeeAddressRequest, - CensusSyncEmployeeRequest, - CensusSyncResponse, - ClassificationCorrectionSource, - CompensationType, - ContributionClassCompensation, - ContributionClassEmployment, - ContributionClassLocation, - ContributionRange, - ContributionStrategy, - ContributionTier, - CoverageTier, - CreateOrganizationRequestType, - CreatedAfter, - CreatedBefore, - DeductionDetail, - DeductionFrequency, - DigitalBenefitCard, - ElectionStatusItem, - EligibilityPolicy, - EligibilityPolicyRule, - EligibilityRuleCompensationType, - EligibilityRuleEmployeeClass, - EligibleGeographicalLocation, - EligibleGeographicalLocationType, - Employee, - EmployeeAddress, - EmployeeAddressInput, - EmployeeClass, - EmployeeId, - EmployeeListResponse, - EmployeeResponse, - EmployeeSearch, - EmployeeStatus, - Employer, - EmployerAddress, - EmployerAddressInput, - EmployerBenefitLifecycleStage, - EmployerBenefitPlanYearResponse, - EmployerBenefitPlanYearsListResponse, - EmployerContact, - EmployerHris, - EmployerHrisResponse, - EmployerHrisStatus, - EmployerId, - EmployerInvoicePdf, - EmployerInvoicePdfResponse, - EmployerInvoicesListResponse, - EmployerPayrollDeductionStatementListResponse, - EmployerResponse, - EmployerSearch, - EmployerSettings, - EmployerSettingsResponse, - Enrollment, - EnrollmentDetail, - EnrollmentId, - EnrollmentListResponse, - EnrollmentRate, - EnrollmentRateSummary, - EnrollmentResponse, - EnrollmentStatus, - ErrorResponse, - EventId, - EventName, - Gender, - GrantType, - Group, - GroupId, - GroupListResponse, - GroupMemberSyncDetailResponse, - GroupMemberSyncFailure, - GroupMemberSyncMemberRequest, - GroupMemberSyncRequestDetail, - GroupMemberSyncRequestDetailResponse, - GroupMemberSyncResponse, - GroupMemberSyncResults, - GroupResponse, - HouseholdMember, - HouseholdMemberRelationship, - HouseholdMemberType, - HouseholdMembersResponse, - HrisConnection, - HrisStatusItem, - IchraContributionClass, - IdpProvider, - IncludeCancelled, - Invoice, - InvoiceId, - InvoicesCursorPagination, - Limit, - ListWebhookEventDeliveriesResponse, - MaritalStatus, - Member, - MemberAddress, - MemberDependentsResponse, - MemberDigitalBenefitCardsResponse, - MemberEmploymentsResponse, - MemberEnrollment, - MemberEnrollmentsResponse, - MemberId, - MemberLegalDependent, - MemberListItem, - MemberListResponse, - MemberQualifyingLifeEvent, - MemberQualifyingLifeEventListResponse, - MemberResponse, - MemberSearch, - MemberStatus, - MissingEmployeeResolution, - NameSuffix, - Offset, - Operation, - Organization, - OrganizationEmployer, - OrganizationEmployerListResponse, - OrganizationHrisProvider, - OrganizationHrisProvidersResponse, - OrganizationType, - OrganizationsListResponse, - Page, - Pagination, - PayrollAccessSetupStatus, - PayrollAccessSetupStatusResponse, - PayrollDeductionStatement, - PayrollIntegrationEmail, - PayrollIntegrationEmailResponse, - Plan, - PlanListResponse, - PlanYearEnrollment, - PlanYearEnrollmentElectionStatus, - PlanYearEnrollmentListResponse, - PlanYearEnrollmentPolicyStatus, - PreferredLanguage, - ProductCode, - QualifyingLifeEventStatus, - QualifyingLifeEventType, - ReissueEnrollment, - ReissueEnrollmentResponse, - Relationship, - RemainingEmployeeAction, - RequestId, - ResourceId, - ResourceType, - Search, - SexAtBirth, - State, - Status, - TaxClassification, - TerminationRule, - UpdateEmployerAddressInput, - WaitingPeriod, - WebhookDelivery, - WebhookEvent, - WebhookEventListResponse, - WebhookEventResponse, - ) - from .errors import ( - BadGatewayError, - BadRequestError, - ConflictError, - ForbiddenError, - InternalServerError, - NotFoundError, - TooManyRequestsError, - UnauthorizedError, - UnprocessableEntityError, - ) - from . import auth, employees, employers, enrollments, groups, members, organizations, plans, webhook_events - from ._default_clients import DefaultAioHttpClient, DefaultAsyncHttpxClient - from .client import AsyncVitableConnect, VitableConnect - from .environment import VitableConnectEnvironment - from .version import __version__ -_dynamic_imports: typing.Dict[str, str] = { - "AccessMethod": ".types", - "AccessTokenResponse": ".types", - "AdditionalAccessMethod": ".types", - "Address": ".types", - "AddressRequest": ".types", - "AsyncVitableConnect": ".client", - "BadGatewayError": ".errors", - "BadRequestError": ".errors", - "BenefitFamily": ".types", - "BenefitFamilyParamItem": ".types", - "BenefitLifecycleStageItem": ".types", - "BenefitPlanNetwork": ".types", - "BenefitPlanYearDetail": ".types", - "BenefitPlanYearId": ".types", - "BenefitPlanYearList": ".types", - "BenefitStatus": ".types", - "BenefitSummary": ".types", - "BoundEntity": ".types", - "BoundEntityResponse": ".types", - "BoundEntityType": ".types", - "CardType": ".types", - "Category": ".types", - "CensusSyncDetailResponse": ".types", - "CensusSyncEmployeeAddressRequest": ".types", - "CensusSyncEmployeeRequest": ".types", - "CensusSyncResponse": ".types", - "ClassificationCorrectionSource": ".types", - "CompensationType": ".types", - "ConflictError": ".errors", - "ContributionClassCompensation": ".types", - "ContributionClassEmployment": ".types", - "ContributionClassLocation": ".types", - "ContributionRange": ".types", - "ContributionStrategy": ".types", - "ContributionTier": ".types", - "CoverageTier": ".types", - "CreateOrganizationRequestType": ".types", - "CreatedAfter": ".types", - "CreatedBefore": ".types", - "DeductionDetail": ".types", - "DeductionFrequency": ".types", - "DefaultAioHttpClient": "._default_clients", - "DefaultAsyncHttpxClient": "._default_clients", - "DigitalBenefitCard": ".types", - "ElectionStatusItem": ".types", - "EligibilityPolicy": ".types", - "EligibilityPolicyRule": ".types", - "EligibilityRuleCompensationType": ".types", - "EligibilityRuleEmployeeClass": ".types", - "EligibleGeographicalLocation": ".types", - "EligibleGeographicalLocationType": ".types", - "Employee": ".types", - "EmployeeAddress": ".types", - "EmployeeAddressInput": ".types", - "EmployeeClass": ".types", - "EmployeeId": ".types", - "EmployeeListResponse": ".types", - "EmployeeResponse": ".types", - "EmployeeSearch": ".types", - "EmployeeStatus": ".types", - "Employer": ".types", - "EmployerAddress": ".types", - "EmployerAddressInput": ".types", - "EmployerBenefitLifecycleStage": ".types", - "EmployerBenefitPlanYearResponse": ".types", - "EmployerBenefitPlanYearsListResponse": ".types", - "EmployerContact": ".types", - "EmployerHris": ".types", - "EmployerHrisResponse": ".types", - "EmployerHrisStatus": ".types", - "EmployerId": ".types", - "EmployerInvoicePdf": ".types", - "EmployerInvoicePdfResponse": ".types", - "EmployerInvoicesListResponse": ".types", - "EmployerPayrollDeductionStatementListResponse": ".types", - "EmployerResponse": ".types", - "EmployerSearch": ".types", - "EmployerSettings": ".types", - "EmployerSettingsResponse": ".types", - "Enrollment": ".types", - "EnrollmentDetail": ".types", - "EnrollmentId": ".types", - "EnrollmentListResponse": ".types", - "EnrollmentRate": ".types", - "EnrollmentRateSummary": ".types", - "EnrollmentResponse": ".types", - "EnrollmentStatus": ".types", - "ErrorResponse": ".types", - "EventId": ".types", - "EventName": ".types", - "ForbiddenError": ".errors", - "Gender": ".types", - "GrantType": ".types", - "Group": ".types", - "GroupId": ".types", - "GroupListResponse": ".types", - "GroupMemberSyncDetailResponse": ".types", - "GroupMemberSyncFailure": ".types", - "GroupMemberSyncMemberRequest": ".types", - "GroupMemberSyncRequestDetail": ".types", - "GroupMemberSyncRequestDetailResponse": ".types", - "GroupMemberSyncResponse": ".types", - "GroupMemberSyncResults": ".types", - "GroupResponse": ".types", - "HouseholdMember": ".types", - "HouseholdMemberRelationship": ".types", - "HouseholdMemberType": ".types", - "HouseholdMembersResponse": ".types", - "HrisConnection": ".types", - "HrisStatusItem": ".types", - "IchraContributionClass": ".types", - "IdpProvider": ".types", - "IncludeCancelled": ".types", - "InternalServerError": ".errors", - "Invoice": ".types", - "InvoiceId": ".types", - "InvoicesCursorPagination": ".types", - "Limit": ".types", - "ListWebhookEventDeliveriesResponse": ".types", - "MaritalStatus": ".types", - "Member": ".types", - "MemberAddress": ".types", - "MemberDependentsResponse": ".types", - "MemberDigitalBenefitCardsResponse": ".types", - "MemberEmploymentsResponse": ".types", - "MemberEnrollment": ".types", - "MemberEnrollmentsResponse": ".types", - "MemberId": ".types", - "MemberLegalDependent": ".types", - "MemberListItem": ".types", - "MemberListResponse": ".types", - "MemberQualifyingLifeEvent": ".types", - "MemberQualifyingLifeEventListResponse": ".types", - "MemberResponse": ".types", - "MemberSearch": ".types", - "MemberStatus": ".types", - "MissingEmployeeResolution": ".types", - "NameSuffix": ".types", - "NotFoundError": ".errors", - "Offset": ".types", - "Operation": ".types", - "Organization": ".types", - "OrganizationEmployer": ".types", - "OrganizationEmployerListResponse": ".types", - "OrganizationHrisProvider": ".types", - "OrganizationHrisProvidersResponse": ".types", - "OrganizationType": ".types", - "OrganizationsListResponse": ".types", - "Page": ".types", - "Pagination": ".types", - "PayrollAccessSetupStatus": ".types", - "PayrollAccessSetupStatusResponse": ".types", - "PayrollDeductionStatement": ".types", - "PayrollIntegrationEmail": ".types", - "PayrollIntegrationEmailResponse": ".types", - "Plan": ".types", - "PlanListResponse": ".types", - "PlanYearEnrollment": ".types", - "PlanYearEnrollmentElectionStatus": ".types", - "PlanYearEnrollmentListResponse": ".types", - "PlanYearEnrollmentPolicyStatus": ".types", - "PreferredLanguage": ".types", - "ProductCode": ".types", - "QualifyingLifeEventStatus": ".types", - "QualifyingLifeEventType": ".types", - "ReissueEnrollment": ".types", - "ReissueEnrollmentResponse": ".types", - "Relationship": ".types", - "RemainingEmployeeAction": ".types", - "RequestId": ".types", - "ResourceId": ".types", - "ResourceType": ".types", - "Search": ".types", - "SexAtBirth": ".types", - "State": ".types", - "Status": ".types", - "TaxClassification": ".types", - "TerminationRule": ".types", - "TooManyRequestsError": ".errors", - "UnauthorizedError": ".errors", - "UnprocessableEntityError": ".errors", - "UpdateEmployerAddressInput": ".types", - "VitableConnect": ".client", - "VitableConnectEnvironment": ".environment", - "WaitingPeriod": ".types", - "WebhookDelivery": ".types", - "WebhookEvent": ".types", - "WebhookEventListResponse": ".types", - "WebhookEventResponse": ".types", - "__version__": ".version", - "auth": ".auth", - "employees": ".employees", - "employers": ".employers", - "enrollments": ".enrollments", - "groups": ".groups", - "members": ".members", - "organizations": ".organizations", - "plans": ".plans", - "webhook_events": ".webhook_events", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e - except AttributeError as e: - raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) +import typing as _t +from . import types +from ._types import NOT_GIVEN, Omit, NoneType, NotGiven, Transport, ProxiesTypes, omit, not_given +from ._utils import file_from_path +from ._client import ( + ENVIRONMENTS, + Client, + Stream, + Timeout, + Transport, + AsyncClient, + AsyncStream, + RequestOptions, + VitableConnect, + AsyncVitableConnect, +) +from ._models import BaseModel +from ._version import __title__, __version__ +from ._response import APIResponse as APIResponse, AsyncAPIResponse as AsyncAPIResponse +from ._constants import DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, DEFAULT_CONNECTION_LIMITS +from ._exceptions import ( + APIError, + ConflictError, + NotFoundError, + APIStatusError, + RateLimitError, + APITimeoutError, + BadRequestError, + APIConnectionError, + AuthenticationError, + InternalServerError, + VitableConnectError, + PermissionDeniedError, + UnprocessableEntityError, + APIResponseValidationError, +) +from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient +from ._utils._logs import setup_logging as _setup_logging __all__ = [ - "AccessMethod", - "AccessTokenResponse", - "AdditionalAccessMethod", - "Address", - "AddressRequest", - "AsyncVitableConnect", - "BadGatewayError", + "types", + "__version__", + "__title__", + "NoneType", + "Transport", + "ProxiesTypes", + "NotGiven", + "NOT_GIVEN", + "not_given", + "Omit", + "omit", + "VitableConnectError", + "APIError", + "APIStatusError", + "APITimeoutError", + "APIConnectionError", + "APIResponseValidationError", "BadRequestError", - "BenefitFamily", - "BenefitFamilyParamItem", - "BenefitLifecycleStageItem", - "BenefitPlanNetwork", - "BenefitPlanYearDetail", - "BenefitPlanYearId", - "BenefitPlanYearList", - "BenefitStatus", - "BenefitSummary", - "BoundEntity", - "BoundEntityResponse", - "BoundEntityType", - "CardType", - "Category", - "CensusSyncDetailResponse", - "CensusSyncEmployeeAddressRequest", - "CensusSyncEmployeeRequest", - "CensusSyncResponse", - "ClassificationCorrectionSource", - "CompensationType", - "ConflictError", - "ContributionClassCompensation", - "ContributionClassEmployment", - "ContributionClassLocation", - "ContributionRange", - "ContributionStrategy", - "ContributionTier", - "CoverageTier", - "CreateOrganizationRequestType", - "CreatedAfter", - "CreatedBefore", - "DeductionDetail", - "DeductionFrequency", - "DefaultAioHttpClient", - "DefaultAsyncHttpxClient", - "DigitalBenefitCard", - "ElectionStatusItem", - "EligibilityPolicy", - "EligibilityPolicyRule", - "EligibilityRuleCompensationType", - "EligibilityRuleEmployeeClass", - "EligibleGeographicalLocation", - "EligibleGeographicalLocationType", - "Employee", - "EmployeeAddress", - "EmployeeAddressInput", - "EmployeeClass", - "EmployeeId", - "EmployeeListResponse", - "EmployeeResponse", - "EmployeeSearch", - "EmployeeStatus", - "Employer", - "EmployerAddress", - "EmployerAddressInput", - "EmployerBenefitLifecycleStage", - "EmployerBenefitPlanYearResponse", - "EmployerBenefitPlanYearsListResponse", - "EmployerContact", - "EmployerHris", - "EmployerHrisResponse", - "EmployerHrisStatus", - "EmployerId", - "EmployerInvoicePdf", - "EmployerInvoicePdfResponse", - "EmployerInvoicesListResponse", - "EmployerPayrollDeductionStatementListResponse", - "EmployerResponse", - "EmployerSearch", - "EmployerSettings", - "EmployerSettingsResponse", - "Enrollment", - "EnrollmentDetail", - "EnrollmentId", - "EnrollmentListResponse", - "EnrollmentRate", - "EnrollmentRateSummary", - "EnrollmentResponse", - "EnrollmentStatus", - "ErrorResponse", - "EventId", - "EventName", - "ForbiddenError", - "Gender", - "GrantType", - "Group", - "GroupId", - "GroupListResponse", - "GroupMemberSyncDetailResponse", - "GroupMemberSyncFailure", - "GroupMemberSyncMemberRequest", - "GroupMemberSyncRequestDetail", - "GroupMemberSyncRequestDetailResponse", - "GroupMemberSyncResponse", - "GroupMemberSyncResults", - "GroupResponse", - "HouseholdMember", - "HouseholdMemberRelationship", - "HouseholdMemberType", - "HouseholdMembersResponse", - "HrisConnection", - "HrisStatusItem", - "IchraContributionClass", - "IdpProvider", - "IncludeCancelled", - "InternalServerError", - "Invoice", - "InvoiceId", - "InvoicesCursorPagination", - "Limit", - "ListWebhookEventDeliveriesResponse", - "MaritalStatus", - "Member", - "MemberAddress", - "MemberDependentsResponse", - "MemberDigitalBenefitCardsResponse", - "MemberEmploymentsResponse", - "MemberEnrollment", - "MemberEnrollmentsResponse", - "MemberId", - "MemberLegalDependent", - "MemberListItem", - "MemberListResponse", - "MemberQualifyingLifeEvent", - "MemberQualifyingLifeEventListResponse", - "MemberResponse", - "MemberSearch", - "MemberStatus", - "MissingEmployeeResolution", - "NameSuffix", + "AuthenticationError", + "PermissionDeniedError", "NotFoundError", - "Offset", - "Operation", - "Organization", - "OrganizationEmployer", - "OrganizationEmployerListResponse", - "OrganizationHrisProvider", - "OrganizationHrisProvidersResponse", - "OrganizationType", - "OrganizationsListResponse", - "Page", - "Pagination", - "PayrollAccessSetupStatus", - "PayrollAccessSetupStatusResponse", - "PayrollDeductionStatement", - "PayrollIntegrationEmail", - "PayrollIntegrationEmailResponse", - "Plan", - "PlanListResponse", - "PlanYearEnrollment", - "PlanYearEnrollmentElectionStatus", - "PlanYearEnrollmentListResponse", - "PlanYearEnrollmentPolicyStatus", - "PreferredLanguage", - "ProductCode", - "QualifyingLifeEventStatus", - "QualifyingLifeEventType", - "ReissueEnrollment", - "ReissueEnrollmentResponse", - "Relationship", - "RemainingEmployeeAction", - "RequestId", - "ResourceId", - "ResourceType", - "Search", - "SexAtBirth", - "State", - "Status", - "TaxClassification", - "TerminationRule", - "TooManyRequestsError", - "UnauthorizedError", + "ConflictError", "UnprocessableEntityError", - "UpdateEmployerAddressInput", + "RateLimitError", + "InternalServerError", + "Timeout", + "RequestOptions", + "Client", + "AsyncClient", + "Stream", + "AsyncStream", "VitableConnect", - "VitableConnectEnvironment", - "WaitingPeriod", - "WebhookDelivery", - "WebhookEvent", - "WebhookEventListResponse", - "WebhookEventResponse", - "__version__", - "auth", - "employees", - "employers", - "enrollments", - "groups", - "members", - "organizations", - "plans", - "webhook_events", + "AsyncVitableConnect", + "ENVIRONMENTS", + "file_from_path", + "BaseModel", + "DEFAULT_TIMEOUT", + "DEFAULT_MAX_RETRIES", + "DEFAULT_CONNECTION_LIMITS", + "DefaultHttpxClient", + "DefaultAsyncHttpxClient", + "DefaultAioHttpClient", ] + +if not _t.TYPE_CHECKING: + from ._utils._resources_proxy import resources as resources + +_setup_logging() + +# Update the __module__ attribute for exported symbols so that +# error messages point to this module instead of the module +# it was originally defined in, e.g. +# vitable_connect._exceptions.NotFoundError -> vitable_connect.NotFoundError +__locals = locals() +for __name in __all__: + if not __name.startswith("__"): + try: + __locals[__name].__module__ = "vitable_connect" + except (TypeError, AttributeError): + # Some of our exported symbols are builtins which we can't set attributes for. + pass diff --git a/src/vitable_connect/_base_client.py b/src/vitable_connect/_base_client.py new file mode 100644 index 0000000..5c2ff6e --- /dev/null +++ b/src/vitable_connect/_base_client.py @@ -0,0 +1,2153 @@ +from __future__ import annotations + +import sys +import json +import time +import uuid +import email +import asyncio +import inspect +import logging +import platform +import warnings +import email.utils +from types import TracebackType +from random import random +from typing import ( + TYPE_CHECKING, + Any, + Dict, + Type, + Union, + Generic, + Mapping, + TypeVar, + Iterable, + Iterator, + Optional, + Generator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Literal, override, get_origin + +import anyio +import httpx +import distro +import pydantic +from httpx import URL +from pydantic import PrivateAttr + +from . import _exceptions +from ._qs import Querystring +from ._files import to_httpx_files, async_to_httpx_files +from ._types import ( + Body, + Omit, + Query, + Headers, + Timeout, + NotGiven, + ResponseT, + AnyMapping, + PostParser, + BinaryTypes, + RequestFiles, + HttpxSendArgs, + RequestOptions, + AsyncBinaryTypes, + HttpxRequestFiles, + ModelBuilderProtocol, + not_given, +) +from ._utils import is_dict, is_list, asyncify, is_given, lru_cache, is_mapping +from ._compat import PYDANTIC_V1, model_copy, model_dump +from ._models import GenericModel, SecurityOptions, FinalRequestOptions, validate_type, construct_type +from ._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + extract_response_type, +) +from ._constants import ( + DEFAULT_TIMEOUT, + MAX_RETRY_DELAY, + DEFAULT_MAX_RETRIES, + INITIAL_RETRY_DELAY, + RAW_RESPONSE_HEADER, + OVERRIDE_CAST_TO_HEADER, + DEFAULT_CONNECTION_LIMITS, +) +from ._streaming import Stream, SSEDecoder, AsyncStream, SSEBytesDecoder +from ._exceptions import ( + APIStatusError, + APITimeoutError, + APIConnectionError, + APIResponseValidationError, +) +from ._utils._json import openapi_dumps + +log: logging.Logger = logging.getLogger(__name__) + +# TODO: make base page type vars covariant +SyncPageT = TypeVar("SyncPageT", bound="BaseSyncPage[Any]") +AsyncPageT = TypeVar("AsyncPageT", bound="BaseAsyncPage[Any]") + + +_T = TypeVar("_T") +_T_co = TypeVar("_T_co", covariant=True) + +_StreamT = TypeVar("_StreamT", bound=Stream[Any]) +_AsyncStreamT = TypeVar("_AsyncStreamT", bound=AsyncStream[Any]) + +if TYPE_CHECKING: + from httpx._config import ( + DEFAULT_TIMEOUT_CONFIG, # pyright: ignore[reportPrivateImportUsage] + ) + + HTTPX_DEFAULT_TIMEOUT = DEFAULT_TIMEOUT_CONFIG +else: + try: + from httpx._config import DEFAULT_TIMEOUT_CONFIG as HTTPX_DEFAULT_TIMEOUT + except ImportError: + # taken from https://github.com/encode/httpx/blob/3ba5fe0d7ac70222590e759c31442b1cab263791/httpx/_config.py#L366 + HTTPX_DEFAULT_TIMEOUT = Timeout(5.0) + + +class PageInfo: + """Stores the necessary information to build the request to retrieve the next page. + + Either `url` or `params` must be set. + """ + + url: URL | NotGiven + params: Query | NotGiven + json: Body | NotGiven + + @overload + def __init__( + self, + *, + url: URL, + ) -> None: ... + + @overload + def __init__( + self, + *, + params: Query, + ) -> None: ... + + @overload + def __init__( + self, + *, + json: Body, + ) -> None: ... + + def __init__( + self, + *, + url: URL | NotGiven = not_given, + json: Body | NotGiven = not_given, + params: Query | NotGiven = not_given, + ) -> None: + self.url = url + self.json = json + self.params = params + + @override + def __repr__(self) -> str: + if self.url: + return f"{self.__class__.__name__}(url={self.url})" + if self.json: + return f"{self.__class__.__name__}(json={self.json})" + return f"{self.__class__.__name__}(params={self.params})" + + +class BasePage(GenericModel, Generic[_T]): + """ + Defines the core interface for pagination. + + Type Args: + ModelT: The pydantic model that represents an item in the response. + + Methods: + has_next_page(): Check if there is another page available + next_page_info(): Get the necessary information to make a request for the next page + """ + + _options: FinalRequestOptions = PrivateAttr() + _model: Type[_T] = PrivateAttr() + + def has_next_page(self) -> bool: + items = self._get_page_items() + if not items: + return False + return self.next_page_info() is not None + + def next_page_info(self) -> Optional[PageInfo]: ... + + def _get_page_items(self) -> Iterable[_T]: # type: ignore[empty-body] + ... + + def _params_from_url(self, url: URL) -> httpx.QueryParams: + # TODO: do we have to preprocess params here? + return httpx.QueryParams(cast(Any, self._options.params)).merge(url.params) + + def _info_to_options(self, info: PageInfo) -> FinalRequestOptions: + options = model_copy(self._options) + options._strip_raw_response_header() + + if not isinstance(info.params, NotGiven): + options.params = {**options.params, **info.params} + return options + + if not isinstance(info.url, NotGiven): + params = self._params_from_url(info.url) + url = info.url.copy_with(params=params) + options.params = dict(url.params) + options.url = str(url) + return options + + if not isinstance(info.json, NotGiven): + if not is_mapping(info.json): + raise TypeError("Pagination is only supported with mappings") + + if not options.json_data: + options.json_data = {**info.json} + else: + if not is_mapping(options.json_data): + raise TypeError("Pagination is only supported with mappings") + + options.json_data = {**options.json_data, **info.json} + return options + + raise ValueError("Unexpected PageInfo state") + + +class BaseSyncPage(BasePage[_T], Generic[_T]): + _client: SyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + client: SyncAPIClient, + model: Type[_T], + options: FinalRequestOptions, + ) -> None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + + self._model = model + self._client = client + self._options = options + + # Pydantic uses a custom `__iter__` method to support casting BaseModels + # to dictionaries. e.g. dict(model). + # As we want to support `for item in page`, this is inherently incompatible + # with the default pydantic behaviour. It is not possible to support both + # use cases at once. Fortunately, this is not a big deal as all other pydantic + # methods should continue to work as expected as there is an alternative method + # to cast a model to a dictionary, model.dict(), which is used internally + # by pydantic. + def __iter__(self) -> Iterator[_T]: # type: ignore + for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + def iter_pages(self: SyncPageT) -> Iterator[SyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = page.get_next_page() + else: + return + + def get_next_page(self: SyncPageT) -> SyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return self._client._request_api_list(self._model, page=self.__class__, options=options) + + +class AsyncPaginator(Generic[_T, AsyncPageT]): + def __init__( + self, + client: AsyncAPIClient, + options: FinalRequestOptions, + page_cls: Type[AsyncPageT], + model: Type[_T], + ) -> None: + self._model = model + self._client = client + self._options = options + self._page_cls = page_cls + + def __await__(self) -> Generator[Any, None, AsyncPageT]: + return self._get_page().__await__() + + async def _get_page(self) -> AsyncPageT: + def _parser(resp: AsyncPageT) -> AsyncPageT: + resp._set_private_attributes( + model=self._model, + options=self._options, + client=self._client, + ) + return resp + + self._options.post_parser = _parser + + return await self._client.request(self._page_cls, self._options) + + async def __aiter__(self) -> AsyncIterator[_T]: + # https://github.com/microsoft/pyright/issues/3464 + page = cast( + AsyncPageT, + await self, # type: ignore + ) + async for item in page: + yield item + + +class BaseAsyncPage(BasePage[_T], Generic[_T]): + _client: AsyncAPIClient = pydantic.PrivateAttr() + + def _set_private_attributes( + self, + model: Type[_T], + client: AsyncAPIClient, + options: FinalRequestOptions, + ) -> None: + if (not PYDANTIC_V1) and getattr(self, "__pydantic_private__", None) is None: + self.__pydantic_private__ = {} + + self._model = model + self._client = client + self._options = options + + async def __aiter__(self) -> AsyncIterator[_T]: + async for page in self.iter_pages(): + for item in page._get_page_items(): + yield item + + async def iter_pages(self: AsyncPageT) -> AsyncIterator[AsyncPageT]: + page = self + while True: + yield page + if page.has_next_page(): + page = await page.get_next_page() + else: + return + + async def get_next_page(self: AsyncPageT) -> AsyncPageT: + info = self.next_page_info() + if not info: + raise RuntimeError( + "No next page expected; please check `.has_next_page()` before calling `.get_next_page()`." + ) + + options = self._info_to_options(info) + return await self._client._request_api_list(self._model, page=self.__class__, options=options) + + +_HttpxClientT = TypeVar("_HttpxClientT", bound=Union[httpx.Client, httpx.AsyncClient]) +_DefaultStreamT = TypeVar("_DefaultStreamT", bound=Union[Stream[Any], AsyncStream[Any]]) + + +class BaseClient(Generic[_HttpxClientT, _DefaultStreamT]): + _client: _HttpxClientT + _version: str + _base_url: URL + max_retries: int + timeout: Union[float, Timeout, None] + _strict_response_validation: bool + _idempotency_header: str | None + _default_stream_cls: type[_DefaultStreamT] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None = DEFAULT_TIMEOUT, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + self._version = version + self._base_url = self._enforce_trailing_slash(URL(base_url)) + self.max_retries = max_retries + self.timeout = timeout + self._custom_headers = custom_headers or {} + self._custom_query = custom_query or {} + self._strict_response_validation = _strict_response_validation + self._idempotency_header = None + self._platform: Platform | None = None + + if max_retries is None: # pyright: ignore[reportUnnecessaryComparison] + raise TypeError( + "max_retries cannot be None. If you want to disable retries, pass `0`; if you want unlimited retries, pass `math.inf` or a very high number; if you want the default behavior, pass `vitable_connect.DEFAULT_MAX_RETRIES`" + ) + + def _enforce_trailing_slash(self, url: URL) -> URL: + if url.raw_path.endswith(b"/"): + return url + return url.copy_with(raw_path=url.raw_path + b"/") + + def _make_status_error_from_response( + self, + response: httpx.Response, + ) -> APIStatusError: + if response.is_closed and not response.is_stream_consumed: + # We can't read the response body as it has been closed + # before it was read. This can happen if an event hook + # raises a status error. + body = None + err_msg = f"Error code: {response.status_code}" + else: + err_text = response.text.strip() + body = err_text + + try: + body = json.loads(err_text) + err_msg = f"Error code: {response.status_code} - {body}" + except Exception: + err_msg = err_text or f"Error code: {response.status_code}" + + return self._make_status_error(err_msg, body=body, response=response) + + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> _exceptions.APIStatusError: + raise NotImplementedError() + + def _auth_headers( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _auth_query( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> dict[str, str]: + return {} + + def _custom_auth( + self, + security: SecurityOptions, # noqa: ARG002 + ) -> httpx.Auth | None: + return None + + def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: + custom_headers = options.headers or {} + headers_dict = _merge_mappings({**self._auth_headers(options.security), **self.default_headers}, custom_headers) + self._validate_headers(headers_dict, custom_headers) + + # headers are case-insensitive while dictionaries are not. + headers = httpx.Headers(headers_dict) + + idempotency_header = self._idempotency_header + if idempotency_header and options.idempotency_key and idempotency_header not in headers: + headers[idempotency_header] = options.idempotency_key + + # Don't set these headers if they were already set or removed by the caller. We check + # `custom_headers`, which can contain `Omit()`, instead of `headers` to account for the removal case. + lower_custom_headers = [header.lower() for header in custom_headers] + if "x-stainless-retry-count" not in lower_custom_headers: + headers["x-stainless-retry-count"] = str(retries_taken) + if "x-stainless-read-timeout" not in lower_custom_headers: + timeout = self.timeout if isinstance(options.timeout, NotGiven) else options.timeout + if isinstance(timeout, Timeout): + timeout = timeout.read + if timeout is not None: + headers["x-stainless-read-timeout"] = str(timeout) + + return headers + + def _prepare_url(self, url: str) -> URL: + """ + Merge a URL argument together with any 'base_url' on the client, + to create the URL used for the outgoing request. + """ + # Copied from httpx's `_merge_url` method. + merge_url = URL(url) + if merge_url.is_relative_url: + merge_raw_path = self.base_url.raw_path + merge_url.raw_path.lstrip(b"/") + return self.base_url.copy_with(raw_path=merge_raw_path) + + return merge_url + + def _make_sse_decoder(self) -> SSEDecoder | SSEBytesDecoder: + return SSEDecoder() + + def _build_request( + self, + options: FinalRequestOptions, + *, + retries_taken: int = 0, + ) -> httpx.Request: + if log.isEnabledFor(logging.DEBUG): + log.debug( + "Request options: %s", + model_dump( + options, + exclude_unset=True, + # Pydantic v1 can't dump every type we support in content, so we exclude it for now. + exclude={ + "content", + } + if PYDANTIC_V1 + else {}, + ), + ) + kwargs: dict[str, Any] = {} + + json_data = options.json_data + if options.extra_json is not None: + if json_data is None: + json_data = cast(Body, options.extra_json) + elif is_mapping(json_data): + json_data = _merge_mappings(json_data, options.extra_json) + else: + raise RuntimeError(f"Unexpected JSON data type, {type(json_data)}, cannot merge with `extra_body`") + + headers = self._build_headers(options, retries_taken=retries_taken) + params = _merge_mappings({**self._auth_query(options.security), **self.default_query}, options.params) + content_type = headers.get("Content-Type") + files = options.files + + # If the given Content-Type header is multipart/form-data then it + # has to be removed so that httpx can generate the header with + # additional information for us as it has to be in this form + # for the server to be able to correctly parse the request: + # multipart/form-data; boundary=---abc-- + if content_type is not None and content_type.startswith("multipart/form-data"): + if "boundary" not in content_type: + # only remove the header if the boundary hasn't been explicitly set + # as the caller doesn't want httpx to come up with their own boundary + headers.pop("Content-Type") + + # As we are now sending multipart/form-data instead of application/json + # we need to tell httpx to use it, https://www.python-httpx.org/advanced/clients/#multipart-file-encoding + if json_data: + if not is_dict(json_data): + raise TypeError( + f"Expected query input to be a dictionary for multipart requests but got {type(json_data)} instead." + ) + kwargs["data"] = self._serialize_multipartform(json_data) + + # httpx determines whether or not to send a "multipart/form-data" + # request based on the truthiness of the "files" argument. + # This gets around that issue by generating a dict value that + # evaluates to true. + # + # https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186 + if not files: + files = cast(HttpxRequestFiles, ForceMultipartDict()) + + prepared_url = self._prepare_url(options.url) + # preserve hard-coded query params from the url + if params and prepared_url.query: + params = {**dict(prepared_url.params.items()), **params} + prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0]) + if "_" in prepared_url.host: + # work around https://github.com/encode/httpx/discussions/2880 + kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")} + + is_body_allowed = options.method.lower() != "get" + + if is_body_allowed: + if options.content is not None and json_data is not None: + raise TypeError("Passing both `content` and `json_data` is not supported") + if options.content is not None and files is not None: + raise TypeError("Passing both `content` and `files` is not supported") + if options.content is not None: + kwargs["content"] = options.content + elif isinstance(json_data, bytes): + kwargs["content"] = json_data + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None + kwargs["files"] = files + else: + headers.pop("Content-Type", None) + kwargs.pop("data", None) + + # TODO: report this error to httpx + return self._client.build_request( # pyright: ignore[reportUnknownMemberType] + headers=headers, + timeout=self.timeout if isinstance(options.timeout, NotGiven) else options.timeout, + method=options.method, + url=prepared_url, + # the `Query` type that we use is incompatible with qs' + # `Params` type as it needs to be typed as `Mapping[str, object]` + # so that passing a `TypedDict` doesn't cause an error. + # https://github.com/microsoft/pyright/issues/3526#event-6715453066 + params=self.qs.stringify(cast(Mapping[str, Any], params)) if params else None, + **kwargs, + ) + + def _serialize_multipartform(self, data: Mapping[object, object]) -> dict[str, object]: + items = self.qs.stringify_items( + # TODO: type ignore is required as stringify_items is well typed but we can't be + # well typed without heavy validation. + data, # type: ignore + array_format="brackets", + ) + serialized: dict[str, object] = {} + for key, value in items: + existing = serialized.get(key) + + if not existing: + serialized[key] = value + continue + + # If a value has already been set for this key then that + # means we're sending data like `array[]=[1, 2, 3]` and we + # need to tell httpx that we want to send multiple values with + # the same key which is done by using a list or a tuple. + # + # Note: 2d arrays should never result in the same key at both + # levels so it's safe to assume that if the value is a list, + # it was because we changed it to be a list. + if is_list(existing): + existing.append(value) + else: + serialized[key] = [existing, value] + + return serialized + + def _maybe_override_cast_to(self, cast_to: type[ResponseT], options: FinalRequestOptions) -> type[ResponseT]: + if not is_given(options.headers): + return cast_to + + # make a copy of the headers so we don't mutate user-input + headers = dict(options.headers) + + # we internally support defining a temporary header to override the + # default `cast_to` type for use with `.with_raw_response` and `.with_streaming_response` + # see _response.py for implementation details + override_cast_to = headers.pop(OVERRIDE_CAST_TO_HEADER, not_given) + if is_given(override_cast_to): + options.headers = headers + return cast(Type[ResponseT], override_cast_to) + + return cast_to + + def _should_stream_response_body(self, request: httpx.Request) -> bool: + return request.headers.get(RAW_RESPONSE_HEADER) == "stream" # type: ignore[no-any-return] + + def _process_response_data( + self, + *, + data: object, + cast_to: type[ResponseT], + response: httpx.Response, + ) -> ResponseT: + if data is None: + return cast(ResponseT, None) + + if cast_to is object: + return cast(ResponseT, data) + + try: + if inspect.isclass(cast_to) and issubclass(cast_to, ModelBuilderProtocol): + return cast(ResponseT, cast_to.build(response=response, data=data)) + + if self._strict_response_validation: + return cast(ResponseT, validate_type(type_=cast_to, value=data)) + + return cast(ResponseT, construct_type(type_=cast_to, value=data)) + except pydantic.ValidationError as err: + raise APIResponseValidationError(response=response, body=data) from err + + @property + def qs(self) -> Querystring: + return Querystring() + + @property + def custom_auth(self) -> httpx.Auth | None: + return None + + @property + def auth_headers(self) -> dict[str, str]: + return {} + + @property + def default_headers(self) -> dict[str, str | Omit]: + return { + "Accept": "application/json", + "Content-Type": "application/json", + "User-Agent": self.user_agent, + **self.platform_headers(), + **self._custom_headers, + } + + @property + def default_query(self) -> dict[str, object]: + return { + **self._custom_query, + } + + def _validate_headers( + self, + headers: Headers, # noqa: ARG002 + custom_headers: Headers, # noqa: ARG002 + ) -> None: + """Validate the given default headers and custom headers. + + Does nothing by default. + """ + return + + @property + def user_agent(self) -> str: + return f"{self.__class__.__name__}/Python {self._version}" + + @property + def base_url(self) -> URL: + return self._base_url + + @base_url.setter + def base_url(self, url: URL | str) -> None: + self._base_url = self._enforce_trailing_slash(url if isinstance(url, URL) else URL(url)) + + def platform_headers(self) -> Dict[str, str]: + # the actual implementation is in a separate `lru_cache` decorated + # function because adding `lru_cache` to methods will leak memory + # https://github.com/python/cpython/issues/88476 + return platform_headers(self._version, platform=self._platform) + + def _parse_retry_after_header(self, response_headers: Optional[httpx.Headers] = None) -> float | None: + """Returns a float of the number of seconds (not milliseconds) to wait after retrying, or None if unspecified. + + About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax + """ + if response_headers is None: + return None + + # First, try the non-standard `retry-after-ms` header for milliseconds, + # which is more precise than integer-seconds `retry-after` + try: + retry_ms_header = response_headers.get("retry-after-ms", None) + return float(retry_ms_header) / 1000 + except (TypeError, ValueError): + pass + + # Next, try parsing `retry-after` header as seconds (allowing nonstandard floats). + retry_header = response_headers.get("retry-after") + try: + # note: the spec indicates that this should only ever be an integer + # but if someone sends a float there's no reason for us to not respect it + return float(retry_header) + except (TypeError, ValueError): + pass + + # Last, try parsing `retry-after` as a date. + retry_date_tuple = email.utils.parsedate_tz(retry_header) + if retry_date_tuple is None: + return None + + retry_date = email.utils.mktime_tz(retry_date_tuple) + return float(retry_date - time.time()) + + def _calculate_retry_timeout( + self, + remaining_retries: int, + options: FinalRequestOptions, + response_headers: Optional[httpx.Headers] = None, + ) -> float: + max_retries = options.get_max_retries(self.max_retries) + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = self._parse_retry_after_header(response_headers) + if retry_after is not None and 0 < retry_after <= 60: + return retry_after + + # Also cap retry count to 1000 to avoid any potential overflows with `pow` + nb_retries = min(max_retries - remaining_retries, 1000) + + # Apply exponential backoff, but not more than the max. + sleep_seconds = min(INITIAL_RETRY_DELAY * pow(2.0, nb_retries), MAX_RETRY_DELAY) + + # Apply some jitter, plus-or-minus half a second. + jitter = 1 - 0.25 * random() + timeout = sleep_seconds * jitter + return timeout if timeout >= 0 else 0 + + def _should_retry(self, response: httpx.Response) -> bool: + # Note: this is not a standard header + should_retry_header = response.headers.get("x-should-retry") + + # If the server explicitly says whether or not to retry, obey. + if should_retry_header == "true": + log.debug("Retrying as header `x-should-retry` is set to `true`") + return True + if should_retry_header == "false": + log.debug("Not retrying as header `x-should-retry` is set to `false`") + return False + + # Retry on request timeouts. + if response.status_code == 408: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on lock timeouts. + if response.status_code == 409: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry on rate limits. + if response.status_code == 429: + log.debug("Retrying due to status code %i", response.status_code) + return True + + # Retry internal errors. + if response.status_code >= 500: + log.debug("Retrying due to status code %i", response.status_code) + return True + + log.debug("Not retrying") + return False + + def _idempotency_key(self) -> str: + return f"stainless-python-retry-{uuid.uuid4()}" + + +class _DefaultHttpxClient(httpx.Client): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultHttpxClient = httpx.Client + """An alias to `httpx.Client` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.Client` will result in httpx's defaults being used, not ours. + """ +else: + DefaultHttpxClient = _DefaultHttpxClient + + +class SyncHttpxClientWrapper(DefaultHttpxClient): + def __del__(self) -> None: + if self.is_closed: + return + + try: + self.close() + except Exception: + pass + + +class SyncAPIClient(BaseClient[httpx.Client, Stream[Any]]): + _client: httpx.Client + _default_stream_cls: type[Stream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.Client | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + _strict_response_validation: bool, + ) -> None: + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.Client` but got {type(http_client)}" + ) + + super().__init__( + version=version, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + base_url=base_url, + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or SyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + # If an error is thrown while constructing a client, self._client + # may not be present + if hasattr(self, "_client"): + self._client.close() + + def __enter__(self: _T) -> _T: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: Type[_StreamT], + ) -> _StreamT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: Type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + cast_to = self._maybe_override_cast_to(cast_to, options) + + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + if input_options.idempotency_key is None and input_options.method.lower() != "get": + # ensure the idempotency key is reused between requests + input_options.idempotency_key = self._idempotency_key() + + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) + + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = self._prepare_options(options) + + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + custom_auth = self._custom_auth(options.security) + if custom_auth is not None: + kwargs["auth"] = custom_auth + + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + response = None + try: + response = self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + err.response.close() + self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + err.response.read() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + break + + assert response is not None, "could not resolve response (should never happen)" + return self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken + if remaining_retries == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining_retries) + + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + time.sleep(timeout) + + def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): + if not issubclass(origin, APIResponse): + raise TypeError(f"API Response types must subclass {APIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + ResponseT, + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = APIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return api_response.parse() + + def _request_api_list( + self, + model: Type[object], + page: Type[SyncPageT], + options: FinalRequestOptions, + ) -> SyncPageT: + def _parser(resp: SyncPageT) -> SyncPageT: + resp._set_private_attributes( + client=self, + model=model, + options=options, + ) + return resp + + options.post_parser = _parser + + return self.request(page, options, stream=False) + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + # cast is required because mypy complains about returning Any even though + # it understands the type variables + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: Literal[True], + stream_cls: type[_StreamT], + ) -> _StreamT: ... + + @overload + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: ... + + def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | None = None, + options: RequestOptions = {}, + files: RequestFiles | None = None, + stream: bool = False, + stream_cls: type[_StreamT] | None = None, + ) -> ResponseT | _StreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, content=content, files=to_httpx_files(files), **options + ) + return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls)) + + def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="patch", url=path, json_data=body, content=content, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, content=content, files=to_httpx_files(files), **options + ) + return self.request(cast_to, opts) + + def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: BinaryTypes | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) + return self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[object], + page: Type[SyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> SyncPageT: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +class _DefaultAsyncHttpxClient(httpx.AsyncClient): + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + super().__init__(**kwargs) + + +try: + import httpx_aiohttp +except ImportError: + + class _DefaultAioHttpClient(httpx.AsyncClient): + def __init__(self, **_kwargs: Any) -> None: + raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra") +else: + + class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore + def __init__(self, **kwargs: Any) -> None: + kwargs.setdefault("timeout", DEFAULT_TIMEOUT) + kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS) + kwargs.setdefault("follow_redirects", True) + + super().__init__(**kwargs) + + +if TYPE_CHECKING: + DefaultAsyncHttpxClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that provides the same defaults that this SDK + uses internally. + + This is useful because overriding the `http_client` with your own instance of + `httpx.AsyncClient` will result in httpx's defaults being used, not ours. + """ + + DefaultAioHttpClient = httpx.AsyncClient + """An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`.""" +else: + DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient + DefaultAioHttpClient = _DefaultAioHttpClient + + +class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient): + def __del__(self) -> None: + if self.is_closed: + return + + try: + # TODO(someday): support non asyncio runtimes here + asyncio.get_running_loop().create_task(self.aclose()) + except Exception: + pass + + +class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]): + _client: httpx.AsyncClient + _default_stream_cls: type[AsyncStream[Any]] | None = None + + def __init__( + self, + *, + version: str, + base_url: str | URL, + _strict_response_validation: bool, + max_retries: int = DEFAULT_MAX_RETRIES, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.AsyncClient | None = None, + custom_headers: Mapping[str, str] | None = None, + custom_query: Mapping[str, object] | None = None, + ) -> None: + if not is_given(timeout): + # if the user passed in a custom http client with a non-default + # timeout set then we use that timeout. + # + # note: there is an edge case here where the user passes in a client + # where they've explicitly set the timeout to match the default timeout + # as this check is structural, meaning that we'll think they didn't + # pass in a timeout and will ignore it + if http_client and http_client.timeout != HTTPX_DEFAULT_TIMEOUT: + timeout = http_client.timeout + else: + timeout = DEFAULT_TIMEOUT + + if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + f"Invalid `http_client` argument; Expected an instance of `httpx.AsyncClient` but got {type(http_client)}" + ) + + super().__init__( + version=version, + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + max_retries=max_retries, + custom_query=custom_query, + custom_headers=custom_headers, + _strict_response_validation=_strict_response_validation, + ) + self._client = http_client or AsyncHttpxClientWrapper( + base_url=base_url, + # cast to a valid type because mypy doesn't understand our type narrowing + timeout=cast(Timeout, timeout), + ) + + def is_closed(self) -> bool: + return self._client.is_closed + + async def close(self) -> None: + """Close the underlying HTTPX client. + + The client will *not* be usable after this. + """ + await self._client.aclose() + + async def __aenter__(self: _T) -> _T: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def _prepare_options( + self, + options: FinalRequestOptions, # noqa: ARG002 + ) -> FinalRequestOptions: + """Hook for mutating the given options""" + return options + + async def _prepare_request( + self, + request: httpx.Request, # noqa: ARG002 + ) -> None: + """This method is used as a callback for mutating the `Request` object + after it has been constructed. + This is useful for cases where you want to add certain headers based off of + the request properties, e.g. `url`, `method` etc. + """ + return None + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def request( + self, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + *, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + if self._platform is None: + # `get_platform` can make blocking IO calls so we + # execute it earlier while we are in an async context + self._platform = await asyncify(get_platform)() + + cast_to = self._maybe_override_cast_to(cast_to, options) + + # create a copy of the options we were given so that if the + # options are mutated later & we then retry, the retries are + # given the original options + input_options = model_copy(options) + if input_options.idempotency_key is None and input_options.method.lower() != "get": + # ensure the idempotency key is reused between requests + input_options.idempotency_key = self._idempotency_key() + + response: httpx.Response | None = None + max_retries = input_options.get_max_retries(self.max_retries) + + retries_taken = 0 + for retries_taken in range(max_retries + 1): + options = model_copy(input_options) + options = await self._prepare_options(options) + + remaining_retries = max_retries - retries_taken + request = self._build_request(options, retries_taken=retries_taken) + await self._prepare_request(request) + + kwargs: HttpxSendArgs = {} + if self.custom_auth is not None: + kwargs["auth"] = self.custom_auth + + if options.follow_redirects is not None: + kwargs["follow_redirects"] = options.follow_redirects + + log.debug("Sending HTTP Request: %s %s", request.method, request.url) + + response = None + try: + response = await self._client.send( + request, + stream=stream or self._should_stream_response_body(request=request), + **kwargs, + ) + except httpx.TimeoutException as err: + log.debug("Encountered httpx.TimeoutException", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising timeout error") + raise APITimeoutError(request=request) from err + except Exception as err: + log.debug("Encountered Exception", exc_info=True) + + if remaining_retries > 0: + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=None, + ) + continue + + log.debug("Raising connection error") + raise APIConnectionError(request=request) from err + + log.debug( + 'HTTP Response: %s %s "%i %s" %s', + request.method, + request.url, + response.status_code, + response.reason_phrase, + response.headers, + ) + + try: + response.raise_for_status() + except httpx.HTTPStatusError as err: # thrown on 4xx and 5xx status code + log.debug("Encountered httpx.HTTPStatusError", exc_info=True) + + if remaining_retries > 0 and self._should_retry(err.response): + await err.response.aclose() + await self._sleep_for_retry( + retries_taken=retries_taken, + max_retries=max_retries, + options=input_options, + response=response, + ) + continue + + # If the response is streamed then we need to explicitly read the response + # to completion before attempting to access the response text. + if not err.response.is_closed: + await err.response.aread() + + log.debug("Re-raising status error") + raise self._make_status_error_from_response(err.response) from None + + break + + assert response is not None, "could not resolve response (should never happen)" + return await self._process_response( + cast_to=cast_to, + options=options, + response=response, + stream=stream, + stream_cls=stream_cls, + retries_taken=retries_taken, + ) + + async def _sleep_for_retry( + self, *, retries_taken: int, max_retries: int, options: FinalRequestOptions, response: httpx.Response | None + ) -> None: + remaining_retries = max_retries - retries_taken + if remaining_retries == 1: + log.debug("1 retry left") + else: + log.debug("%i retries left", remaining_retries) + + timeout = self._calculate_retry_timeout(remaining_retries, options, response.headers if response else None) + log.info("Retrying request to %s in %f seconds", options.url, timeout) + + await anyio.sleep(timeout) + + async def _process_response( + self, + *, + cast_to: Type[ResponseT], + options: FinalRequestOptions, + response: httpx.Response, + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + retries_taken: int = 0, + ) -> ResponseT: + origin = get_origin(cast_to) or cast_to + + if ( + inspect.isclass(origin) + and issubclass(origin, BaseAPIResponse) + # we only want to actually return the custom BaseAPIResponse class if we're + # returning the raw response, or if we're not streaming SSE, as if we're streaming + # SSE then `cast_to` doesn't actively reflect the type we need to parse into + and (not stream or bool(response.request.headers.get(RAW_RESPONSE_HEADER))) + ): + if not issubclass(origin, AsyncAPIResponse): + raise TypeError(f"API Response types must subclass {AsyncAPIResponse}; Received {origin}") + + response_cls = cast("type[BaseAPIResponse[Any]]", cast_to) + return cast( + "ResponseT", + response_cls( + raw=response, + client=self, + cast_to=extract_response_type(response_cls), + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ), + ) + + if cast_to == httpx.Response: + return cast(ResponseT, response) + + api_response = AsyncAPIResponse( + raw=response, + client=self, + cast_to=cast("type[ResponseT]", cast_to), # pyright: ignore[reportUnnecessaryCast] + stream=stream, + stream_cls=stream_cls, + options=options, + retries_taken=retries_taken, + ) + if bool(response.request.headers.get(RAW_RESPONSE_HEADER)): + return cast(ResponseT, api_response) + + return await api_response.parse() + + def _request_api_list( + self, + model: Type[_T], + page: Type[AsyncPageT], + options: FinalRequestOptions, + ) -> AsyncPaginator[_T, AsyncPageT]: + return AsyncPaginator(client=self, options=options, page_cls=page, model=model) + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def get( + self, + path: str, + *, + cast_to: Type[ResponseT], + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + opts = FinalRequestOptions.construct(method="get", url=path, **options) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[False] = False, + ) -> ResponseT: ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: Literal[True], + stream_cls: type[_AsyncStreamT], + ) -> _AsyncStreamT: ... + + @overload + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: ... + + async def post( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + stream: bool = False, + stream_cls: type[_AsyncStreamT] | None = None, + ) -> ResponseT | _AsyncStreamT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="post", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) + + async def patch( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="patch", + url=path, + json_data=body, + content=content, + files=await async_to_httpx_files(files), + **options, + ) + return await self.request(cast_to, opts) + + async def put( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | None = None, + files: RequestFiles | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if files is not None and content is not None: + raise TypeError("Passing both `files` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct( + method="put", url=path, json_data=body, content=content, files=await async_to_httpx_files(files), **options + ) + return await self.request(cast_to, opts) + + async def delete( + self, + path: str, + *, + cast_to: Type[ResponseT], + body: Body | None = None, + content: AsyncBinaryTypes | None = None, + options: RequestOptions = {}, + ) -> ResponseT: + if body is not None and content is not None: + raise TypeError("Passing both `body` and `content` is not supported") + if isinstance(body, bytes): + warnings.warn( + "Passing raw bytes as `body` is deprecated and will be removed in a future version. " + "Please pass raw bytes via the `content` parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + opts = FinalRequestOptions.construct(method="delete", url=path, json_data=body, content=content, **options) + return await self.request(cast_to, opts) + + def get_api_list( + self, + path: str, + *, + model: Type[_T], + page: Type[AsyncPageT], + body: Body | None = None, + options: RequestOptions = {}, + method: str = "get", + ) -> AsyncPaginator[_T, AsyncPageT]: + opts = FinalRequestOptions.construct(method=method, url=path, json_data=body, **options) + return self._request_api_list(model, page, opts) + + +def make_request_options( + *, + query: Query | None = None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + idempotency_key: str | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + post_parser: PostParser | NotGiven = not_given, + security: SecurityOptions | None = None, +) -> RequestOptions: + """Create a dict of type RequestOptions without keys of NotGiven values.""" + options: RequestOptions = {} + if extra_headers is not None: + options["headers"] = extra_headers + + if extra_body is not None: + options["extra_json"] = cast(AnyMapping, extra_body) + + if query is not None: + options["params"] = query + + if extra_query is not None: + options["params"] = {**options.get("params", {}), **extra_query} + + if not isinstance(timeout, NotGiven): + options["timeout"] = timeout + + if idempotency_key is not None: + options["idempotency_key"] = idempotency_key + + if is_given(post_parser): + # internal + options["post_parser"] = post_parser # type: ignore + + if security is not None: + options["security"] = security + + return options + + +class ForceMultipartDict(Dict[str, None]): + def __bool__(self) -> bool: + return True + + +class OtherPlatform: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"Other:{self.name}" + + +Platform = Union[ + OtherPlatform, + Literal[ + "MacOS", + "Linux", + "Windows", + "FreeBSD", + "OpenBSD", + "iOS", + "Android", + "Unknown", + ], +] + + +def get_platform() -> Platform: + try: + system = platform.system().lower() + platform_name = platform.platform().lower() + except Exception: + return "Unknown" + + if "iphone" in platform_name or "ipad" in platform_name: + # Tested using Python3IDE on an iPhone 11 and Pythonista on an iPad 7 + # system is Darwin and platform_name is a string like: + # - Darwin-21.6.0-iPhone12,1-64bit + # - Darwin-21.6.0-iPad7,11-64bit + return "iOS" + + if system == "darwin": + return "MacOS" + + if system == "windows": + return "Windows" + + if "android" in platform_name: + # Tested using Pydroid 3 + # system is Linux and platform_name is a string like 'Linux-5.10.81-android12-9-00001-geba40aecb3b7-ab8534902-aarch64-with-libc' + return "Android" + + if system == "linux": + # https://distro.readthedocs.io/en/latest/#distro.id + distro_id = distro.id() + if distro_id == "freebsd": + return "FreeBSD" + + if distro_id == "openbsd": + return "OpenBSD" + + return "Linux" + + if platform_name: + return OtherPlatform(platform_name) + + return "Unknown" + + +@lru_cache(maxsize=None) +def platform_headers(version: str, *, platform: Platform | None) -> Dict[str, str]: + return { + "X-Stainless-Lang": "python", + "X-Stainless-Package-Version": version, + "X-Stainless-OS": str(platform or get_platform()), + "X-Stainless-Arch": str(get_architecture()), + "X-Stainless-Runtime": get_python_runtime(), + "X-Stainless-Runtime-Version": get_python_version(), + } + + +class OtherArch: + def __init__(self, name: str) -> None: + self.name = name + + @override + def __str__(self) -> str: + return f"other:{self.name}" + + +Arch = Union[OtherArch, Literal["x32", "x64", "arm", "arm64", "unknown"]] + + +def get_python_runtime() -> str: + try: + return platform.python_implementation() + except Exception: + return "unknown" + + +def get_python_version() -> str: + try: + return platform.python_version() + except Exception: + return "unknown" + + +def get_architecture() -> Arch: + try: + machine = platform.machine().lower() + except Exception: + return "unknown" + + if machine in ("arm64", "aarch64"): + return "arm64" + + # TODO: untested + if machine == "arm": + return "arm" + + if machine == "x86_64": + return "x64" + + # TODO: untested + if sys.maxsize <= 2**32: + return "x32" + + if machine: + return OtherArch(machine) + + return "unknown" + + +def _merge_mappings( + obj1: Mapping[_T_co, Union[_T, Omit]], + obj2: Mapping[_T_co, Union[_T, Omit]], +) -> Dict[_T_co, _T]: + """Merge two mappings of the same type, removing any values that are instances of `Omit`. + + In cases with duplicate keys the second mapping takes precedence. + """ + merged = {**obj1, **obj2} + return {key: value for key, value in merged.items() if not isinstance(value, Omit)} diff --git a/src/vitable_connect/_client.py b/src/vitable_connect/_client.py new file mode 100644 index 0000000..0f4702f --- /dev/null +++ b/src/vitable_connect/_client.py @@ -0,0 +1,870 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING, Any, Dict, Mapping, cast +from typing_extensions import Self, Literal, override + +import httpx + +from . import _exceptions +from ._qs import Querystring +from ._types import ( + Omit, + Timeout, + NotGiven, + Transport, + ProxiesTypes, + RequestOptions, + not_given, +) +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) +from ._compat import cached_property +from ._models import SecurityOptions +from ._version import __version__ +from ._streaming import Stream as Stream, AsyncStream as AsyncStream +from ._exceptions import APIStatusError, VitableConnectError +from ._base_client import ( + DEFAULT_MAX_RETRIES, + SyncAPIClient, + AsyncAPIClient, +) + +if TYPE_CHECKING: + from .resources import ( + auth, + plans, + groups, + members, + employees, + employers, + enrollments, + organizations, + webhook_events, + ) + from .resources.auth import AuthResource, AsyncAuthResource + from .resources.plans import PlansResource, AsyncPlansResource + from .resources.members import MembersResource, AsyncMembersResource + from .resources.employees import EmployeesResource, AsyncEmployeesResource + from .resources.employers import EmployersResource, AsyncEmployersResource + from .resources.enrollments import EnrollmentsResource, AsyncEnrollmentsResource + from .resources.groups.groups import GroupsResource, AsyncGroupsResource + from .resources.organizations import OrganizationsResource, AsyncOrganizationsResource + from .resources.webhook_events import WebhookEventsResource, AsyncWebhookEventsResource + +__all__ = [ + "ENVIRONMENTS", + "Timeout", + "Transport", + "ProxiesTypes", + "RequestOptions", + "VitableConnect", + "AsyncVitableConnect", + "Client", + "AsyncClient", +] + +ENVIRONMENTS: Dict[str, str] = { + "production": "https://api.vitablehealth.com", + "environment_1": "https://api.uat.vitablehealth.com", +} + + +class VitableConnect(SyncAPIClient): + # client options + api_key: str + + _environment: Literal["production", "environment_1"] | NotGiven + + def __init__( + self, + *, + api_key: str | None = None, + environment: Literal["production", "environment_1"] | NotGiven = not_given, + base_url: str | httpx.URL | None | NotGiven = not_given, + timeout: float | Timeout | None | NotGiven = not_given, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. + # We provide a `DefaultHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#client) for more details. + http_client: httpx.Client | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new synchronous VitableConnect client instance. + + This automatically infers the `api_key` argument from the `VITABLE_CONNECT_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("VITABLE_CONNECT_API_KEY") + if api_key is None: + raise VitableConnectError( + "The api_key client option must be set either by passing api_key to the client or by setting the VITABLE_CONNECT_API_KEY environment variable" + ) + self.api_key = api_key + + self._environment = environment + + base_url_env = os.environ.get("VITABLE_CONNECT_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `VITABLE_CONNECT_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + + custom_headers_env = os.environ.get("VITABLE_CONNECT_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + @cached_property + def auth(self) -> AuthResource: + """Issue short-lived access tokens for scoped API access""" + from .resources.auth import AuthResource + + return AuthResource(self) + + @cached_property + def employees(self) -> EmployeesResource: + from .resources.employees import EmployeesResource + + return EmployeesResource(self) + + @cached_property + def employers(self) -> EmployersResource: + from .resources.employers import EmployersResource + + return EmployersResource(self) + + @cached_property + def enrollments(self) -> EnrollmentsResource: + """Manage benefit enrollments and elections for employees""" + from .resources.enrollments import EnrollmentsResource + + return EnrollmentsResource(self) + + @cached_property + def webhook_events(self) -> WebhookEventsResource: + from .resources.webhook_events import WebhookEventsResource + + return WebhookEventsResource(self) + + @cached_property + def groups(self) -> GroupsResource: + from .resources.groups import GroupsResource + + return GroupsResource(self) + + @cached_property + def members(self) -> MembersResource: + """Browse the members covered across your book and read a member's profile""" + from .resources.members import MembersResource + + return MembersResource(self) + + @cached_property + def organizations(self) -> OrganizationsResource: + from .resources.organizations import OrganizationsResource + + return OrganizationsResource(self) + + @cached_property + def plans(self) -> PlansResource: + from .resources.plans import PlansResource + + return PlansResource(self) + + @cached_property + def with_raw_response(self) -> VitableConnectWithRawResponse: + return VitableConnectWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> VitableConnectWithStreamedResponse: + return VitableConnectWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @override + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + headers: dict[str, str] = {} + if security.get("api_key_auth", False): + for key, value in self._api_key_auth.items(): + headers.setdefault(key, value) + return headers + + @property + def _api_key_auth(self) -> dict[str, str]: + api_key = self.api_key + return {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": "false", + **self._custom_headers, + } + + def copy( + self, + *, + api_key: str | None = None, + environment: Literal["production", "environment_1"] | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.Client | None = None, + max_retries: int | NotGiven = not_given, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + base_url=base_url or self.base_url, + environment=environment or self._environment, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=body) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=body) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=body) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=body) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=body) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=body) + return APIStatusError(err_msg, response=response, body=body) + + +class AsyncVitableConnect(AsyncAPIClient): + # client options + api_key: str + + _environment: Literal["production", "environment_1"] | NotGiven + + def __init__( + self, + *, + api_key: str | None = None, + environment: Literal["production", "environment_1"] | NotGiven = not_given, + base_url: str | httpx.URL | None | NotGiven = not_given, + timeout: float | Timeout | None | NotGiven = not_given, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + # Configure a custom httpx client. + # We provide a `DefaultAsyncHttpxClient` class that you can pass to retain the default values we use for `limits`, `timeout` & `follow_redirects`. + # See the [httpx documentation](https://www.python-httpx.org/api/#asyncclient) for more details. + http_client: httpx.AsyncClient | None = None, + # Enable or disable schema validation for data returned by the API. + # When enabled an error APIResponseValidationError is raised + # if the API responds with invalid data for the expected schema. + # + # This parameter may be removed or changed in the future. + # If you rely on this feature, please open a GitHub issue + # outlining your use-case to help us decide if it should be + # part of our public interface in the future. + _strict_response_validation: bool = False, + ) -> None: + """Construct a new async AsyncVitableConnect client instance. + + This automatically infers the `api_key` argument from the `VITABLE_CONNECT_API_KEY` environment variable if it is not provided. + """ + if api_key is None: + api_key = os.environ.get("VITABLE_CONNECT_API_KEY") + if api_key is None: + raise VitableConnectError( + "The api_key client option must be set either by passing api_key to the client or by setting the VITABLE_CONNECT_API_KEY environment variable" + ) + self.api_key = api_key + + self._environment = environment + + base_url_env = os.environ.get("VITABLE_CONNECT_BASE_URL") + if is_given(base_url) and base_url is not None: + # cast required because mypy doesn't understand the type narrowing + base_url = cast("str | httpx.URL", base_url) # pyright: ignore[reportUnnecessaryCast] + elif is_given(environment): + if base_url_env and base_url is not None: + raise ValueError( + "Ambiguous URL; The `VITABLE_CONNECT_BASE_URL` env var and the `environment` argument are given. If you want to use the environment, you must pass base_url=None", + ) + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + elif base_url_env is not None: + base_url = base_url_env + else: + self._environment = environment = "production" + + try: + base_url = ENVIRONMENTS[environment] + except KeyError as exc: + raise ValueError(f"Unknown environment: {environment}") from exc + + custom_headers_env = os.environ.get("VITABLE_CONNECT_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + + super().__init__( + version=__version__, + base_url=base_url, + max_retries=max_retries, + timeout=timeout, + http_client=http_client, + custom_headers=default_headers, + custom_query=default_query, + _strict_response_validation=_strict_response_validation, + ) + + @cached_property + def auth(self) -> AsyncAuthResource: + """Issue short-lived access tokens for scoped API access""" + from .resources.auth import AsyncAuthResource + + return AsyncAuthResource(self) + + @cached_property + def employees(self) -> AsyncEmployeesResource: + from .resources.employees import AsyncEmployeesResource + + return AsyncEmployeesResource(self) + + @cached_property + def employers(self) -> AsyncEmployersResource: + from .resources.employers import AsyncEmployersResource + + return AsyncEmployersResource(self) + + @cached_property + def enrollments(self) -> AsyncEnrollmentsResource: + """Manage benefit enrollments and elections for employees""" + from .resources.enrollments import AsyncEnrollmentsResource + + return AsyncEnrollmentsResource(self) + + @cached_property + def webhook_events(self) -> AsyncWebhookEventsResource: + from .resources.webhook_events import AsyncWebhookEventsResource + + return AsyncWebhookEventsResource(self) + + @cached_property + def groups(self) -> AsyncGroupsResource: + from .resources.groups import AsyncGroupsResource + + return AsyncGroupsResource(self) + + @cached_property + def members(self) -> AsyncMembersResource: + """Browse the members covered across your book and read a member's profile""" + from .resources.members import AsyncMembersResource + + return AsyncMembersResource(self) + + @cached_property + def organizations(self) -> AsyncOrganizationsResource: + from .resources.organizations import AsyncOrganizationsResource + + return AsyncOrganizationsResource(self) + + @cached_property + def plans(self) -> AsyncPlansResource: + from .resources.plans import AsyncPlansResource + + return AsyncPlansResource(self) + + @cached_property + def with_raw_response(self) -> AsyncVitableConnectWithRawResponse: + return AsyncVitableConnectWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncVitableConnectWithStreamedResponse: + return AsyncVitableConnectWithStreamedResponse(self) + + @property + @override + def qs(self) -> Querystring: + return Querystring(array_format="comma") + + @override + def _auth_headers(self, security: SecurityOptions) -> dict[str, str]: + headers: dict[str, str] = {} + if security.get("api_key_auth", False): + for key, value in self._api_key_auth.items(): + headers.setdefault(key, value) + return headers + + @property + def _api_key_auth(self) -> dict[str, str]: + api_key = self.api_key + return {"Authorization": f"Bearer {api_key}"} + + @property + @override + def default_headers(self) -> dict[str, str | Omit]: + return { + **super().default_headers, + "X-Stainless-Async": f"async:{get_async_library()}", + **self._custom_headers, + } + + def copy( + self, + *, + api_key: str | None = None, + environment: Literal["production", "environment_1"] | None = None, + base_url: str | httpx.URL | None = None, + timeout: float | Timeout | None | NotGiven = not_given, + http_client: httpx.AsyncClient | None = None, + max_retries: int | NotGiven = not_given, + default_headers: Mapping[str, str] | None = None, + set_default_headers: Mapping[str, str] | None = None, + default_query: Mapping[str, object] | None = None, + set_default_query: Mapping[str, object] | None = None, + _extra_kwargs: Mapping[str, Any] = {}, + ) -> Self: + """ + Create a new client instance re-using the same options given to the current client with optional overriding. + """ + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + http_client = http_client or self._client + return self.__class__( + api_key=api_key or self.api_key, + base_url=base_url or self.base_url, + environment=environment or self._environment, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, + ) + + # Alias for `copy` for nicer inline usage, e.g. + # client.with_options(timeout=10).foo.create(...) + with_options = copy + + @override + def _make_status_error( + self, + err_msg: str, + *, + body: object, + response: httpx.Response, + ) -> APIStatusError: + if response.status_code == 400: + return _exceptions.BadRequestError(err_msg, response=response, body=body) + + if response.status_code == 401: + return _exceptions.AuthenticationError(err_msg, response=response, body=body) + + if response.status_code == 403: + return _exceptions.PermissionDeniedError(err_msg, response=response, body=body) + + if response.status_code == 404: + return _exceptions.NotFoundError(err_msg, response=response, body=body) + + if response.status_code == 409: + return _exceptions.ConflictError(err_msg, response=response, body=body) + + if response.status_code == 422: + return _exceptions.UnprocessableEntityError(err_msg, response=response, body=body) + + if response.status_code == 429: + return _exceptions.RateLimitError(err_msg, response=response, body=body) + + if response.status_code >= 500: + return _exceptions.InternalServerError(err_msg, response=response, body=body) + return APIStatusError(err_msg, response=response, body=body) + + +class VitableConnectWithRawResponse: + _client: VitableConnect + + def __init__(self, client: VitableConnect) -> None: + self._client = client + + @cached_property + def auth(self) -> auth.AuthResourceWithRawResponse: + """Issue short-lived access tokens for scoped API access""" + from .resources.auth import AuthResourceWithRawResponse + + return AuthResourceWithRawResponse(self._client.auth) + + @cached_property + def employees(self) -> employees.EmployeesResourceWithRawResponse: + from .resources.employees import EmployeesResourceWithRawResponse + + return EmployeesResourceWithRawResponse(self._client.employees) + + @cached_property + def employers(self) -> employers.EmployersResourceWithRawResponse: + from .resources.employers import EmployersResourceWithRawResponse + + return EmployersResourceWithRawResponse(self._client.employers) + + @cached_property + def enrollments(self) -> enrollments.EnrollmentsResourceWithRawResponse: + """Manage benefit enrollments and elections for employees""" + from .resources.enrollments import EnrollmentsResourceWithRawResponse + + return EnrollmentsResourceWithRawResponse(self._client.enrollments) + + @cached_property + def webhook_events(self) -> webhook_events.WebhookEventsResourceWithRawResponse: + from .resources.webhook_events import WebhookEventsResourceWithRawResponse + + return WebhookEventsResourceWithRawResponse(self._client.webhook_events) + + @cached_property + def groups(self) -> groups.GroupsResourceWithRawResponse: + from .resources.groups import GroupsResourceWithRawResponse + + return GroupsResourceWithRawResponse(self._client.groups) + + @cached_property + def members(self) -> members.MembersResourceWithRawResponse: + """Browse the members covered across your book and read a member's profile""" + from .resources.members import MembersResourceWithRawResponse + + return MembersResourceWithRawResponse(self._client.members) + + @cached_property + def organizations(self) -> organizations.OrganizationsResourceWithRawResponse: + from .resources.organizations import OrganizationsResourceWithRawResponse + + return OrganizationsResourceWithRawResponse(self._client.organizations) + + @cached_property + def plans(self) -> plans.PlansResourceWithRawResponse: + from .resources.plans import PlansResourceWithRawResponse + + return PlansResourceWithRawResponse(self._client.plans) + + +class AsyncVitableConnectWithRawResponse: + _client: AsyncVitableConnect + + def __init__(self, client: AsyncVitableConnect) -> None: + self._client = client + + @cached_property + def auth(self) -> auth.AsyncAuthResourceWithRawResponse: + """Issue short-lived access tokens for scoped API access""" + from .resources.auth import AsyncAuthResourceWithRawResponse + + return AsyncAuthResourceWithRawResponse(self._client.auth) + + @cached_property + def employees(self) -> employees.AsyncEmployeesResourceWithRawResponse: + from .resources.employees import AsyncEmployeesResourceWithRawResponse + + return AsyncEmployeesResourceWithRawResponse(self._client.employees) + + @cached_property + def employers(self) -> employers.AsyncEmployersResourceWithRawResponse: + from .resources.employers import AsyncEmployersResourceWithRawResponse + + return AsyncEmployersResourceWithRawResponse(self._client.employers) + + @cached_property + def enrollments(self) -> enrollments.AsyncEnrollmentsResourceWithRawResponse: + """Manage benefit enrollments and elections for employees""" + from .resources.enrollments import AsyncEnrollmentsResourceWithRawResponse + + return AsyncEnrollmentsResourceWithRawResponse(self._client.enrollments) + + @cached_property + def webhook_events(self) -> webhook_events.AsyncWebhookEventsResourceWithRawResponse: + from .resources.webhook_events import AsyncWebhookEventsResourceWithRawResponse + + return AsyncWebhookEventsResourceWithRawResponse(self._client.webhook_events) + + @cached_property + def groups(self) -> groups.AsyncGroupsResourceWithRawResponse: + from .resources.groups import AsyncGroupsResourceWithRawResponse + + return AsyncGroupsResourceWithRawResponse(self._client.groups) + + @cached_property + def members(self) -> members.AsyncMembersResourceWithRawResponse: + """Browse the members covered across your book and read a member's profile""" + from .resources.members import AsyncMembersResourceWithRawResponse + + return AsyncMembersResourceWithRawResponse(self._client.members) + + @cached_property + def organizations(self) -> organizations.AsyncOrganizationsResourceWithRawResponse: + from .resources.organizations import AsyncOrganizationsResourceWithRawResponse + + return AsyncOrganizationsResourceWithRawResponse(self._client.organizations) + + @cached_property + def plans(self) -> plans.AsyncPlansResourceWithRawResponse: + from .resources.plans import AsyncPlansResourceWithRawResponse + + return AsyncPlansResourceWithRawResponse(self._client.plans) + + +class VitableConnectWithStreamedResponse: + _client: VitableConnect + + def __init__(self, client: VitableConnect) -> None: + self._client = client + + @cached_property + def auth(self) -> auth.AuthResourceWithStreamingResponse: + """Issue short-lived access tokens for scoped API access""" + from .resources.auth import AuthResourceWithStreamingResponse + + return AuthResourceWithStreamingResponse(self._client.auth) + + @cached_property + def employees(self) -> employees.EmployeesResourceWithStreamingResponse: + from .resources.employees import EmployeesResourceWithStreamingResponse + + return EmployeesResourceWithStreamingResponse(self._client.employees) + + @cached_property + def employers(self) -> employers.EmployersResourceWithStreamingResponse: + from .resources.employers import EmployersResourceWithStreamingResponse + + return EmployersResourceWithStreamingResponse(self._client.employers) + + @cached_property + def enrollments(self) -> enrollments.EnrollmentsResourceWithStreamingResponse: + """Manage benefit enrollments and elections for employees""" + from .resources.enrollments import EnrollmentsResourceWithStreamingResponse + + return EnrollmentsResourceWithStreamingResponse(self._client.enrollments) + + @cached_property + def webhook_events(self) -> webhook_events.WebhookEventsResourceWithStreamingResponse: + from .resources.webhook_events import WebhookEventsResourceWithStreamingResponse + + return WebhookEventsResourceWithStreamingResponse(self._client.webhook_events) + + @cached_property + def groups(self) -> groups.GroupsResourceWithStreamingResponse: + from .resources.groups import GroupsResourceWithStreamingResponse + + return GroupsResourceWithStreamingResponse(self._client.groups) + + @cached_property + def members(self) -> members.MembersResourceWithStreamingResponse: + """Browse the members covered across your book and read a member's profile""" + from .resources.members import MembersResourceWithStreamingResponse + + return MembersResourceWithStreamingResponse(self._client.members) + + @cached_property + def organizations(self) -> organizations.OrganizationsResourceWithStreamingResponse: + from .resources.organizations import OrganizationsResourceWithStreamingResponse + + return OrganizationsResourceWithStreamingResponse(self._client.organizations) + + @cached_property + def plans(self) -> plans.PlansResourceWithStreamingResponse: + from .resources.plans import PlansResourceWithStreamingResponse + + return PlansResourceWithStreamingResponse(self._client.plans) + + +class AsyncVitableConnectWithStreamedResponse: + _client: AsyncVitableConnect + + def __init__(self, client: AsyncVitableConnect) -> None: + self._client = client + + @cached_property + def auth(self) -> auth.AsyncAuthResourceWithStreamingResponse: + """Issue short-lived access tokens for scoped API access""" + from .resources.auth import AsyncAuthResourceWithStreamingResponse + + return AsyncAuthResourceWithStreamingResponse(self._client.auth) + + @cached_property + def employees(self) -> employees.AsyncEmployeesResourceWithStreamingResponse: + from .resources.employees import AsyncEmployeesResourceWithStreamingResponse + + return AsyncEmployeesResourceWithStreamingResponse(self._client.employees) + + @cached_property + def employers(self) -> employers.AsyncEmployersResourceWithStreamingResponse: + from .resources.employers import AsyncEmployersResourceWithStreamingResponse + + return AsyncEmployersResourceWithStreamingResponse(self._client.employers) + + @cached_property + def enrollments(self) -> enrollments.AsyncEnrollmentsResourceWithStreamingResponse: + """Manage benefit enrollments and elections for employees""" + from .resources.enrollments import AsyncEnrollmentsResourceWithStreamingResponse + + return AsyncEnrollmentsResourceWithStreamingResponse(self._client.enrollments) + + @cached_property + def webhook_events(self) -> webhook_events.AsyncWebhookEventsResourceWithStreamingResponse: + from .resources.webhook_events import AsyncWebhookEventsResourceWithStreamingResponse + + return AsyncWebhookEventsResourceWithStreamingResponse(self._client.webhook_events) + + @cached_property + def groups(self) -> groups.AsyncGroupsResourceWithStreamingResponse: + from .resources.groups import AsyncGroupsResourceWithStreamingResponse + + return AsyncGroupsResourceWithStreamingResponse(self._client.groups) + + @cached_property + def members(self) -> members.AsyncMembersResourceWithStreamingResponse: + """Browse the members covered across your book and read a member's profile""" + from .resources.members import AsyncMembersResourceWithStreamingResponse + + return AsyncMembersResourceWithStreamingResponse(self._client.members) + + @cached_property + def organizations(self) -> organizations.AsyncOrganizationsResourceWithStreamingResponse: + from .resources.organizations import AsyncOrganizationsResourceWithStreamingResponse + + return AsyncOrganizationsResourceWithStreamingResponse(self._client.organizations) + + @cached_property + def plans(self) -> plans.AsyncPlansResourceWithStreamingResponse: + from .resources.plans import AsyncPlansResourceWithStreamingResponse + + return AsyncPlansResourceWithStreamingResponse(self._client.plans) + + +Client = VitableConnect + +AsyncClient = AsyncVitableConnect diff --git a/src/vitable_connect/_compat.py b/src/vitable_connect/_compat.py new file mode 100644 index 0000000..e6690a4 --- /dev/null +++ b/src/vitable_connect/_compat.py @@ -0,0 +1,226 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload +from datetime import date, datetime +from typing_extensions import Self, Literal, TypedDict + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import IncEx, StrBytesIntFloat + +_T = TypeVar("_T") +_ModelT = TypeVar("_ModelT", bound=pydantic.BaseModel) + +# --------------- Pydantic v2, v3 compatibility --------------- + +# Pyright incorrectly reports some of our functions as overriding a method when they don't +# pyright: reportIncompatibleMethodOverride=false + +PYDANTIC_V1 = pydantic.VERSION.startswith("1.") + +if TYPE_CHECKING: + + def parse_date(value: date | StrBytesIntFloat) -> date: # noqa: ARG001 + ... + + def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: # noqa: ARG001 + ... + + def get_args(t: type[Any]) -> tuple[Any, ...]: # noqa: ARG001 + ... + + def is_union(tp: type[Any] | None) -> bool: # noqa: ARG001 + ... + + def get_origin(t: type[Any]) -> type[Any] | None: # noqa: ARG001 + ... + + def is_literal_type(type_: type[Any]) -> bool: # noqa: ARG001 + ... + + def is_typeddict(type_: type[Any]) -> bool: # noqa: ARG001 + ... + +else: + # v1 re-exports + if PYDANTIC_V1: + from pydantic.typing import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, + ) + from pydantic.datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime + else: + from ._utils import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + parse_date as parse_date, + is_typeddict as is_typeddict, + parse_datetime as parse_datetime, + is_literal_type as is_literal_type, + ) + + +# refactored config +if TYPE_CHECKING: + from pydantic import ConfigDict as ConfigDict +else: + if PYDANTIC_V1: + # TODO: provide an error message here? + ConfigDict = None + else: + from pydantic import ConfigDict as ConfigDict + + +# renamed methods / properties +def parse_obj(model: type[_ModelT], value: object) -> _ModelT: + if PYDANTIC_V1: + return cast(_ModelT, model.parse_obj(value)) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + else: + return model.model_validate(value) + + +def field_is_required(field: FieldInfo) -> bool: + if PYDANTIC_V1: + return field.required # type: ignore + return field.is_required() + + +def field_get_default(field: FieldInfo) -> Any: + value = field.get_default() + if PYDANTIC_V1: + return value + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + + +def field_outer_type(field: FieldInfo) -> Any: + if PYDANTIC_V1: + return field.outer_type_ # type: ignore + return field.annotation + + +def get_model_config(model: type[pydantic.BaseModel]) -> Any: + if PYDANTIC_V1: + return model.__config__ # type: ignore + return model.model_config + + +def get_model_fields(model: type[pydantic.BaseModel]) -> dict[str, FieldInfo]: + if PYDANTIC_V1: + return model.__fields__ # type: ignore + return model.model_fields + + +def model_copy(model: _ModelT, *, deep: bool = False) -> _ModelT: + if PYDANTIC_V1: + return model.copy(deep=deep) # type: ignore + return model.model_copy(deep=deep) + + +def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str: + if PYDANTIC_V1: + return model.json(indent=indent) # type: ignore + return model.model_dump_json(indent=indent) + + +class _ModelDumpKwargs(TypedDict, total=False): + by_alias: bool + + +def model_dump( + model: pydantic.BaseModel, + *, + exclude: IncEx | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + warnings: bool = True, + mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, +) -> dict[str, Any]: + if (not PYDANTIC_V1) or hasattr(model, "model_dump"): + kwargs: _ModelDumpKwargs = {} + if by_alias is not None: + kwargs["by_alias"] = by_alias + return model.model_dump( + mode=mode, + exclude=exclude, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + # warnings are not supported in Pydantic v1 + warnings=True if PYDANTIC_V1 else warnings, + **kwargs, + ) + return cast( + "dict[str, Any]", + model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) + ), + ) + + +def model_parse(model: type[_ModelT], data: Any) -> _ModelT: + if PYDANTIC_V1: + return model.parse_obj(data) # pyright: ignore[reportDeprecated] + return model.model_validate(data) + + +# generic models +if TYPE_CHECKING: + + class GenericModel(pydantic.BaseModel): ... + +else: + if PYDANTIC_V1: + import pydantic.generics + + class GenericModel(pydantic.generics.GenericModel, pydantic.BaseModel): ... + else: + # there no longer needs to be a distinction in v2 but + # we still have to create our own subclass to avoid + # inconsistent MRO ordering errors + class GenericModel(pydantic.BaseModel): ... + + +# cached properties +if TYPE_CHECKING: + cached_property = property + + # we define a separate type (copied from typeshed) + # that represents that `cached_property` is `set`able + # at runtime, which differs from `@property`. + # + # this is a separate type as editors likely special case + # `@property` and we don't want to cause issues just to have + # more helpful internal types. + + class typed_cached_property(Generic[_T]): + func: Callable[[Any], _T] + attrname: str | None + + def __init__(self, func: Callable[[Any], _T]) -> None: ... + + @overload + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... + + @overload + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T: ... + + def __get__(self, instance: object, owner: type[Any] | None = None) -> _T | Self: + raise NotImplementedError() + + def __set_name__(self, owner: type[Any], name: str) -> None: ... + + # __set__ is not defined at runtime, but @cached_property is designed to be settable + def __set__(self, instance: object, value: _T) -> None: ... +else: + from functools import cached_property as cached_property + + typed_cached_property = cached_property diff --git a/src/vitable_connect/_constants.py b/src/vitable_connect/_constants.py new file mode 100644 index 0000000..6ddf2c7 --- /dev/null +++ b/src/vitable_connect/_constants.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import httpx + +RAW_RESPONSE_HEADER = "X-Stainless-Raw-Response" +OVERRIDE_CAST_TO_HEADER = "____stainless_override_cast_to" + +# default timeout is 1 minute +DEFAULT_TIMEOUT = httpx.Timeout(timeout=60, connect=5.0) +DEFAULT_MAX_RETRIES = 2 +DEFAULT_CONNECTION_LIMITS = httpx.Limits(max_connections=100, max_keepalive_connections=20) + +INITIAL_RETRY_DELAY = 0.5 +MAX_RETRY_DELAY = 8.0 diff --git a/src/vitable_connect/_default_clients.py b/src/vitable_connect/_default_clients.py deleted file mode 100644 index 8a96cf5..0000000 --- a/src/vitable_connect/_default_clients.py +++ /dev/null @@ -1,32 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import httpx - -SDK_DEFAULT_TIMEOUT = 60 - -try: - import httpx_aiohttp # type: ignore[import-not-found] -except ImportError: - - class DefaultAioHttpClient(httpx.AsyncClient): # type: ignore - def __init__(self, **kwargs: typing.Any) -> None: - raise RuntimeError( - "To use the aiohttp client, install the aiohttp extra: pip install vitable-connect[aiohttp]" - ) - -else: - - class DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore - def __init__(self, **kwargs: typing.Any) -> None: - kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) - kwargs.setdefault("follow_redirects", True) - super().__init__(**kwargs) - - -class DefaultAsyncHttpxClient(httpx.AsyncClient): - def __init__(self, **kwargs: typing.Any) -> None: - kwargs.setdefault("timeout", SDK_DEFAULT_TIMEOUT) - kwargs.setdefault("follow_redirects", True) - super().__init__(**kwargs) diff --git a/src/vitable_connect/_exceptions.py b/src/vitable_connect/_exceptions.py new file mode 100644 index 0000000..69f7ce5 --- /dev/null +++ b/src/vitable_connect/_exceptions.py @@ -0,0 +1,108 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +__all__ = [ + "BadRequestError", + "AuthenticationError", + "PermissionDeniedError", + "NotFoundError", + "ConflictError", + "UnprocessableEntityError", + "RateLimitError", + "InternalServerError", +] + + +class VitableConnectError(Exception): + pass + + +class APIError(VitableConnectError): + message: str + request: httpx.Request + + body: object | None + """The API response body. + + If the API responded with a valid JSON structure then this property will be the + decoded result. + + If it isn't a valid JSON structure then this will be the raw response. + + If there was no response associated with this error then it will be `None`. + """ + + def __init__(self, message: str, request: httpx.Request, *, body: object | None) -> None: # noqa: ARG002 + super().__init__(message) + self.request = request + self.message = message + self.body = body + + +class APIResponseValidationError(APIError): + response: httpx.Response + status_code: int + + def __init__(self, response: httpx.Response, body: object | None, *, message: str | None = None) -> None: + super().__init__(message or "Data returned by API invalid for expected schema.", response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIStatusError(APIError): + """Raised when an API response has a status code of 4xx or 5xx.""" + + response: httpx.Response + status_code: int + + def __init__(self, message: str, *, response: httpx.Response, body: object | None) -> None: + super().__init__(message, response.request, body=body) + self.response = response + self.status_code = response.status_code + + +class APIConnectionError(APIError): + def __init__(self, *, message: str = "Connection error.", request: httpx.Request) -> None: + super().__init__(message, request, body=None) + + +class APITimeoutError(APIConnectionError): + def __init__(self, request: httpx.Request) -> None: + super().__init__(message="Request timed out.", request=request) + + +class BadRequestError(APIStatusError): + status_code: Literal[400] = 400 # pyright: ignore[reportIncompatibleVariableOverride] + + +class AuthenticationError(APIStatusError): + status_code: Literal[401] = 401 # pyright: ignore[reportIncompatibleVariableOverride] + + +class PermissionDeniedError(APIStatusError): + status_code: Literal[403] = 403 # pyright: ignore[reportIncompatibleVariableOverride] + + +class NotFoundError(APIStatusError): + status_code: Literal[404] = 404 # pyright: ignore[reportIncompatibleVariableOverride] + + +class ConflictError(APIStatusError): + status_code: Literal[409] = 409 # pyright: ignore[reportIncompatibleVariableOverride] + + +class UnprocessableEntityError(APIStatusError): + status_code: Literal[422] = 422 # pyright: ignore[reportIncompatibleVariableOverride] + + +class RateLimitError(APIStatusError): + status_code: Literal[429] = 429 # pyright: ignore[reportIncompatibleVariableOverride] + + +class InternalServerError(APIStatusError): + pass diff --git a/src/vitable_connect/_files.py b/src/vitable_connect/_files.py new file mode 100644 index 0000000..76da9e0 --- /dev/null +++ b/src/vitable_connect/_files.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import io +import os +import pathlib +from typing import Sequence, cast, overload +from typing_extensions import TypeVar, TypeGuard + +import anyio + +from ._types import ( + FileTypes, + FileContent, + RequestFiles, + HttpxFileTypes, + Base64FileInput, + HttpxFileContent, + HttpxRequestFiles, +) +from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t + +_T = TypeVar("_T") + + +def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]: + return isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + + +def is_file_content(obj: object) -> TypeGuard[FileContent]: + return ( + isinstance(obj, bytes) or isinstance(obj, tuple) or isinstance(obj, io.IOBase) or isinstance(obj, os.PathLike) + ) + + +def assert_is_file_content(obj: object, *, key: str | None = None) -> None: + if not is_file_content(obj): + prefix = f"Expected entry at `{key}`" if key is not None else f"Expected file input `{obj!r}`" + raise RuntimeError( + f"{prefix} to be bytes, an io.IOBase instance, PathLike or a tuple but received {type(obj)} instead." + ) from None + + +@overload +def to_httpx_files(files: None) -> None: ... + + +@overload +def to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... + + +def to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: _transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, _transform_file(file)) for key, file in files] + else: + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +def _transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = pathlib.Path(file) + return (path.name, path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +def read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return pathlib.Path(file).read_bytes() + return file + + +@overload +async def async_to_httpx_files(files: None) -> None: ... + + +@overload +async def async_to_httpx_files(files: RequestFiles) -> HttpxRequestFiles: ... + + +async def async_to_httpx_files(files: RequestFiles | None) -> HttpxRequestFiles | None: + if files is None: + return None + + if is_mapping_t(files): + files = {key: await _async_transform_file(file) for key, file in files.items()} + elif is_sequence_t(files): + files = [(key, await _async_transform_file(file)) for key, file in files] + else: + raise TypeError(f"Unexpected file type input {type(files)}, expected mapping or sequence") + + return files + + +async def _async_transform_file(file: FileTypes) -> HttpxFileTypes: + if is_file_content(file): + if isinstance(file, os.PathLike): + path = anyio.Path(file) + return (path.name, await path.read_bytes()) + + return file + + if is_tuple_t(file): + return (file[0], await async_read_file_content(file[1]), *file[2:]) + + raise TypeError(f"Expected file types input to be a FileContent type or to be a tuple") + + +async def async_read_file_content(file: FileContent) -> HttpxFileContent: + if isinstance(file, os.PathLike): + return await anyio.Path(file).read_bytes() + + return file + + +def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T: + """Copy only the containers along the given paths. + + Used to guard against mutation by extract_files without copying the entire structure. + Only dicts and lists that lie on a path are copied; everything else + is returned by reference. + + For example, given paths=[["foo", "files", "file"]] and the structure: + { + "foo": { + "bar": {"baz": {}}, + "files": {"file": } + } + } + The root dict, "foo", and "files" are copied (they lie on the path). + "bar" and "baz" are returned by reference (off the path). + """ + return _deepcopy_with_paths(item, paths, 0) + + +def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T: + if not paths: + return item + if is_mapping(item): + key_to_paths: dict[str, list[Sequence[str]]] = {} + for path in paths: + if index < len(path): + key_to_paths.setdefault(path[index], []).append(path) + + # if no path continues through this mapping, it won't be mutated and copying it is redundant + if not key_to_paths: + return item + + result = dict(item) + for key, subpaths in key_to_paths.items(): + if key in result: + result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1) + return cast(_T, result) + if is_list(item): + array_paths = [path for path in paths if index < len(path) and path[index] == ""] + + # if no path expects a list here, nothing will be mutated inside it - return by reference + if not array_paths: + return cast(_T, item) + return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item]) + return item diff --git a/src/vitable_connect/_models.py b/src/vitable_connect/_models.py new file mode 100644 index 0000000..ca8639a --- /dev/null +++ b/src/vitable_connect/_models.py @@ -0,0 +1,958 @@ +from __future__ import annotations + +import os +import inspect +import weakref +from typing import ( + IO, + TYPE_CHECKING, + Any, + Type, + Union, + Generic, + TypeVar, + Callable, + Iterable, + Optional, + AsyncIterable, + cast, +) +from datetime import date, datetime +from typing_extensions import ( + List, + Unpack, + Literal, + ClassVar, + Protocol, + Required, + Annotated, + ParamSpec, + TypeAlias, + TypedDict, + TypeGuard, + final, + override, + runtime_checkable, +) + +import pydantic +from pydantic.fields import FieldInfo + +from ._types import ( + Body, + IncEx, + Query, + ModelT, + Headers, + Timeout, + NotGiven, + AnyMapping, + HttpxRequestFiles, +) +from ._utils import ( + PropertyInfo, + is_list, + is_given, + json_safe, + lru_cache, + is_mapping, + parse_date, + coerce_boolean, + parse_datetime, + strip_not_given, + extract_type_arg, + is_annotated_type, + is_type_alias_type, + strip_annotated_type, +) +from ._compat import ( + PYDANTIC_V1, + ConfigDict, + GenericModel as BaseGenericModel, + get_args, + is_union, + parse_obj, + get_origin, + is_literal_type, + get_model_config, + get_model_fields, + field_get_default, +) +from ._constants import RAW_RESPONSE_HEADER + +if TYPE_CHECKING: + from pydantic import GetCoreSchemaHandler, ValidatorFunctionWrapHandler + from pydantic_core import CoreSchema, core_schema + from pydantic_core.core_schema import ModelField, ModelSchema, LiteralSchema, ModelFieldsSchema +else: + try: + from pydantic_core import CoreSchema, core_schema + except ImportError: + CoreSchema = None + core_schema = None + +__all__ = ["BaseModel", "GenericModel"] + +_T = TypeVar("_T") +_BaseModelT = TypeVar("_BaseModelT", bound="BaseModel") + +P = ParamSpec("P") + + +@runtime_checkable +class _ConfigProtocol(Protocol): + allow_population_by_field_name: bool + + +class BaseModel(pydantic.BaseModel): + if PYDANTIC_V1: + + @property + @override + def model_fields_set(self) -> set[str]: + # a forwards-compat shim for pydantic v2 + return self.__fields_set__ # type: ignore + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + extra: Any = pydantic.Extra.allow # type: ignore + else: + model_config: ClassVar[ConfigDict] = ConfigDict( + extra="allow", defer_build=coerce_boolean(os.environ.get("DEFER_PYDANTIC_BUILD", "true")) + ) + + def to_dict( + self, + *, + mode: Literal["json", "python"] = "python", + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> dict[str, object]: + """Recursively generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + mode: + If mode is 'json', the dictionary will only contain JSON serializable types. e.g. `datetime` will be turned into a string, `"2024-3-22T18:11:19.117000Z"`. + If mode is 'python', the dictionary may contain any Python objects. e.g. `datetime(2024, 3, 22)` + + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value from the output. + exclude_none: Whether to exclude fields that have a value of `None` from the output. + warnings: Whether to log warnings when invalid fields are encountered. This is only supported in Pydantic v2. + """ + return self.model_dump( + mode=mode, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + def to_json( + self, + *, + indent: int | None = 2, + use_api_names: bool = True, + exclude_unset: bool = True, + exclude_defaults: bool = False, + exclude_none: bool = False, + warnings: bool = True, + ) -> str: + """Generates a JSON string representing this model as it would be received from or sent to the API (but with indentation). + + By default, fields that were not set by the API will not be included, + and keys will match the API response, *not* the property names from the model. + + For example, if the API responds with `"fooBar": true` but we've defined a `foo_bar: bool` property, + the output will use the `"fooBar"` key (unless `use_api_names=False` is passed). + + Args: + indent: Indentation to use in the JSON output. If `None` is passed, the output will be compact. Defaults to `2` + use_api_names: Whether to use the key that the API responded with or the property name. Defaults to `True`. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + warnings: Whether to show any warnings that occurred during serialization. This is only supported in Pydantic v2. + """ + return self.model_dump_json( + indent=indent, + by_alias=use_api_names, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + warnings=warnings, + ) + + @override + def __str__(self) -> str: + # mypy complains about an invalid self arg + return f"{self.__repr_name__()}({self.__repr_str__(', ')})" # type: ignore[misc] + + # Override the 'construct' method in a way that supports recursive parsing without validation. + # Based on https://github.com/samuelcolvin/pydantic/issues/1168#issuecomment-817742836. + @classmethod + @override + def construct( # pyright: ignore[reportIncompatibleMethodOverride] + __cls: Type[ModelT], + _fields_set: set[str] | None = None, + **values: object, + ) -> ModelT: + m = __cls.__new__(__cls) + fields_values: dict[str, object] = {} + + config = get_model_config(__cls) + populate_by_name = ( + config.allow_population_by_field_name + if isinstance(config, _ConfigProtocol) + else config.get("populate_by_name") + ) + + if _fields_set is None: + _fields_set = set() + + model_fields = get_model_fields(__cls) + for name, field in model_fields.items(): + key = field.alias + if key is None or (key not in values and populate_by_name): + key = name + + if key in values: + fields_values[name] = _construct_field(value=values[key], field=field, key=key) + _fields_set.add(name) + else: + fields_values[name] = field_get_default(field) + + extra_field_type = _get_extra_fields_type(__cls) + + _extra = {} + for key, value in values.items(): + if key not in model_fields: + parsed = construct_type(value=value, type_=extra_field_type) if extra_field_type is not None else value + + if PYDANTIC_V1: + _fields_set.add(key) + fields_values[key] = parsed + else: + _extra[key] = parsed + + object.__setattr__(m, "__dict__", fields_values) + + if PYDANTIC_V1: + # init_private_attributes() does not exist in v2 + m._init_private_attributes() # type: ignore + + # copied from Pydantic v1's `construct()` method + object.__setattr__(m, "__fields_set__", _fields_set) + else: + # these properties are copied from Pydantic's `model_construct()` method + object.__setattr__(m, "__pydantic_private__", None) + object.__setattr__(m, "__pydantic_extra__", _extra) + object.__setattr__(m, "__pydantic_fields_set__", _fields_set) + + return m + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + # because the type signatures are technically different + # although not in practice + model_construct = construct + + if PYDANTIC_V1: + # we define aliases for some of the new pydantic v2 methods so + # that we can just document these methods without having to specify + # a specific pydantic version as some users may not know which + # pydantic version they are currently using + + @override + def model_dump( + self, + *, + mode: Literal["json", "python"] | str = "python", + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + ) -> dict[str, Any]: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump + + Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. + + Args: + mode: The mode in which `to_python` should run. + If mode is 'json', the output will only contain JSON serializable types. + If mode is 'python', the output may contain non-JSON-serializable Python objects. + include: A set of fields to include in the output. + exclude: A set of fields to exclude from the output. + context: Additional context to pass to the serializer. + by_alias: Whether to use the field's alias in the dictionary key if defined. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that are set to their default value. + exclude_none: Whether to exclude fields that have a value of `None`. + exclude_computed_fields: Whether to exclude computed fields. + While this can be useful for round-tripping, it is usually recommended to use the dedicated + `round_trip` parameter instead. + round_trip: If True, dumped values should be valid as input for non-idempotent types such as Json[T]. + warnings: How to handle serialization errors. False/"none" ignores them, True/"warn" logs errors, + "error" raises a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError]. + fallback: A function to call when an unknown value is encountered. If not provided, + a [`PydanticSerializationError`][pydantic_core.PydanticSerializationError] error is raised. + serialize_as_any: Whether to serialize fields with duck-typing serialization behavior. + + Returns: + A dictionary representation of the model. + """ + if mode not in {"json", "python"}: + raise ValueError("mode must be either 'json' or 'python'") + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") + dumped = super().dict( # pyright: ignore[reportDeprecated] + include=include, + exclude=exclude, + by_alias=by_alias if by_alias is not None else False, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + return cast("dict[str, Any]", json_safe(dumped)) if mode == "json" else dumped + + @override + def model_dump_json( + self, + *, + indent: int | None = None, + ensure_ascii: bool = False, + include: IncEx | None = None, + exclude: IncEx | None = None, + context: Any | None = None, + by_alias: bool | None = None, + exclude_unset: bool = False, + exclude_defaults: bool = False, + exclude_none: bool = False, + exclude_computed_fields: bool = False, + round_trip: bool = False, + warnings: bool | Literal["none", "warn", "error"] = True, + fallback: Callable[[Any], Any] | None = None, + serialize_as_any: bool = False, + ) -> str: + """Usage docs: https://docs.pydantic.dev/2.4/concepts/serialization/#modelmodel_dump_json + + Generates a JSON representation of the model using Pydantic's `to_json` method. + + Args: + indent: Indentation to use in the JSON output. If None is passed, the output will be compact. + include: Field(s) to include in the JSON output. Can take either a string or set of strings. + exclude: Field(s) to exclude from the JSON output. Can take either a string or set of strings. + by_alias: Whether to serialize using field aliases. + exclude_unset: Whether to exclude fields that have not been explicitly set. + exclude_defaults: Whether to exclude fields that have the default value. + exclude_none: Whether to exclude fields that have a value of `None`. + round_trip: Whether to use serialization/deserialization between JSON and class instance. + warnings: Whether to show any warnings that occurred during serialization. + + Returns: + A JSON string representation of the model. + """ + if round_trip != False: + raise ValueError("round_trip is only supported in Pydantic v2") + if warnings != True: + raise ValueError("warnings is only supported in Pydantic v2") + if context is not None: + raise ValueError("context is only supported in Pydantic v2") + if serialize_as_any != False: + raise ValueError("serialize_as_any is only supported in Pydantic v2") + if fallback is not None: + raise ValueError("fallback is only supported in Pydantic v2") + if ensure_ascii != False: + raise ValueError("ensure_ascii is only supported in Pydantic v2") + if exclude_computed_fields != False: + raise ValueError("exclude_computed_fields is only supported in Pydantic v2") + return super().json( # type: ignore[reportDeprecated] + indent=indent, + include=include, + exclude=exclude, + by_alias=by_alias if by_alias is not None else False, + exclude_unset=exclude_unset, + exclude_defaults=exclude_defaults, + exclude_none=exclude_none, + ) + + +class _EagerIterable(list[_T], Generic[_T]): + """ + Accepts any Iterable[T] input (including generators), consumes it + eagerly, and validates all items upfront. + + Validation preserves the original container type where possible + (e.g. a set[T] stays a set[T]). Serialization (model_dump / JSON) + always emits a list — round-tripping through model_dump() will not + restore the original container type. + """ + + @classmethod + def __get_pydantic_core_schema__( + cls, + source_type: Any, + handler: GetCoreSchemaHandler, + ) -> CoreSchema: + (item_type,) = get_args(source_type) or (Any,) + item_schema: CoreSchema = handler.generate_schema(item_type) + list_of_items_schema: CoreSchema = core_schema.list_schema(item_schema) + + return core_schema.no_info_wrap_validator_function( + cls._validate, + list_of_items_schema, + serialization=core_schema.plain_serializer_function_ser_schema( + cls._serialize, + info_arg=False, + ), + ) + + @staticmethod + def _validate(v: Iterable[_T], handler: "ValidatorFunctionWrapHandler") -> Any: + original_type: type[Any] = type(v) + + # Normalize to list so list_schema can validate each item + if isinstance(v, list): + items: list[_T] = v + else: + try: + items = list(v) + except TypeError as e: + raise TypeError("Value is not iterable") from e + + # Validate items against the inner schema + validated: list[_T] = handler(items) + + # Reconstruct original container type + if original_type is list: + return validated + # str(list) produces the list's repr, not a string built from items, + # so skip reconstruction for str and its subclasses. + if issubclass(original_type, str): + return validated + try: + return original_type(validated) + except (TypeError, ValueError): + # If the type cannot be reconstructed, just return the validated list + return validated + + @staticmethod + def _serialize(v: Iterable[_T]) -> list[_T]: + """Always serialize as a list so Pydantic's JSON encoder is happy.""" + if isinstance(v, list): + return v + return list(v) + + +EagerIterable: TypeAlias = Annotated[Iterable[_T], _EagerIterable] + + +def _construct_field(value: object, field: FieldInfo, key: str) -> object: + if value is None: + return field_get_default(field) + + if PYDANTIC_V1: + type_ = cast(type, field.outer_type_) # type: ignore + else: + type_ = field.annotation # type: ignore + + if type_ is None: + raise RuntimeError(f"Unexpected field type is None for {key}") + + return construct_type(value=value, type_=type_, metadata=getattr(field, "metadata", None)) + + +def _get_extra_fields_type(cls: type[pydantic.BaseModel]) -> type | None: + if PYDANTIC_V1: + # TODO + return None + + schema = cls.__pydantic_core_schema__ + if schema["type"] == "model": + fields = schema["schema"] + if fields["type"] == "model-fields": + extras = fields.get("extras_schema") + if extras and "cls" in extras: + # mypy can't narrow the type + return extras["cls"] # type: ignore[no-any-return] + + return None + + +def is_basemodel(type_: type) -> bool: + """Returns whether or not the given type is either a `BaseModel` or a union of `BaseModel`""" + if is_union(type_): + for variant in get_args(type_): + if is_basemodel(variant): + return True + + return False + + return is_basemodel_type(type_) + + +def is_basemodel_type(type_: type) -> TypeGuard[type[BaseModel] | type[GenericModel]]: + origin = get_origin(type_) or type_ + if not inspect.isclass(origin): + return False + return issubclass(origin, BaseModel) or issubclass(origin, GenericModel) + + +def build( + base_model_cls: Callable[P, _BaseModelT], + *args: P.args, + **kwargs: P.kwargs, +) -> _BaseModelT: + """Construct a BaseModel class without validation. + + This is useful for cases where you need to instantiate a `BaseModel` + from an API response as this provides type-safe params which isn't supported + by helpers like `construct_type()`. + + ```py + build(MyModel, my_field_a="foo", my_field_b=123) + ``` + """ + if args: + raise TypeError( + "Received positional arguments which are not supported; Keyword arguments must be used instead", + ) + + return cast(_BaseModelT, construct_type(type_=base_model_cls, value=kwargs)) + + +def construct_type_unchecked(*, value: object, type_: type[_T]) -> _T: + """Loose coercion to the expected type with construction of nested values. + + Note: the returned value from this function is not guaranteed to match the + given type. + """ + return cast(_T, construct_type(value=value, type_=type_)) + + +def construct_type(*, value: object, type_: object, metadata: Optional[List[Any]] = None) -> object: + """Loose coercion to the expected type with construction of nested values. + + If the given value does not match the expected type then it is returned as-is. + """ + + # store a reference to the original type we were given before we extract any inner + # types so that we can properly resolve forward references in `TypeAliasType` annotations + original_type = None + + # we allow `object` as the input type because otherwise, passing things like + # `Literal['value']` will be reported as a type error by type checkers + type_ = cast("type[object]", type_) + if is_type_alias_type(type_): + original_type = type_ # type: ignore[unreachable] + type_ = type_.__value__ # type: ignore[unreachable] + + # unwrap `Annotated[T, ...]` -> `T` + if metadata is not None and len(metadata) > 0: + meta: tuple[Any, ...] = tuple(metadata) + elif is_annotated_type(type_): + meta = get_args(type_)[1:] + type_ = extract_type_arg(type_, 0) + else: + meta = tuple() + + # we need to use the origin class for any types that are subscripted generics + # e.g. Dict[str, object] + origin = get_origin(type_) or type_ + args = get_args(type_) + + if is_union(origin): + try: + return validate_type(type_=cast("type[object]", original_type or type_), value=value) + except Exception: + pass + + # if the type is a discriminated union then we want to construct the right variant + # in the union, even if the data doesn't match exactly, otherwise we'd break code + # that relies on the constructed class types, e.g. + # + # class FooType: + # kind: Literal['foo'] + # value: str + # + # class BarType: + # kind: Literal['bar'] + # value: int + # + # without this block, if the data we get is something like `{'kind': 'bar', 'value': 'foo'}` then + # we'd end up constructing `FooType` when it should be `BarType`. + discriminator = _build_discriminated_union_meta(union=type_, meta_annotations=meta) + if discriminator and is_mapping(value): + variant_value = value.get(discriminator.field_alias_from or discriminator.field_name) + if variant_value and isinstance(variant_value, str): + variant_type = discriminator.mapping.get(variant_value) + if variant_type: + return construct_type(type_=variant_type, value=value) + + # if the data is not valid, use the first variant that doesn't fail while deserializing + for variant in args: + try: + return construct_type(value=value, type_=variant) + except Exception: + continue + + raise RuntimeError(f"Could not convert data into a valid instance of {type_}") + + if origin == dict: + if not is_mapping(value): + return value + + _, items_type = get_args(type_) # Dict[_, items_type] + return {key: construct_type(value=item, type_=items_type) for key, item in value.items()} + + if ( + not is_literal_type(type_) + and inspect.isclass(origin) + and (issubclass(origin, BaseModel) or issubclass(origin, GenericModel)) + ): + if is_list(value): + return [cast(Any, type_).construct(**entry) if is_mapping(entry) else entry for entry in value] + + if is_mapping(value): + if issubclass(type_, BaseModel): + return type_.construct(**value) # type: ignore[arg-type] + + return cast(Any, type_).construct(**value) + + if origin == list: + if not is_list(value): + return value + + inner_type = args[0] # List[inner_type] + return [construct_type(value=entry, type_=inner_type) for entry in value] + + if origin == float: + if isinstance(value, int): + coerced = float(value) + if coerced != value: + return value + return coerced + + return value + + if type_ == datetime: + try: + return parse_datetime(value) # type: ignore + except Exception: + return value + + if type_ == date: + try: + return parse_date(value) # type: ignore + except Exception: + return value + + return value + + +@runtime_checkable +class CachedDiscriminatorType(Protocol): + __discriminator__: DiscriminatorDetails + + +DISCRIMINATOR_CACHE: weakref.WeakKeyDictionary[type, DiscriminatorDetails] = weakref.WeakKeyDictionary() + + +class DiscriminatorDetails: + field_name: str + """The name of the discriminator field in the variant class, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] + ``` + + Will result in field_name='type' + """ + + field_alias_from: str | None + """The name of the discriminator field in the API response, e.g. + + ```py + class Foo(BaseModel): + type: Literal['foo'] = Field(alias='type_from_api') + ``` + + Will result in field_alias_from='type_from_api' + """ + + mapping: dict[str, type] + """Mapping of discriminator value to variant type, e.g. + + {'foo': FooVariant, 'bar': BarVariant} + """ + + def __init__( + self, + *, + mapping: dict[str, type], + discriminator_field: str, + discriminator_alias: str | None, + ) -> None: + self.mapping = mapping + self.field_name = discriminator_field + self.field_alias_from = discriminator_alias + + +def _build_discriminated_union_meta(*, union: type, meta_annotations: tuple[Any, ...]) -> DiscriminatorDetails | None: + cached = DISCRIMINATOR_CACHE.get(union) + if cached is not None: + return cached + + discriminator_field_name: str | None = None + + for annotation in meta_annotations: + if isinstance(annotation, PropertyInfo) and annotation.discriminator is not None: + discriminator_field_name = annotation.discriminator + break + + if not discriminator_field_name: + return None + + mapping: dict[str, type] = {} + discriminator_alias: str | None = None + + for variant in get_args(union): + variant = strip_annotated_type(variant) + if is_basemodel_type(variant): + if PYDANTIC_V1: + field_info = cast("dict[str, FieldInfo]", variant.__fields__).get(discriminator_field_name) # pyright: ignore[reportDeprecated, reportUnnecessaryCast] + if not field_info: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field_info.alias + + if (annotation := getattr(field_info, "annotation", None)) and is_literal_type(annotation): + for entry in get_args(annotation): + if isinstance(entry, str): + mapping[entry] = variant + else: + field = _extract_field_schema_pv2(variant, discriminator_field_name) + if not field: + continue + + # Note: if one variant defines an alias then they all should + discriminator_alias = field.get("serialization_alias") + + field_schema = field["schema"] + + if field_schema["type"] == "literal": + for entry in cast("LiteralSchema", field_schema)["expected"]: + if isinstance(entry, str): + mapping[entry] = variant + + if not mapping: + return None + + details = DiscriminatorDetails( + mapping=mapping, + discriminator_field=discriminator_field_name, + discriminator_alias=discriminator_alias, + ) + DISCRIMINATOR_CACHE.setdefault(union, details) + return details + + +def _extract_field_schema_pv2(model: type[BaseModel], field_name: str) -> ModelField | None: + schema = model.__pydantic_core_schema__ + if schema["type"] == "definitions": + schema = schema["schema"] + + if schema["type"] != "model": + return None + + schema = cast("ModelSchema", schema) + fields_schema = schema["schema"] + if fields_schema["type"] != "model-fields": + return None + + fields_schema = cast("ModelFieldsSchema", fields_schema) + field = fields_schema["fields"].get(field_name) + if not field: + return None + + return cast("ModelField", field) # pyright: ignore[reportUnnecessaryCast] + + +def validate_type(*, type_: type[_T], value: object) -> _T: + """Strict validation that the given value matches the expected type""" + if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): + return cast(_T, parse_obj(type_, value)) + + return cast(_T, _validate_non_model_type(type_=type_, value=value)) + + +def set_pydantic_config(typ: Any, config: pydantic.ConfigDict) -> None: + """Add a pydantic config for the given type. + + Note: this is a no-op on Pydantic v1. + """ + setattr(typ, "__pydantic_config__", config) # noqa: B010 + + +# our use of subclassing here causes weirdness for type checkers, +# so we just pretend that we don't subclass +if TYPE_CHECKING: + GenericModel = BaseModel +else: + + class GenericModel(BaseGenericModel, BaseModel): + pass + + +if not PYDANTIC_V1: + from pydantic import TypeAdapter as _TypeAdapter + + _CachedTypeAdapter = cast("TypeAdapter[object]", lru_cache(maxsize=None)(_TypeAdapter)) + + if TYPE_CHECKING: + from pydantic import TypeAdapter + else: + TypeAdapter = _CachedTypeAdapter + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + return TypeAdapter(type_).validate_python(value) + +elif not TYPE_CHECKING: # TODO: condition is weird + + class RootModel(GenericModel, Generic[_T]): + """Used as a placeholder to easily convert runtime types to a Pydantic format + to provide validation. + + For example: + ```py + validated = RootModel[int](__root__="5").__root__ + # validated: 5 + ``` + """ + + __root__: _T + + def _validate_non_model_type(*, type_: type[_T], value: object) -> _T: + model = _create_pydantic_model(type_).validate(value) + return cast(_T, model.__root__) + + def _create_pydantic_model(type_: _T) -> Type[RootModel[_T]]: + return RootModel[type_] # type: ignore + + +class SecurityOptions(TypedDict, total=False): + api_key_auth: bool + + +class FinalRequestOptionsInput(TypedDict, total=False): + method: Required[str] + url: Required[str] + params: Query + headers: Headers + max_retries: int + timeout: float | Timeout | None + files: HttpxRequestFiles | None + idempotency_key: str + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] + json_data: Body + extra_json: AnyMapping + follow_redirects: bool + security: SecurityOptions + + +@final +class FinalRequestOptions(pydantic.BaseModel): + method: str + url: str + params: Query = {} + headers: Union[Headers, NotGiven] = NotGiven() + max_retries: Union[int, NotGiven] = NotGiven() + timeout: Union[float, Timeout, None, NotGiven] = NotGiven() + files: Union[HttpxRequestFiles, None] = None + idempotency_key: Union[str, None] = None + post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven() + follow_redirects: Union[bool, None] = None + security: SecurityOptions = {"api_key_auth": True} + + content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None + # It should be noted that we cannot use `json` here as that would override + # a BaseModel method in an incompatible fashion. + json_data: Union[Body, None] = None + extra_json: Union[AnyMapping, None] = None + + if PYDANTIC_V1: + + class Config(pydantic.BaseConfig): # pyright: ignore[reportDeprecated] + arbitrary_types_allowed: bool = True + else: + model_config: ClassVar[ConfigDict] = ConfigDict(arbitrary_types_allowed=True) + + def get_max_retries(self, max_retries: int) -> int: + if isinstance(self.max_retries, NotGiven): + return max_retries + return self.max_retries + + def _strip_raw_response_header(self) -> None: + if not is_given(self.headers): + return + + if self.headers.get(RAW_RESPONSE_HEADER): + self.headers = {**self.headers} + self.headers.pop(RAW_RESPONSE_HEADER) + + # override the `construct` method so that we can run custom transformations. + # this is necessary as we don't want to do any actual runtime type checking + # (which means we can't use validators) but we do want to ensure that `NotGiven` + # values are not present + # + # type ignore required because we're adding explicit types to `**values` + @classmethod + def construct( # type: ignore + cls, + _fields_set: set[str] | None = None, + **values: Unpack[FinalRequestOptionsInput], + ) -> FinalRequestOptions: + kwargs: dict[str, Any] = { + # we unconditionally call `strip_not_given` on any value + # as it will just ignore any non-mapping types + key: strip_not_given(value) + for key, value in values.items() + } + if PYDANTIC_V1: + return cast(FinalRequestOptions, super().construct(_fields_set, **kwargs)) # pyright: ignore[reportDeprecated] + return super().model_construct(_fields_set, **kwargs) + + if not TYPE_CHECKING: + # type checkers incorrectly complain about this assignment + model_construct = construct diff --git a/src/vitable_connect/_qs.py b/src/vitable_connect/_qs.py new file mode 100644 index 0000000..4127c19 --- /dev/null +++ b/src/vitable_connect/_qs.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from typing import Any, List, Tuple, Union, Mapping, TypeVar +from urllib.parse import parse_qs, urlencode +from typing_extensions import get_args + +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given +from ._utils import flatten + +_T = TypeVar("_T") + +PrimitiveData = Union[str, int, float, bool, None] +# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] +# https://github.com/microsoft/pyright/issues/3555 +Data = Union[PrimitiveData, List[Any], Tuple[Any], "Mapping[str, Any]"] +Params = Mapping[str, Data] + + +class Querystring: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + *, + array_format: ArrayFormat = "repeat", + nested_format: NestedFormat = "brackets", + ) -> None: + self.array_format = array_format + self.nested_format = nested_format + + def parse(self, query: str) -> Mapping[str, object]: + # Note: custom format syntax is not supported yet + return parse_qs(query) + + def stringify( + self, + params: Params, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> str: + return urlencode( + self.stringify_items( + params, + array_format=array_format, + nested_format=nested_format, + ) + ) + + def stringify_items( + self, + params: Params, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> list[tuple[str, str]]: + opts = Options( + qs=self, + array_format=array_format, + nested_format=nested_format, + ) + return flatten([self._stringify_item(key, value, opts) for key, value in params.items()]) + + def _stringify_item( + self, + key: str, + value: Data, + opts: Options, + ) -> list[tuple[str, str]]: + if isinstance(value, Mapping): + items: list[tuple[str, str]] = [] + nested_format = opts.nested_format + for subkey, subvalue in value.items(): + items.extend( + self._stringify_item( + # TODO: error if unknown format + f"{key}.{subkey}" if nested_format == "dots" else f"{key}[{subkey}]", + subvalue, + opts, + ) + ) + return items + + if isinstance(value, (list, tuple)): + array_format = opts.array_format + if array_format == "comma": + return [ + ( + key, + ",".join(self._primitive_value_to_str(item) for item in value if item is not None), + ), + ] + elif array_format == "repeat": + items = [] + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + elif array_format == "indices": + items = [] + for i, item in enumerate(value): + items.extend(self._stringify_item(f"{key}[{i}]", item, opts)) + return items + elif array_format == "brackets": + items = [] + key = key + "[]" + for item in value: + items.extend(self._stringify_item(key, item, opts)) + return items + else: + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + serialised = self._primitive_value_to_str(value) + if not serialised: + return [] + return [(key, serialised)] + + def _primitive_value_to_str(self, value: PrimitiveData) -> str: + # copied from httpx + if value is True: + return "true" + elif value is False: + return "false" + elif value is None: + return "" + return str(value) + + +_qs = Querystring() +parse = _qs.parse +stringify = _qs.stringify +stringify_items = _qs.stringify_items + + +class Options: + array_format: ArrayFormat + nested_format: NestedFormat + + def __init__( + self, + qs: Querystring = _qs, + *, + array_format: ArrayFormat | NotGiven = not_given, + nested_format: NestedFormat | NotGiven = not_given, + ) -> None: + self.array_format = qs.array_format if isinstance(array_format, NotGiven) else array_format + self.nested_format = qs.nested_format if isinstance(nested_format, NotGiven) else nested_format diff --git a/src/vitable_connect/_resource.py b/src/vitable_connect/_resource.py new file mode 100644 index 0000000..5dbf6c2 --- /dev/null +++ b/src/vitable_connect/_resource.py @@ -0,0 +1,43 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import time +from typing import TYPE_CHECKING + +import anyio + +if TYPE_CHECKING: + from ._client import VitableConnect, AsyncVitableConnect + + +class SyncAPIResource: + _client: VitableConnect + + def __init__(self, client: VitableConnect) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + def _sleep(self, seconds: float) -> None: + time.sleep(seconds) + + +class AsyncAPIResource: + _client: AsyncVitableConnect + + def __init__(self, client: AsyncVitableConnect) -> None: + self._client = client + self._get = client.get + self._post = client.post + self._patch = client.patch + self._put = client.put + self._delete = client.delete + self._get_api_list = client.get_api_list + + async def _sleep(self, seconds: float) -> None: + await anyio.sleep(seconds) diff --git a/src/vitable_connect/_response.py b/src/vitable_connect/_response.py new file mode 100644 index 0000000..e1f6411 --- /dev/null +++ b/src/vitable_connect/_response.py @@ -0,0 +1,835 @@ +from __future__ import annotations + +import os +import inspect +import logging +import datetime +import functools +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Union, + Generic, + TypeVar, + Callable, + Iterator, + AsyncIterator, + cast, + overload, +) +from typing_extensions import Awaitable, ParamSpec, override, get_origin + +import anyio +import httpx +import pydantic + +from ._types import NoneType +from ._utils import is_given, extract_type_arg, is_annotated_type, is_type_alias_type, extract_type_var_from_base +from ._models import BaseModel, is_basemodel +from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER +from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type +from ._exceptions import VitableConnectError, APIResponseValidationError + +if TYPE_CHECKING: + from ._models import FinalRequestOptions + from ._base_client import BaseClient + + +P = ParamSpec("P") +R = TypeVar("R") +_T = TypeVar("_T") +_APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]") +_AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]") + +log: logging.Logger = logging.getLogger(__name__) + + +class BaseAPIResponse(Generic[R]): + _cast_to: type[R] + _client: BaseClient[Any, Any] + _parsed_by_type: dict[type[Any], Any] + _is_sse_stream: bool + _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None + _options: FinalRequestOptions + + http_response: httpx.Response + + retries_taken: int + """The number of retries made. If no retries happened this will be `0`""" + + def __init__( + self, + *, + raw: httpx.Response, + cast_to: type[R], + client: BaseClient[Any, Any], + stream: bool, + stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None, + options: FinalRequestOptions, + retries_taken: int = 0, + ) -> None: + self._cast_to = cast_to + self._client = client + self._parsed_by_type = {} + self._is_sse_stream = stream + self._stream_cls = stream_cls + self._options = options + self.http_response = raw + self.retries_taken = retries_taken + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def http_request(self) -> httpx.Request: + """Returns the httpx Request instance associated with the current response.""" + return self.http_response.request + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def url(self) -> httpx.URL: + """Returns the URL for which the request was made.""" + return self.http_response.url + + @property + def method(self) -> str: + return self.http_request.method + + @property + def http_version(self) -> str: + return self.http_response.http_version + + @property + def elapsed(self) -> datetime.timedelta: + """The time taken for the complete request/response cycle to complete.""" + return self.http_response.elapsed + + @property + def is_closed(self) -> bool: + """Whether or not the response body has been closed. + + If this is False then there is response data that has not been read yet. + You must either fully consume the response body or call `.close()` + before discarding the response to prevent resource leaks. + """ + return self.http_response.is_closed + + @override + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>" + ) + + def _parse(self, *, to: type[_T] | None = None) -> R | _T: + cast_to = to if to is not None else self._cast_to + + # unwrap `TypeAlias('Name', T)` -> `T` + if is_type_alias_type(cast_to): + cast_to = cast_to.__value__ # type: ignore[unreachable] + + # unwrap `Annotated[T, ...]` -> `T` + if cast_to and is_annotated_type(cast_to): + cast_to = extract_type_arg(cast_to, 0) + + origin = get_origin(cast_to) or cast_to + + if self._is_sse_stream: + if to: + if not is_stream_class_type(to): + raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}") + + return cast( + _T, + to( + cast_to=extract_stream_chunk_type( + to, + failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]", + ), + response=self.http_response, + client=cast(Any, self._client), + options=self._options, + ), + ) + + if self._stream_cls: + return cast( + R, + self._stream_cls( + cast_to=extract_stream_chunk_type(self._stream_cls), + response=self.http_response, + client=cast(Any, self._client), + options=self._options, + ), + ) + + stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls) + if stream_cls is None: + raise MissingStreamClassError() + + return cast( + R, + stream_cls( + cast_to=cast_to, + response=self.http_response, + client=cast(Any, self._client), + options=self._options, + ), + ) + + if cast_to is NoneType: + return cast(R, None) + + response = self.http_response + if cast_to == str: + return cast(R, response.text) + + if cast_to == bytes: + return cast(R, response.content) + + if cast_to == int: + return cast(R, int(response.text)) + + if cast_to == float: + return cast(R, float(response.text)) + + if cast_to == bool: + return cast(R, response.text.lower() == "true") + + if origin == APIResponse: + raise RuntimeError("Unexpected state - cast_to is `APIResponse`") + + if inspect.isclass(origin) and issubclass(origin, httpx.Response): + # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response + # and pass that class to our request functions. We cannot change the variance to be either + # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct + # the response class ourselves but that is something that should be supported directly in httpx + # as it would be easy to incorrectly construct the Response object due to the multitude of arguments. + if cast_to != httpx.Response: + raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`") + return cast(R, response) + + if ( + inspect.isclass( + origin # pyright: ignore[reportUnknownArgumentType] + ) + and not issubclass(origin, BaseModel) + and issubclass(origin, pydantic.BaseModel) + ): + raise TypeError( + "Pydantic models must subclass our base model type, e.g. `from vitable_connect import BaseModel`" + ) + + if ( + cast_to is not object + and not origin is list + and not origin is dict + and not origin is Union + and not issubclass(origin, BaseModel) + ): + raise RuntimeError( + f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}." + ) + + # split is required to handle cases where additional information is included + # in the response, e.g. application/json; charset=utf-8 + content_type, *_ = response.headers.get("content-type", "*").split(";") + if not content_type.endswith("json"): + if is_basemodel(cast_to): + try: + data = response.json() + except Exception as exc: + log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc) + else: + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + if self._client._strict_response_validation: + raise APIResponseValidationError( + response=response, + message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.", + body=response.text, + ) + + # If the API responds with content that isn't JSON then we just return + # the (decoded) text without performing any parsing so that you can still + # handle the response however you need to. + return response.text # type: ignore + + data = response.json() + + return self._client._process_response_data( + data=data, + cast_to=cast_to, # type: ignore + response=response, + ) + + +class APIResponse(BaseAPIResponse[R]): + @overload + def parse(self, *, to: type[_T]) -> _T: ... + + @overload + def parse(self) -> R: ... + + def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from vitable_connect import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `int` + - `float` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return self.http_response.read() + except httpx.StreamConsumed as exc: + # The default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message. + raise StreamAlreadyConsumed() from exc + + def text(self) -> str: + """Read and decode the response content into a string.""" + self.read() + return self.http_response.text + + def json(self) -> object: + """Read and decode the JSON response content.""" + self.read() + return self.http_response.json() + + def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.http_response.close() + + def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + for chunk in self.http_response.iter_bytes(chunk_size): + yield chunk + + def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + for chunk in self.http_response.iter_text(chunk_size): + yield chunk + + def iter_lines(self) -> Iterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + for chunk in self.http_response.iter_lines(): + yield chunk + + +class AsyncAPIResponse(BaseAPIResponse[R]): + @overload + async def parse(self, *, to: type[_T]) -> _T: ... + + @overload + async def parse(self) -> R: ... + + async def parse(self, *, to: type[_T] | None = None) -> R | _T: + """Returns the rich python representation of this response's data. + + For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`. + + You can customise the type that the response is parsed into through + the `to` argument, e.g. + + ```py + from vitable_connect import BaseModel + + + class MyModel(BaseModel): + foo: str + + + obj = response.parse(to=MyModel) + print(obj.foo) + ``` + + We support parsing: + - `BaseModel` + - `dict` + - `list` + - `Union` + - `str` + - `httpx.Response` + """ + cache_key = to if to is not None else self._cast_to + cached = self._parsed_by_type.get(cache_key) + if cached is not None: + return cached # type: ignore[no-any-return] + + if not self._is_sse_stream: + await self.read() + + parsed = self._parse(to=to) + if is_given(self._options.post_parser): + parsed = self._options.post_parser(parsed) + + self._parsed_by_type[cache_key] = parsed + return parsed + + async def read(self) -> bytes: + """Read and return the binary response content.""" + try: + return await self.http_response.aread() + except httpx.StreamConsumed as exc: + # the default error raised by httpx isn't very + # helpful in our case so we re-raise it with + # a different error message + raise StreamAlreadyConsumed() from exc + + async def text(self) -> str: + """Read and decode the response content into a string.""" + await self.read() + return self.http_response.text + + async def json(self) -> object: + """Read and decode the JSON response content.""" + await self.read() + return self.http_response.json() + + async def close(self) -> None: + """Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.http_response.aclose() + + async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: + """ + A byte-iterator over the decoded response content. + + This automatically handles gzip, deflate and brotli encoded responses. + """ + async for chunk in self.http_response.aiter_bytes(chunk_size): + yield chunk + + async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: + """A str-iterator over the decoded response content + that handles both gzip, deflate, etc but also detects the content's + string encoding. + """ + async for chunk in self.http_response.aiter_text(chunk_size): + yield chunk + + async def iter_lines(self) -> AsyncIterator[str]: + """Like `iter_text()` but will only yield chunks for each line""" + async for chunk in self.http_response.aiter_lines(): + yield chunk + + +class BinaryAPIResponse(APIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(): + f.write(data) + + +class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]): + """Subclass of APIResponse providing helpers for dealing with binary data. + + Note: If you want to stream the response data instead of eagerly reading it + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + + async def write_to_file( + self, + file: str | os.PathLike[str], + ) -> None: + """Write the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + + Note: if you want to stream the data to the file instead of writing + all at once then you should use `.with_streaming_response` when making + the API request, e.g. `.with_streaming_response.get_binary_response()` + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(): + await f.write(data) + + +class StreamedBinaryAPIResponse(APIResponse[bytes]): + def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + with open(file, mode="wb") as f: + for data in self.iter_bytes(chunk_size): + f.write(data) + + +class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]): + async def stream_to_file( + self, + file: str | os.PathLike[str], + *, + chunk_size: int | None = None, + ) -> None: + """Streams the output to the given file. + + Accepts a filename or any path-like object, e.g. pathlib.Path + """ + path = anyio.Path(file) + async with await path.open(mode="wb") as f: + async for data in self.iter_bytes(chunk_size): + await f.write(data) + + +class MissingStreamClassError(TypeError): + def __init__(self) -> None: + super().__init__( + "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `vitable_connect._streaming` for reference", + ) + + +class StreamAlreadyConsumed(VitableConnectError): + """ + Attempted to read or stream content, but the content has already + been streamed. + + This can happen if you use a method like `.iter_lines()` and then attempt + to read th entire response body afterwards, e.g. + + ```py + response = await client.post(...) + async for line in response.iter_lines(): + ... # do something with `line` + + content = await response.read() + # ^ error + ``` + + If you want this behaviour you'll need to either manually accumulate the response + content or call `await response.read()` before iterating over the stream. + """ + + def __init__(self) -> None: + message = ( + "Attempted to read or stream some content, but the content has " + "already been streamed. " + "This could be due to attempting to stream the response " + "content more than once." + "\n\n" + "You can fix this by manually accumulating the response content while streaming " + "or by calling `.read()` before starting to stream." + ) + super().__init__(message) + + +class ResponseContextManager(Generic[_APIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, request_func: Callable[[], _APIResponseT]) -> None: + self._request_func = request_func + self.__response: _APIResponseT | None = None + + def __enter__(self) -> _APIResponseT: + self.__response = self._request_func() + return self.__response + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + self.__response.close() + + +class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]): + """Context manager for ensuring that a request is not made + until it is entered and that the response will always be closed + when the context manager exits + """ + + def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None: + self._api_request = api_request + self.__response: _AsyncAPIResponseT | None = None + + async def __aenter__(self) -> _AsyncAPIResponseT: + self.__response = await self._api_request + return self.__response + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self.__response is not None: + await self.__response.close() + + +def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request)) + + return wrapped + + +def async_to_streamed_response_wrapper( + func: Callable[P, Awaitable[R]], +) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support streaming and returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request)) + + return wrapped + + +def to_custom_streamed_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, ResponseContextManager[_APIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = functools.partial(func, *args, **kwargs) + + return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request)) + + return wrapped + + +def async_to_custom_streamed_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support streaming and returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "stream" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + make_request = func(*args, **kwargs) + + return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request)) + + return wrapped + + +def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(APIResponse[R], func(*args, **kwargs)) + + return wrapped + + +def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]: + """Higher order function that takes one of our bound API methods and wraps it + to support returning the raw `APIResponse` object directly. + """ + + @functools.wraps(func) + async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]: + extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + + kwargs["extra_headers"] = extra_headers + + return cast(AsyncAPIResponse[R], await func(*args, **kwargs)) + + return wrapped + + +def to_custom_raw_response_wrapper( + func: Callable[P, object], + response_cls: type[_APIResponseT], +) -> Callable[P, _APIResponseT]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(_APIResponseT, func(*args, **kwargs)) + + return wrapped + + +def async_to_custom_raw_response_wrapper( + func: Callable[P, Awaitable[object]], + response_cls: type[_AsyncAPIResponseT], +) -> Callable[P, Awaitable[_AsyncAPIResponseT]]: + """Higher order function that takes one of our bound API methods and an `APIResponse` class + and wraps the method to support returning the given response class directly. + + Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])` + """ + + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]: + extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})} + extra_headers[RAW_RESPONSE_HEADER] = "raw" + extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls + + kwargs["extra_headers"] = extra_headers + + return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs)) + + return wrapped + + +def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type: + """Given a type like `APIResponse[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(APIResponse[bytes]): + ... + + extract_response_type(MyResponse) -> bytes + ``` + """ + return extract_type_var_from_base( + typ, + generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)), + index=0, + ) diff --git a/src/vitable_connect/_streaming.py b/src/vitable_connect/_streaming.py new file mode 100644 index 0000000..4b6ad0b --- /dev/null +++ b/src/vitable_connect/_streaming.py @@ -0,0 +1,338 @@ +# Note: initially copied from https://github.com/florimondmanca/httpx-sse/blob/master/src/httpx_sse/_decoders.py +from __future__ import annotations + +import json +import inspect +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Iterator, Optional, AsyncIterator, cast +from typing_extensions import Self, Protocol, TypeGuard, override, get_origin, runtime_checkable + +import httpx + +from ._utils import extract_type_var_from_base + +if TYPE_CHECKING: + from ._client import VitableConnect, AsyncVitableConnect + from ._models import FinalRequestOptions + + +_T = TypeVar("_T") + + +class Stream(Generic[_T]): + """Provides the core interface to iterate over a synchronous stream response.""" + + response: httpx.Response + _options: Optional[FinalRequestOptions] = None + _decoder: SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: VitableConnect, + options: Optional[FinalRequestOptions] = None, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._options = options + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + def __next__(self) -> _T: + return self._iterator.__next__() + + def __iter__(self) -> Iterator[_T]: + for item in self._iterator: + yield item + + def _iter_events(self) -> Iterator[ServerSentEvent]: + yield from self._decoder.iter_bytes(self.response.iter_bytes()) + + def __stream__(self) -> Iterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + try: + for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + response.close() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + self.response.close() + + +class AsyncStream(Generic[_T]): + """Provides the core interface to iterate over an asynchronous stream response.""" + + response: httpx.Response + _options: Optional[FinalRequestOptions] = None + _decoder: SSEDecoder | SSEBytesDecoder + + def __init__( + self, + *, + cast_to: type[_T], + response: httpx.Response, + client: AsyncVitableConnect, + options: Optional[FinalRequestOptions] = None, + ) -> None: + self.response = response + self._cast_to = cast_to + self._client = client + self._options = options + self._decoder = client._make_sse_decoder() + self._iterator = self.__stream__() + + async def __anext__(self) -> _T: + return await self._iterator.__anext__() + + async def __aiter__(self) -> AsyncIterator[_T]: + async for item in self._iterator: + yield item + + async def _iter_events(self) -> AsyncIterator[ServerSentEvent]: + async for sse in self._decoder.aiter_bytes(self.response.aiter_bytes()): + yield sse + + async def __stream__(self) -> AsyncIterator[_T]: + cast_to = cast(Any, self._cast_to) + response = self.response + process_data = self._client._process_response_data + iterator = self._iter_events() + + try: + async for sse in iterator: + yield process_data(data=sse.json(), cast_to=cast_to, response=response) + finally: + # Ensure the response is closed even if the consumer doesn't read all data + await response.aclose() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + async def close(self) -> None: + """ + Close the response and release the connection. + + Automatically called if the response body is read to completion. + """ + await self.response.aclose() + + +class ServerSentEvent: + def __init__( + self, + *, + event: str | None = None, + data: str | None = None, + id: str | None = None, + retry: int | None = None, + ) -> None: + if data is None: + data = "" + + self._id = id + self._data = data + self._event = event or None + self._retry = retry + + @property + def event(self) -> str | None: + return self._event + + @property + def id(self) -> str | None: + return self._id + + @property + def retry(self) -> int | None: + return self._retry + + @property + def data(self) -> str: + return self._data + + def json(self) -> Any: + return json.loads(self.data) + + @override + def __repr__(self) -> str: + return f"ServerSentEvent(event={self.event}, data={self.data}, id={self.id}, retry={self.retry})" + + +class SSEDecoder: + _data: list[str] + _event: str | None + _retry: int | None + _last_event_id: str | None + + def __init__(self) -> None: + self._event = None + self._data = [] + self._last_event_id = None + self._retry = None + + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + for chunk in self._iter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + def _iter_chunks(self, iterator: Iterator[bytes]) -> Iterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + async for chunk in self._aiter_chunks(iterator): + # Split before decoding so splitlines() only uses \r and \n + for raw_line in chunk.splitlines(): + line = raw_line.decode("utf-8") + sse = self.decode(line) + if sse: + yield sse + + async def _aiter_chunks(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[bytes]: + """Given an iterator that yields raw binary data, iterate over it and yield individual SSE chunks""" + data = b"" + async for chunk in iterator: + for line in chunk.splitlines(keepends=True): + data += line + if data.endswith((b"\r\r", b"\n\n", b"\r\n\r\n")): + yield data + data = b"" + if data: + yield data + + def decode(self, line: str) -> ServerSentEvent | None: + # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 + + if not line: + if not self._event and not self._data and not self._last_event_id and self._retry is None: + return None + + sse = ServerSentEvent( + event=self._event, + data="\n".join(self._data), + id=self._last_event_id, + retry=self._retry, + ) + + # NOTE: as per the SSE spec, do not reset last_event_id. + self._event = None + self._data = [] + self._retry = None + + return sse + + if line.startswith(":"): + return None + + fieldname, _, value = line.partition(":") + + if value.startswith(" "): + value = value[1:] + + if fieldname == "event": + self._event = value + elif fieldname == "data": + self._data.append(value) + elif fieldname == "id": + if "\0" in value: + pass + else: + self._last_event_id = value + elif fieldname == "retry": + try: + self._retry = int(value) + except (TypeError, ValueError): + pass + else: + pass # Field is ignored. + + return None + + +@runtime_checkable +class SSEBytesDecoder(Protocol): + def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: + """Given an iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: + """Given an async iterator that yields raw binary data, iterate over it & yield every event encountered""" + ... + + +def is_stream_class_type(typ: type) -> TypeGuard[type[Stream[object]] | type[AsyncStream[object]]]: + """TypeGuard for determining whether or not the given type is a subclass of `Stream` / `AsyncStream`""" + origin = get_origin(typ) or typ + return inspect.isclass(origin) and issubclass(origin, (Stream, AsyncStream)) + + +def extract_stream_chunk_type( + stream_cls: type, + *, + failure_message: str | None = None, +) -> type: + """Given a type like `Stream[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyStream(Stream[bytes]): + ... + + extract_stream_chunk_type(MyStream) -> bytes + ``` + """ + from ._base_client import Stream, AsyncStream + + return extract_type_var_from_base( + stream_cls, + index=0, + generic_bases=cast("tuple[type, ...]", (Stream, AsyncStream)), + failure_message=failure_message, + ) diff --git a/src/vitable_connect/_types.py b/src/vitable_connect/_types.py new file mode 100644 index 0000000..9085b00 --- /dev/null +++ b/src/vitable_connect/_types.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + Dict, + List, + Type, + Tuple, + Union, + Mapping, + TypeVar, + Callable, + Iterable, + Iterator, + Optional, + Sequence, + AsyncIterable, +) +from typing_extensions import ( + Set, + Literal, + Protocol, + TypeAlias, + TypedDict, + SupportsIndex, + overload, + override, + runtime_checkable, +) + +import httpx +import pydantic +from httpx import URL, Proxy, Timeout, Response, BaseTransport, AsyncBaseTransport + +if TYPE_CHECKING: + from ._models import BaseModel, SecurityOptions + from ._response import APIResponse, AsyncAPIResponse + +Transport = BaseTransport +AsyncTransport = AsyncBaseTransport +Query = Mapping[str, object] +Body = object +AnyMapping = Mapping[str, object] +ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) +_T = TypeVar("_T") + +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + + +# Approximates httpx internal ProxiesTypes and RequestFiles types +# while adding support for `PathLike` instances +ProxiesDict = Dict["str | URL", Union[None, str, URL, Proxy]] +ProxiesTypes = Union[str, Proxy, ProxiesDict] +if TYPE_CHECKING: + Base64FileInput = Union[IO[bytes], PathLike[str]] + FileContent = Union[IO[bytes], bytes, PathLike[str]] +else: + Base64FileInput = Union[IO[bytes], PathLike] + FileContent = Union[IO[bytes], bytes, PathLike] # PathLike is not subscriptable in Python 3.8. + + +# Used for sending raw binary data / streaming data in request bodies +# e.g. for file uploads without multipart encoding +BinaryTypes = Union[bytes, bytearray, IO[bytes], Iterable[bytes]] +AsyncBinaryTypes = Union[bytes, bytearray, IO[bytes], AsyncIterable[bytes]] + +FileTypes = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], +] +RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]] + +# duplicate of the above but without our custom file support +HttpxFileContent = Union[IO[bytes], bytes] +HttpxFileTypes = Union[ + # file (or bytes) + HttpxFileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], HttpxFileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], HttpxFileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[Optional[str], HttpxFileContent, Optional[str], Mapping[str, str]], +] +HttpxRequestFiles = Union[Mapping[str, HttpxFileTypes], Sequence[Tuple[str, HttpxFileTypes]]] + +# Workaround to support (cast_to: Type[ResponseT]) -> ResponseT +# where ResponseT includes `None`. In order to support directly +# passing `None`, overloads would have to be defined for every +# method that uses `ResponseT` which would lead to an unacceptable +# amount of code duplication and make it unreadable. See _base_client.py +# for example usage. +# +# This unfortunately means that you will either have +# to import this type and pass it explicitly: +# +# from vitable_connect import NoneType +# client.get('/foo', cast_to=NoneType) +# +# or build it yourself: +# +# client.get('/foo', cast_to=type(None)) +if TYPE_CHECKING: + NoneType: Type[None] +else: + NoneType = type(None) + + +class RequestOptions(TypedDict, total=False): + headers: Headers + max_retries: int + timeout: float | Timeout | None + params: Query + extra_json: AnyMapping + idempotency_key: str + follow_redirects: bool + security: SecurityOptions + + +# Sentinel class used until PEP 0661 is accepted +class NotGiven: + """ + For parameters with a meaningful None value, we need to distinguish between + the user explicitly passing None, and the user not passing the parameter at + all. + + User code shouldn't need to use not_given directly. + + For example: + + ```py + def create(timeout: Timeout | None | NotGiven = not_given): ... + + + create(timeout=1) # 1s timeout + create(timeout=None) # No timeout + create() # Default timeout behavior + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + +not_given = NotGiven() +# for backwards compatibility: +NOT_GIVEN = NotGiven() + + +class Omit: + """ + To explicitly omit something from being sent in a request, use `omit`. + + ```py + # as the default `Content-Type` header is `application/json` that will be sent + client.post("/upload/files", files={"file": b"my raw file content"}) + + # you can't explicitly override the header as it has to be dynamically generated + # to look something like: 'multipart/form-data; boundary=0d8382fcf5f8c3be01ca2e11002d2983' + client.post(..., headers={"Content-Type": "multipart/form-data"}) + + # instead you can remove the default `application/json` header by passing omit + client.post(..., headers={"Content-Type": omit}) + ``` + """ + + def __bool__(self) -> Literal[False]: + return False + + +omit = Omit() + + +@runtime_checkable +class ModelBuilderProtocol(Protocol): + @classmethod + def build( + cls: type[_T], + *, + response: Response, + data: object, + ) -> _T: ... + + +Headers = Mapping[str, Union[str, Omit]] + + +class HeadersLikeProtocol(Protocol): + def get(self, __key: str) -> str | None: ... + + +HeadersLike = Union[Headers, HeadersLikeProtocol] + +ResponseT = TypeVar( + "ResponseT", + bound=Union[ + object, + str, + None, + "BaseModel", + List[Any], + Dict[str, Any], + Response, + ModelBuilderProtocol, + "APIResponse[Any]", + "AsyncAPIResponse[Any]", + ], +) + +StrBytesIntFloat = Union[str, bytes, int, float] + +# Note: copied from Pydantic +# https://github.com/pydantic/pydantic/blob/6f31f8f68ef011f84357330186f603ff295312fd/pydantic/main.py#L79 +IncEx: TypeAlias = Union[Set[int], Set[str], Mapping[int, Union["IncEx", bool]], Mapping[str, Union["IncEx", bool]]] + +PostParser = Callable[[Any], Any] + + +@runtime_checkable +class InheritsGeneric(Protocol): + """Represents a type that has inherited from `Generic` + + The `__orig_bases__` property can be used to determine the resolved + type variable for a given base class. + """ + + __orig_bases__: tuple[_GenericAlias] + + +class _GenericAlias(Protocol): + __origin__: type[object] + + +class HttpxSendArgs(TypedDict, total=False): + auth: httpx.Auth + follow_redirects: bool + + +_T_co = TypeVar("_T_co", covariant=True) + + +if TYPE_CHECKING: + # This works because str.__contains__ does not accept object (either in typeshed or at runtime) + # https://github.com/hauntsaninja/useful_types/blob/5e9710f3875107d068e7679fd7fec9cfab0eff3b/useful_types/__init__.py#L285 + # + # Note: index() and count() methods are intentionally omitted to allow pyright to properly + # infer TypedDict types when dict literals are used in lists assigned to SequenceNotStr. + class SequenceNotStr(Protocol[_T_co]): + @overload + def __getitem__(self, index: SupportsIndex, /) -> _T_co: ... + @overload + def __getitem__(self, index: slice, /) -> Sequence[_T_co]: ... + def __contains__(self, value: object, /) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[_T_co]: ... + def __reversed__(self) -> Iterator[_T_co]: ... +else: + # just point this to a normal `Sequence` at runtime to avoid having to special case + # deserializing our custom sequence type + SequenceNotStr = Sequence diff --git a/src/vitable_connect/_utils/__init__.py b/src/vitable_connect/_utils/__init__.py new file mode 100644 index 0000000..1c090e5 --- /dev/null +++ b/src/vitable_connect/_utils/__init__.py @@ -0,0 +1,64 @@ +from ._path import path_template as path_template +from ._sync import asyncify as asyncify +from ._proxy import LazyProxy as LazyProxy +from ._utils import ( + flatten as flatten, + is_dict as is_dict, + is_list as is_list, + is_given as is_given, + is_tuple as is_tuple, + json_safe as json_safe, + lru_cache as lru_cache, + is_mapping as is_mapping, + is_tuple_t as is_tuple_t, + is_iterable as is_iterable, + is_sequence as is_sequence, + coerce_float as coerce_float, + is_mapping_t as is_mapping_t, + removeprefix as removeprefix, + removesuffix as removesuffix, + extract_files as extract_files, + is_sequence_t as is_sequence_t, + required_args as required_args, + coerce_boolean as coerce_boolean, + coerce_integer as coerce_integer, + file_from_path as file_from_path, + strip_not_given as strip_not_given, + get_async_library as get_async_library, + maybe_coerce_float as maybe_coerce_float, + get_required_header as get_required_header, + maybe_coerce_boolean as maybe_coerce_boolean, + maybe_coerce_integer as maybe_coerce_integer, +) +from ._compat import ( + get_args as get_args, + is_union as is_union, + get_origin as get_origin, + is_typeddict as is_typeddict, + is_literal_type as is_literal_type, +) +from ._typing import ( + is_list_type as is_list_type, + is_union_type as is_union_type, + extract_type_arg as extract_type_arg, + is_iterable_type as is_iterable_type, + is_required_type as is_required_type, + is_sequence_type as is_sequence_type, + is_annotated_type as is_annotated_type, + is_type_alias_type as is_type_alias_type, + strip_annotated_type as strip_annotated_type, + extract_type_var_from_base as extract_type_var_from_base, +) +from ._streams import consume_sync_iterator as consume_sync_iterator, consume_async_iterator as consume_async_iterator +from ._transform import ( + PropertyInfo as PropertyInfo, + transform as transform, + async_transform as async_transform, + maybe_transform as maybe_transform, + async_maybe_transform as async_maybe_transform, +) +from ._reflection import ( + function_has_argument as function_has_argument, + assert_signatures_in_sync as assert_signatures_in_sync, +) +from ._datetime_parse import parse_date as parse_date, parse_datetime as parse_datetime diff --git a/src/vitable_connect/_utils/_compat.py b/src/vitable_connect/_utils/_compat.py new file mode 100644 index 0000000..2c70b29 --- /dev/null +++ b/src/vitable_connect/_utils/_compat.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import sys +import typing_extensions +from typing import Any, Type, Union, Literal, Optional +from datetime import date, datetime +from typing_extensions import get_args as _get_args, get_origin as _get_origin + +from .._types import StrBytesIntFloat +from ._datetime_parse import parse_date as _parse_date, parse_datetime as _parse_datetime + +_LITERAL_TYPES = {Literal, typing_extensions.Literal} + + +def get_args(tp: type[Any]) -> tuple[Any, ...]: + return _get_args(tp) + + +def get_origin(tp: type[Any]) -> type[Any] | None: + return _get_origin(tp) + + +def is_union(tp: Optional[Type[Any]]) -> bool: + if sys.version_info < (3, 10): + return tp is Union # type: ignore[comparison-overlap] + else: + import types + + return tp is Union or tp is types.UnionType # type: ignore[comparison-overlap] + + +def is_typeddict(tp: Type[Any]) -> bool: + return typing_extensions.is_typeddict(tp) + + +def is_literal_type(tp: Type[Any]) -> bool: + return get_origin(tp) in _LITERAL_TYPES + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + return _parse_date(value) + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + return _parse_datetime(value) diff --git a/src/vitable_connect/_utils/_datetime_parse.py b/src/vitable_connect/_utils/_datetime_parse.py new file mode 100644 index 0000000..7cb9d9e --- /dev/null +++ b/src/vitable_connect/_utils/_datetime_parse.py @@ -0,0 +1,136 @@ +""" +This file contains code from https://github.com/pydantic/pydantic/blob/main/pydantic/v1/datetime_parse.py +without the Pydantic v1 specific errors. +""" + +from __future__ import annotations + +import re +from typing import Dict, Union, Optional +from datetime import date, datetime, timezone, timedelta + +from .._types import StrBytesIntFloat + +date_expr = r"(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})" +time_expr = ( + r"(?P\d{1,2}):(?P\d{1,2})" + r"(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?" + r"(?PZ|[+-]\d{2}(?::?\d{2})?)?$" +) + +date_re = re.compile(f"{date_expr}$") +datetime_re = re.compile(f"{date_expr}[T ]{time_expr}") + + +EPOCH = datetime(1970, 1, 1) +# if greater than this, the number is in ms, if less than or equal it's in seconds +# (in seconds this is 11th October 2603, in ms it's 20th August 1970) +MS_WATERSHED = int(2e10) +# slightly more than datetime.max in ns - (datetime.max - EPOCH).total_seconds() * 1e9 +MAX_NUMBER = int(3e20) + + +def _get_numeric(value: StrBytesIntFloat, native_expected_type: str) -> Union[None, int, float]: + if isinstance(value, (int, float)): + return value + try: + return float(value) + except ValueError: + return None + except TypeError: + raise TypeError(f"invalid type; expected {native_expected_type}, string, bytes, int or float") from None + + +def _from_unix_seconds(seconds: Union[int, float]) -> datetime: + if seconds > MAX_NUMBER: + return datetime.max + elif seconds < -MAX_NUMBER: + return datetime.min + + while abs(seconds) > MS_WATERSHED: + seconds /= 1000 + dt = EPOCH + timedelta(seconds=seconds) + return dt.replace(tzinfo=timezone.utc) + + +def _parse_timezone(value: Optional[str]) -> Union[None, int, timezone]: + if value == "Z": + return timezone.utc + elif value is not None: + offset_mins = int(value[-2:]) if len(value) > 3 else 0 + offset = 60 * int(value[1:3]) + offset_mins + if value[0] == "-": + offset = -offset + return timezone(timedelta(minutes=offset)) + else: + return None + + +def parse_datetime(value: Union[datetime, StrBytesIntFloat]) -> datetime: + """ + Parse a datetime/int/float/string and return a datetime.datetime. + + This function supports time zone offsets. When the input contains one, + the output uses a timezone with a fixed offset from UTC. + + Raise ValueError if the input is well formatted but not a valid datetime. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, datetime): + return value + + number = _get_numeric(value, "datetime") + if number is not None: + return _from_unix_seconds(number) + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + + match = datetime_re.match(value) + if match is None: + raise ValueError("invalid datetime format") + + kw = match.groupdict() + if kw["microsecond"]: + kw["microsecond"] = kw["microsecond"].ljust(6, "0") + + tzinfo = _parse_timezone(kw.pop("tzinfo")) + kw_: Dict[str, Union[None, int, timezone]] = {k: int(v) for k, v in kw.items() if v is not None} + kw_["tzinfo"] = tzinfo + + return datetime(**kw_) # type: ignore + + +def parse_date(value: Union[date, StrBytesIntFloat]) -> date: + """ + Parse a date/int/float/string and return a datetime.date. + + Raise ValueError if the input is well formatted but not a valid date. + Raise ValueError if the input isn't well formatted. + """ + if isinstance(value, date): + if isinstance(value, datetime): + return value.date() + else: + return value + + number = _get_numeric(value, "date") + if number is not None: + return _from_unix_seconds(number).date() + + if isinstance(value, bytes): + value = value.decode() + + assert not isinstance(value, (float, int)) + match = date_re.match(value) + if match is None: + raise ValueError("invalid date format") + + kw = {k: int(v) for k, v in match.groupdict().items()} + + try: + return date(**kw) + except ValueError: + raise ValueError("invalid date format") from None diff --git a/src/vitable_connect/_utils/_json.py b/src/vitable_connect/_utils/_json.py new file mode 100644 index 0000000..6058421 --- /dev/null +++ b/src/vitable_connect/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/src/vitable_connect/_utils/_logs.py b/src/vitable_connect/_utils/_logs.py new file mode 100644 index 0000000..c0710a8 --- /dev/null +++ b/src/vitable_connect/_utils/_logs.py @@ -0,0 +1,25 @@ +import os +import logging + +logger: logging.Logger = logging.getLogger("vitable_connect") +httpx_logger: logging.Logger = logging.getLogger("httpx") + + +def _basic_config() -> None: + # e.g. [2023-10-05 14:12:26 - vitable_connect._base_client:818 - DEBUG] HTTP Request: POST http://127.0.0.1:4010/foo/bar "200 OK" + logging.basicConfig( + format="[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + + +def setup_logging() -> None: + env = os.environ.get("VITABLE_CONNECT_LOG") + if env == "debug": + _basic_config() + logger.setLevel(logging.DEBUG) + httpx_logger.setLevel(logging.DEBUG) + elif env == "info": + _basic_config() + logger.setLevel(logging.INFO) + httpx_logger.setLevel(logging.INFO) diff --git a/src/vitable_connect/_utils/_path.py b/src/vitable_connect/_utils/_path.py new file mode 100644 index 0000000..4d6e1e4 --- /dev/null +++ b/src/vitable_connect/_utils/_path.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +import re +from typing import ( + Any, + Mapping, + Callable, +) +from urllib.parse import quote + +# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E). +_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$") + +_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") + + +def _quote_path_segment_part(value: str) -> str: + """Percent-encode `value` for use in a URI path segment. + + Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.3 + """ + # quote() already treats unreserved characters (letters, digits, and -._~) + # as safe, so we only need to add sub-delims, ':', and '@'. + # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted. + return quote(value, safe="!$&'()*+,;=:@") + + +def _quote_query_part(value: str) -> str: + """Percent-encode `value` for use in a URI query string. + + Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.4 + """ + return quote(value, safe="!$'()*+,;:@/?") + + +def _quote_fragment_part(value: str) -> str: + """Percent-encode `value` for use in a URI fragment. + + Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe. + https://datatracker.ietf.org/doc/html/rfc3986#section-3.5 + """ + return quote(value, safe="!$&'()*+,;=:@/?") + + +def _interpolate( + template: str, + values: Mapping[str, Any], + quoter: Callable[[str], str], +) -> str: + """Replace {name} placeholders in `template`, quoting each value with `quoter`. + + Placeholder names are looked up in `values`. + + Raises: + KeyError: If a placeholder is not found in `values`. + """ + # re.split with a capturing group returns alternating + # [text, name, text, name, ..., text] elements. + parts = _PLACEHOLDER_RE.split(template) + + for i in range(1, len(parts), 2): + name = parts[i] + if name not in values: + raise KeyError(f"a value for placeholder {{{name}}} was not provided") + val = values[name] + if val is None: + parts[i] = "null" + elif isinstance(val, bool): + parts[i] = "true" if val else "false" + else: + parts[i] = quoter(str(values[name])) + + return "".join(parts) + + +def path_template(template: str, /, **kwargs: Any) -> str: + """Interpolate {name} placeholders in `template` from keyword arguments. + + Args: + template: The template string containing {name} placeholders. + **kwargs: Keyword arguments to interpolate into the template. + + Returns: + The template with placeholders interpolated and percent-encoded. + + Safe characters for percent-encoding are dependent on the URI component. + Placeholders in path and fragment portions are percent-encoded where the `segment` + and `fragment` sets from RFC 3986 respectively are considered safe. + Placeholders in the query portion are percent-encoded where the `query` set from + RFC 3986 §3.3 is considered safe except for = and & characters. + + Raises: + KeyError: If a placeholder is not found in `kwargs`. + ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments). + """ + # Split the template into path, query, and fragment portions. + fragment_template: str | None = None + query_template: str | None = None + + rest = template + if "#" in rest: + rest, fragment_template = rest.split("#", 1) + if "?" in rest: + rest, query_template = rest.split("?", 1) + path_template = rest + + # Interpolate each portion with the appropriate quoting rules. + path_result = _interpolate(path_template, kwargs, _quote_path_segment_part) + + # Reject dot-segments (. and ..) in the final assembled path. The check + # runs after interpolation so that adjacent placeholders or a mix of static + # text and placeholders that together form a dot-segment are caught. + # Also reject percent-encoded dot-segments to protect against incorrectly + # implemented normalization in servers/proxies. + for segment in path_result.split("/"): + if _DOT_SEGMENT_RE.match(segment): + raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed") + + result = path_result + if query_template is not None: + result += "?" + _interpolate(query_template, kwargs, _quote_query_part) + if fragment_template is not None: + result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part) + + return result diff --git a/src/vitable_connect/_utils/_proxy.py b/src/vitable_connect/_utils/_proxy.py new file mode 100644 index 0000000..0f239a3 --- /dev/null +++ b/src/vitable_connect/_utils/_proxy.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Generic, TypeVar, Iterable, cast +from typing_extensions import override + +T = TypeVar("T") + + +class LazyProxy(Generic[T], ABC): + """Implements data methods to pretend that an instance is another instance. + + This includes forwarding attribute access and other methods. + """ + + # Note: we have to special case proxies that themselves return proxies + # to support using a proxy as a catch-all for any random access, e.g. `proxy.foo.bar.baz` + + def __getattr__(self, attr: str) -> object: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied # pyright: ignore + return getattr(proxied, attr) + + @override + def __repr__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return repr(self.__get_proxied__()) + + @override + def __str__(self) -> str: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return proxied.__class__.__name__ + return str(proxied) + + @override + def __dir__(self) -> Iterable[str]: + proxied = self.__get_proxied__() + if isinstance(proxied, LazyProxy): + return [] + return proxied.__dir__() + + @property # type: ignore + @override + def __class__(self) -> type: # pyright: ignore + try: + proxied = self.__get_proxied__() + except Exception: + return type(self) + if issubclass(type(proxied), LazyProxy): + return type(proxied) + return proxied.__class__ + + def __get_proxied__(self) -> T: + return self.__load__() + + def __as_proxied__(self) -> T: + """Helper method that returns the current proxy, typed as the loaded object""" + return cast(T, self) + + @abstractmethod + def __load__(self) -> T: ... diff --git a/src/vitable_connect/_utils/_reflection.py b/src/vitable_connect/_utils/_reflection.py new file mode 100644 index 0000000..89aa712 --- /dev/null +++ b/src/vitable_connect/_utils/_reflection.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +import inspect +from typing import Any, Callable + + +def function_has_argument(func: Callable[..., Any], arg_name: str) -> bool: + """Returns whether or not the given function has a specific parameter""" + sig = inspect.signature(func) + return arg_name in sig.parameters + + +def assert_signatures_in_sync( + source_func: Callable[..., Any], + check_func: Callable[..., Any], + *, + exclude_params: set[str] = set(), +) -> None: + """Ensure that the signature of the second function matches the first.""" + + check_sig = inspect.signature(check_func) + source_sig = inspect.signature(source_func) + + errors: list[str] = [] + + for name, source_param in source_sig.parameters.items(): + if name in exclude_params: + continue + + custom_param = check_sig.parameters.get(name) + if not custom_param: + errors.append(f"the `{name}` param is missing") + continue + + if custom_param.annotation != source_param.annotation: + errors.append( + f"types for the `{name}` param are do not match; source={repr(source_param.annotation)} checking={repr(custom_param.annotation)}" + ) + continue + + if errors: + raise AssertionError(f"{len(errors)} errors encountered when comparing signatures:\n\n" + "\n\n".join(errors)) diff --git a/src/vitable_connect/_utils/_resources_proxy.py b/src/vitable_connect/_utils/_resources_proxy.py new file mode 100644 index 0000000..59bd5e6 --- /dev/null +++ b/src/vitable_connect/_utils/_resources_proxy.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from typing import Any +from typing_extensions import override + +from ._proxy import LazyProxy + + +class ResourcesProxy(LazyProxy[Any]): + """A proxy for the `vitable_connect.resources` module. + + This is used so that we can lazily import `vitable_connect.resources` only when + needed *and* so that users can just import `vitable_connect` and reference `vitable_connect.resources` + """ + + @override + def __load__(self) -> Any: + import importlib + + mod = importlib.import_module("vitable_connect.resources") + return mod + + +resources = ResourcesProxy().__as_proxied__() diff --git a/src/vitable_connect/_utils/_streams.py b/src/vitable_connect/_utils/_streams.py new file mode 100644 index 0000000..f4a0208 --- /dev/null +++ b/src/vitable_connect/_utils/_streams.py @@ -0,0 +1,12 @@ +from typing import Any +from typing_extensions import Iterator, AsyncIterator + + +def consume_sync_iterator(iterator: Iterator[Any]) -> None: + for _ in iterator: + ... + + +async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None: + async for _ in iterator: + ... diff --git a/src/vitable_connect/_utils/_sync.py b/src/vitable_connect/_utils/_sync.py new file mode 100644 index 0000000..f6027c1 --- /dev/null +++ b/src/vitable_connect/_utils/_sync.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import asyncio +import functools +from typing import TypeVar, Callable, Awaitable +from typing_extensions import ParamSpec + +import anyio +import sniffio +import anyio.to_thread + +T_Retval = TypeVar("T_Retval") +T_ParamSpec = ParamSpec("T_ParamSpec") + + +async def to_thread( + func: Callable[T_ParamSpec, T_Retval], /, *args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs +) -> T_Retval: + if sniffio.current_async_library() == "asyncio": + return await asyncio.to_thread(func, *args, **kwargs) + + return await anyio.to_thread.run_sync( + functools.partial(func, *args, **kwargs), + ) + + +# inspired by `asyncer`, https://github.com/tiangolo/asyncer +def asyncify(function: Callable[T_ParamSpec, T_Retval]) -> Callable[T_ParamSpec, Awaitable[T_Retval]]: + """ + Take a blocking function and create an async one that receives the same + positional and keyword arguments. + + Usage: + + ```python + def blocking_func(arg1, arg2, kwarg1=None): + # blocking code + return result + + + result = asyncify(blocking_function)(arg1, arg2, kwarg1=value1) + ``` + + ## Arguments + + `function`: a blocking regular callable (e.g. a function) + + ## Return + + An async function that takes the same positional and keyword arguments as the + original one, that when called runs the same original function in a thread worker + and returns the result. + """ + + async def wrapper(*args: T_ParamSpec.args, **kwargs: T_ParamSpec.kwargs) -> T_Retval: + return await to_thread(function, *args, **kwargs) + + return wrapper diff --git a/src/vitable_connect/_utils/_transform.py b/src/vitable_connect/_utils/_transform.py new file mode 100644 index 0000000..5207549 --- /dev/null +++ b/src/vitable_connect/_utils/_transform.py @@ -0,0 +1,457 @@ +from __future__ import annotations + +import io +import base64 +import pathlib +from typing import Any, Mapping, TypeVar, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints + +import anyio +import pydantic + +from ._utils import ( + is_list, + is_given, + lru_cache, + is_mapping, + is_iterable, + is_sequence, +) +from .._files import is_base64_file_input +from ._compat import get_origin, is_typeddict +from ._typing import ( + is_list_type, + is_union_type, + extract_type_arg, + is_iterable_type, + is_required_type, + is_sequence_type, + is_annotated_type, + strip_annotated_type, +) + +_T = TypeVar("_T") + + +# TODO: support for drilling globals() and locals() +# TODO: ensure works correctly with forward references in all cases + + +PropertyFormat = Literal["iso8601", "base64", "custom"] + + +class PropertyInfo: + """Metadata class to be used in Annotated types to provide information about a given type. + + For example: + + class MyParams(TypedDict): + account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')] + + This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API. + """ + + alias: str | None + format: PropertyFormat | None + format_template: str | None + discriminator: str | None + + def __init__( + self, + *, + alias: str | None = None, + format: PropertyFormat | None = None, + format_template: str | None = None, + discriminator: str | None = None, + ) -> None: + self.alias = alias + self.format = format + self.format_template = format_template + self.discriminator = discriminator + + @override + def __repr__(self) -> str: + return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')" + + +def maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `transform()` that allows `None` to be passed. + + See `transform()` for more details. + """ + if data is None: + return None + return transform(data, expected_type) + + +# Wrapper over _transform_recursive providing fake types +def transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = _transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +@lru_cache(maxsize=8096) +def _get_annotated_type(type_: type) -> type | None: + """If the given type is an `Annotated` type then it is returned, if not `None` is returned. + + This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]` + """ + if is_required_type(type_): + # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]` + type_ = get_args(type_)[0] + + if is_annotated_type(type_): + return type_ + + return None + + +def _maybe_transform_key(key: str, type_: type) -> str: + """Transform the given `data` based on the annotations provided in `type_`. + + Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata. + """ + annotated_type = _get_annotated_type(type_) + if annotated_type is None: + # no `Annotated` definition for this type, no transformation needed + return key + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.alias is not None: + return annotation.alias + + return key + + +def _no_transform_needed(annotation: type) -> bool: + return annotation == float or annotation == int + + +def _transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + from .._compat import model_dump + + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type + if is_typeddict(stripped_type) and is_mapping(data): + return _transform_typeddict(data, stripped_type) + + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) + ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + + inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + + return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = _transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True, mode="json") + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return _format_data(data, annotation.format, annotation.format_template) + + return data + + +def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = data.read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +def _transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + if not is_given(value): + # we don't need to include omitted values here as they'll + # be stripped out before the request is sent anyway + continue + + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_) + return result + + +async def async_maybe_transform( + data: object, + expected_type: object, +) -> Any | None: + """Wrapper over `async_transform()` that allows `None` to be passed. + + See `async_transform()` for more details. + """ + if data is None: + return None + return await async_transform(data, expected_type) + + +async def async_transform( + data: _T, + expected_type: object, +) -> _T: + """Transform dictionaries based off of type information from the given type, for example: + + ```py + class Params(TypedDict, total=False): + card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]] + + + transformed = transform({"card_id": ""}, Params) + # {'cardID': ''} + ``` + + Any keys / data that does not have type information given will be included as is. + + It should be noted that the transformations that this function does are not represented in the type system. + """ + transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type)) + return cast(_T, transformed) + + +async def _async_transform_recursive( + data: object, + *, + annotation: type, + inner_type: type | None = None, +) -> object: + """Transform the given data against the expected type. + + Args: + annotation: The direct type annotation given to the particular piece of data. + This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc + + inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type + is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in + the list can be transformed using the metadata from the container type. + + Defaults to the same value as the `annotation` argument. + """ + from .._compat import model_dump + + if inner_type is None: + inner_type = annotation + + stripped_type = strip_annotated_type(inner_type) + origin = get_origin(stripped_type) or stripped_type + if is_typeddict(stripped_type) and is_mapping(data): + return await _async_transform_typeddict(data, stripped_type) + + if origin == dict and is_mapping(data): + items_type = get_args(stripped_type)[1] + return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()} + + if ( + # List[T] + (is_list_type(stripped_type) and is_list(data)) + # Iterable[T] + or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str)) + # Sequence[T] + or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str)) + ): + # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually + # intended as an iterable, so we don't transform it. + if isinstance(data, dict): + return cast(object, data) + + inner_type = extract_type_arg(stripped_type, 0) + if _no_transform_needed(inner_type): + # for some types there is no need to transform anything, so we can get a small + # perf boost from skipping that work. + # + # but we still need to convert to a list to ensure the data is json-serializable + if is_list(data): + return data + return list(data) + + return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data] + + if is_union_type(stripped_type): + # For union types we run the transformation against all subtypes to ensure that everything is transformed. + # + # TODO: there may be edge cases where the same normalized field name will transform to two different names + # in different subtypes. + for subtype in get_args(stripped_type): + data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype) + return data + + if isinstance(data, pydantic.BaseModel): + return model_dump(data, exclude_unset=True, mode="json") + + annotated_type = _get_annotated_type(annotation) + if annotated_type is None: + return data + + # ignore the first argument as it is the actual type + annotations = get_args(annotated_type)[1:] + for annotation in annotations: + if isinstance(annotation, PropertyInfo) and annotation.format is not None: + return await _async_format_data(data, annotation.format, annotation.format_template) + + return data + + +async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object: + if isinstance(data, (date, datetime)): + if format_ == "iso8601": + return data.isoformat() + + if format_ == "custom" and format_template is not None: + return data.strftime(format_template) + + if format_ == "base64" and is_base64_file_input(data): + binary: str | bytes | None = None + + if isinstance(data, pathlib.Path): + binary = await anyio.Path(data).read_bytes() + elif isinstance(data, io.IOBase): + binary = data.read() + + if isinstance(binary, str): # type: ignore[unreachable] + binary = binary.encode() + + if not isinstance(binary, bytes): + raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}") + + return base64.b64encode(binary).decode("ascii") + + return data + + +async def _async_transform_typeddict( + data: Mapping[str, object], + expected_type: type, +) -> Mapping[str, object]: + result: dict[str, object] = {} + annotations = get_type_hints(expected_type, include_extras=True) + for key, value in data.items(): + if not is_given(value): + # we don't need to include omitted values here as they'll + # be stripped out before the request is sent anyway + continue + + type_ = annotations.get(key) + if type_ is None: + # we do not have a type annotation for this field, leave it as is + result[key] = value + else: + result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_) + return result + + +@lru_cache(maxsize=8096) +def get_type_hints( + obj: Any, + globalns: dict[str, Any] | None = None, + localns: Mapping[str, Any] | None = None, + include_extras: bool = False, +) -> dict[str, Any]: + return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras) diff --git a/src/vitable_connect/_utils/_typing.py b/src/vitable_connect/_utils/_typing.py new file mode 100644 index 0000000..193109f --- /dev/null +++ b/src/vitable_connect/_utils/_typing.py @@ -0,0 +1,156 @@ +from __future__ import annotations + +import sys +import typing +import typing_extensions +from typing import Any, TypeVar, Iterable, cast +from collections import abc as _c_abc +from typing_extensions import ( + TypeIs, + Required, + Annotated, + get_args, + get_origin, +) + +from ._utils import lru_cache +from .._types import InheritsGeneric +from ._compat import is_union as _is_union + + +def is_annotated_type(typ: type) -> bool: + return get_origin(typ) == Annotated + + +def is_list_type(typ: type) -> bool: + return (get_origin(typ) or typ) == list + + +def is_sequence_type(typ: type) -> bool: + origin = get_origin(typ) or typ + return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence + + +def is_iterable_type(typ: type) -> bool: + """If the given type is `typing.Iterable[T]`""" + origin = get_origin(typ) or typ + return origin == Iterable or origin == _c_abc.Iterable + + +def is_union_type(typ: type) -> bool: + return _is_union(get_origin(typ)) + + +def is_required_type(typ: type) -> bool: + return get_origin(typ) == Required + + +def is_typevar(typ: type) -> bool: + # type ignore is required because type checkers + # think this expression will always return False + return type(typ) == TypeVar # type: ignore + + +_TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,) +if sys.version_info >= (3, 12): + _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType) + + +def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]: + """Return whether the provided argument is an instance of `TypeAliasType`. + + ```python + type Int = int + is_type_alias_type(Int) + # > True + Str = TypeAliasType("Str", str) + is_type_alias_type(Str) + # > True + ``` + """ + return isinstance(tp, _TYPE_ALIAS_TYPES) + + +# Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]] +@lru_cache(maxsize=8096) +def strip_annotated_type(typ: type) -> type: + if is_required_type(typ) or is_annotated_type(typ): + return strip_annotated_type(cast(type, get_args(typ)[0])) + + return typ + + +def extract_type_arg(typ: type, index: int) -> type: + args = get_args(typ) + try: + return cast(type, args[index]) + except IndexError as err: + raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err + + +def extract_type_var_from_base( + typ: type, + *, + generic_bases: tuple[type, ...], + index: int, + failure_message: str | None = None, +) -> type: + """Given a type like `Foo[T]`, returns the generic type variable `T`. + + This also handles the case where a concrete subclass is given, e.g. + ```py + class MyResponse(Foo[bytes]): + ... + + extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes + ``` + + And where a generic subclass is given: + ```py + _T = TypeVar('_T') + class MyResponse(Foo[_T]): + ... + + extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes + ``` + """ + cls = cast(object, get_origin(typ) or typ) + if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains] + # we're given the class directly + return extract_type_arg(typ, index) + + # if a subclass is given + # --- + # this is needed as __orig_bases__ is not present in the typeshed stubs + # because it is intended to be for internal use only, however there does + # not seem to be a way to resolve generic TypeVars for inherited subclasses + # without using it. + if isinstance(cls, InheritsGeneric): + target_base_class: Any | None = None + for base in cls.__orig_bases__: + if base.__origin__ in generic_bases: + target_base_class = base + break + + if target_base_class is None: + raise RuntimeError( + "Could not find the generic base class;\n" + "This should never happen;\n" + f"Does {cls} inherit from one of {generic_bases} ?" + ) + + extracted = extract_type_arg(target_base_class, index) + if is_typevar(extracted): + # If the extracted type argument is itself a type variable + # then that means the subclass itself is generic, so we have + # to resolve the type argument from the class itself, not + # the base class. + # + # Note: if there is more than 1 type argument, the subclass could + # change the ordering of the type arguments, this is not currently + # supported. + return extract_type_arg(typ, index) + + return extracted + + raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}") diff --git a/src/vitable_connect/_utils/_utils.py b/src/vitable_connect/_utils/_utils.py new file mode 100644 index 0000000..199cd23 --- /dev/null +++ b/src/vitable_connect/_utils/_utils.py @@ -0,0 +1,433 @@ +from __future__ import annotations + +import os +import re +import inspect +import functools +from typing import ( + Any, + Tuple, + Mapping, + TypeVar, + Callable, + Iterable, + Sequence, + cast, + overload, +) +from pathlib import Path +from datetime import date, datetime +from typing_extensions import TypeGuard, get_args + +import sniffio + +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike + +_T = TypeVar("_T") +_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) +_MappingT = TypeVar("_MappingT", bound=Mapping[str, object]) +_SequenceT = TypeVar("_SequenceT", bound=Sequence[object]) +CallableT = TypeVar("CallableT", bound=Callable[..., Any]) + + +def flatten(t: Iterable[Iterable[_T]]) -> list[_T]: + return [item for sublist in t for item in sublist] + + +def extract_files( + # TODO: this needs to take Dict but variance issues..... + # create protocol type ? + query: Mapping[str, object], + *, + paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", +) -> list[tuple[str, FileTypes]]: + """Recursively extract files from the given dictionary based on specified paths. + + A path may look like this ['foo', 'files', '', 'data']. + + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + + Note: this mutates the given dictionary. + """ + files: list[tuple[str, FileTypes]] = [] + for path in paths: + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) + return files + + +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + +def _extract_items( + obj: object, + path: Sequence[str], + *, + index: int, + flattened_key: str | None, + array_format: ArrayFormat, +) -> list[tuple[str, FileTypes]]: + try: + key = path[index] + except IndexError: + if not is_given(obj): + # no value was provided - we can safely ignore + return [] + + # cyclical import + from .._files import assert_is_file_content + + # We have exhausted the path, return the entry we found. + assert flattened_key is not None + + if is_list(obj): + files: list[tuple[str, FileTypes]] = [] + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) + return files + + assert_is_file_content(obj, key=flattened_key) + return [(flattened_key, cast(FileTypes, obj))] + + index += 1 + if is_dict(obj): + try: + # Remove the field if there are no more dict keys in the path, + # only "" traversal markers or end. + if all(p == "" for p in path[index:]): + item = obj.pop(key) + else: + item = obj[key] + except KeyError: + # Key was not present in the dictionary, this is not indicative of an error + # as the given path may not point to a required field. We also do not want + # to enforce required fields as the API may differ from the spec in some cases. + return [] + if flattened_key is None: + flattened_key = key + else: + flattened_key += f"[{key}]" + return _extract_items( + item, + path, + index=index, + flattened_key=flattened_key, + array_format=array_format, + ) + elif is_list(obj): + if key != "": + return [] + + return flatten( + [ + _extract_items( + item, + path, + index=index, + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, + ) + for array_index, item in enumerate(obj) + ] + ) + + # Something unexpected was passed, just ignore it. + return [] + + +def is_given(obj: _T | NotGiven | Omit) -> TypeGuard[_T]: + return not isinstance(obj, NotGiven) and not isinstance(obj, Omit) + + +# Type safe methods for narrowing types with TypeVars. +# The default narrowing for isinstance(obj, dict) is dict[unknown, unknown], +# however this cause Pyright to rightfully report errors. As we know we don't +# care about the contained types we can safely use `object` in its place. +# +# There are two separate functions defined, `is_*` and `is_*_t` for different use cases. +# `is_*` is for when you're dealing with an unknown input +# `is_*_t` is for when you're narrowing a known union type to a specific subset + + +def is_tuple(obj: object) -> TypeGuard[tuple[object, ...]]: + return isinstance(obj, tuple) + + +def is_tuple_t(obj: _TupleT | object) -> TypeGuard[_TupleT]: + return isinstance(obj, tuple) + + +def is_sequence(obj: object) -> TypeGuard[Sequence[object]]: + return isinstance(obj, Sequence) + + +def is_sequence_t(obj: _SequenceT | object) -> TypeGuard[_SequenceT]: + return isinstance(obj, Sequence) + + +def is_mapping(obj: object) -> TypeGuard[Mapping[str, object]]: + return isinstance(obj, Mapping) + + +def is_mapping_t(obj: _MappingT | object) -> TypeGuard[_MappingT]: + return isinstance(obj, Mapping) + + +def is_dict(obj: object) -> TypeGuard[dict[object, object]]: + return isinstance(obj, dict) + + +def is_list(obj: object) -> TypeGuard[list[object]]: + return isinstance(obj, list) + + +def is_iterable(obj: object) -> TypeGuard[Iterable[object]]: + return isinstance(obj, Iterable) + + +# copied from https://github.com/Rapptz/RoboDanny +def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str: + size = len(seq) + if size == 0: + return "" + + if size == 1: + return seq[0] + + if size == 2: + return f"{seq[0]} {final} {seq[1]}" + + return delim.join(seq[:-1]) + f" {final} {seq[-1]}" + + +def quote(string: str) -> str: + """Add single quotation marks around the given string. Does *not* do any escaping.""" + return f"'{string}'" + + +def required_args(*variants: Sequence[str]) -> Callable[[CallableT], CallableT]: + """Decorator to enforce a given set of arguments or variants of arguments are passed to the decorated function. + + Useful for enforcing runtime validation of overloaded functions. + + Example usage: + ```py + @overload + def foo(*, a: str) -> str: ... + + + @overload + def foo(*, b: bool) -> str: ... + + + # This enforces the same constraints that a static type checker would + # i.e. that either a or b must be passed to the function + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: bool | None = None) -> str: ... + ``` + """ + + def inner(func: CallableT) -> CallableT: + params = inspect.signature(func).parameters + positional = [ + name + for name, param in params.items() + if param.kind + in { + param.POSITIONAL_ONLY, + param.POSITIONAL_OR_KEYWORD, + } + ] + + @functools.wraps(func) + def wrapper(*args: object, **kwargs: object) -> object: + given_params: set[str] = set() + for i, _ in enumerate(args): + try: + given_params.add(positional[i]) + except IndexError: + raise TypeError( + f"{func.__name__}() takes {len(positional)} argument(s) but {len(args)} were given" + ) from None + + for key in kwargs.keys(): + given_params.add(key) + + for variant in variants: + matches = all((param in given_params for param in variant)) + if matches: + break + else: # no break + if len(variants) > 1: + variations = human_join( + ["(" + human_join([quote(arg) for arg in variant], final="and") + ")" for variant in variants] + ) + msg = f"Missing required arguments; Expected either {variations} arguments to be given" + else: + assert len(variants) > 0 + + # TODO: this error message is not deterministic + missing = list(set(variants[0]) - given_params) + if len(missing) > 1: + msg = f"Missing required arguments: {human_join([quote(arg) for arg in missing])}" + else: + msg = f"Missing required argument: {quote(missing[0])}" + raise TypeError(msg) + return func(*args, **kwargs) + + return wrapper # type: ignore + + return inner + + +_K = TypeVar("_K") +_V = TypeVar("_V") + + +@overload +def strip_not_given(obj: None) -> None: ... + + +@overload +def strip_not_given(obj: Mapping[_K, _V | NotGiven]) -> dict[_K, _V]: ... + + +@overload +def strip_not_given(obj: object) -> object: ... + + +def strip_not_given(obj: object | None) -> object: + """Remove all top-level keys where their values are instances of `NotGiven`""" + if obj is None: + return None + + if not is_mapping(obj): + return obj + + return {key: value for key, value in obj.items() if not isinstance(value, NotGiven)} + + +def coerce_integer(val: str) -> int: + return int(val, base=10) + + +def coerce_float(val: str) -> float: + return float(val) + + +def coerce_boolean(val: str) -> bool: + return val == "true" or val == "1" or val == "on" + + +def maybe_coerce_integer(val: str | None) -> int | None: + if val is None: + return None + return coerce_integer(val) + + +def maybe_coerce_float(val: str | None) -> float | None: + if val is None: + return None + return coerce_float(val) + + +def maybe_coerce_boolean(val: str | None) -> bool | None: + if val is None: + return None + return coerce_boolean(val) + + +def removeprefix(string: str, prefix: str) -> str: + """Remove a prefix from a string. + + Backport of `str.removeprefix` for Python < 3.9 + """ + if string.startswith(prefix): + return string[len(prefix) :] + return string + + +def removesuffix(string: str, suffix: str) -> str: + """Remove a suffix from a string. + + Backport of `str.removesuffix` for Python < 3.9 + """ + if string.endswith(suffix): + return string[: -len(suffix)] + return string + + +def file_from_path(path: str) -> FileTypes: + contents = Path(path).read_bytes() + file_name = os.path.basename(path) + return (file_name, contents) + + +def get_required_header(headers: HeadersLike, header: str) -> str: + lower_header = header.lower() + if is_mapping_t(headers): + # mypy doesn't understand the type narrowing here + for k, v in headers.items(): # type: ignore + if k.lower() == lower_header and isinstance(v, str): + return v + + # to deal with the case where the header looks like Stainless-Event-Id + intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize()) + + for normalized_header in [header, lower_header, header.upper(), intercaps_header]: + value = headers.get(normalized_header) + if value: + return value + + raise ValueError(f"Could not find {header} header") + + +def get_async_library() -> str: + try: + return sniffio.current_async_library() + except Exception: + return "false" + + +def lru_cache(*, maxsize: int | None = 128) -> Callable[[CallableT], CallableT]: + """A version of functools.lru_cache that retains the type signature + for the wrapped function arguments. + """ + wrapper = functools.lru_cache( # noqa: TID251 + maxsize=maxsize, + ) + return cast(Any, wrapper) # type: ignore[no-any-return] + + +def json_safe(data: object) -> object: + """Translates a mapping / sequence recursively in the same fashion + as `pydantic` v2's `model_dump(mode="json")`. + """ + if is_mapping(data): + return {json_safe(key): json_safe(value) for key, value in data.items()} + + if is_iterable(data) and not isinstance(data, (str, bytes, bytearray)): + return [json_safe(item) for item in data] + + if isinstance(data, (datetime, date)): + return data.isoformat() + + return data diff --git a/src/vitable_connect/_version.py b/src/vitable_connect/_version.py new file mode 100644 index 0000000..f000ac5 --- /dev/null +++ b/src/vitable_connect/_version.py @@ -0,0 +1,4 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +__title__ = "vitable_connect" +__version__ = "0.9.0" # x-release-please-version diff --git a/src/vitable_connect/auth/__init__.py b/src/vitable_connect/auth/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/auth/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/auth/client.py b/src/vitable_connect/auth/client.py deleted file mode 100644 index 705a51e..0000000 --- a/src/vitable_connect/auth/client.py +++ /dev/null @@ -1,141 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from ..types.access_token_response import AccessTokenResponse -from ..types.bound_entity import BoundEntity -from ..types.grant_type import GrantType -from .raw_client import AsyncRawAuthClient, RawAuthClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class AuthClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawAuthClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawAuthClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawAuthClient - """ - return self._raw_client - - def issue_access_token( - self, - *, - grant_type: GrantType, - bound_entity: typing.Optional[BoundEntity] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AccessTokenResponse: - """ - Issues a short-lived access token from the authenticated API key. Access tokens can optionally be bound to a specific employer or employee for scoped access. Tokens expire after 15 minutes. - - Parameters - ---------- - grant_type : GrantType - Token issuance flow. Currently only 'client_credentials' supported. - - * `client_credentials` - client_credentials - - bound_entity : typing.Optional[BoundEntity] - Optional entity to bind the token to for scoped access - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AccessTokenResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.auth.issue_access_token( - grant_type="client_credentials", - ) - """ - _response = self._raw_client.issue_access_token( - grant_type=grant_type, bound_entity=bound_entity, request_options=request_options - ) - return _response.data - - -class AsyncAuthClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawAuthClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawAuthClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawAuthClient - """ - return self._raw_client - - async def issue_access_token( - self, - *, - grant_type: GrantType, - bound_entity: typing.Optional[BoundEntity] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AccessTokenResponse: - """ - Issues a short-lived access token from the authenticated API key. Access tokens can optionally be bound to a specific employer or employee for scoped access. Tokens expire after 15 minutes. - - Parameters - ---------- - grant_type : GrantType - Token issuance flow. Currently only 'client_credentials' supported. - - * `client_credentials` - client_credentials - - bound_entity : typing.Optional[BoundEntity] - Optional entity to bind the token to for scoped access - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AccessTokenResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.auth.issue_access_token( - grant_type="client_credentials", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.issue_access_token( - grant_type=grant_type, bound_entity=bound_entity, request_options=request_options - ) - return _response.data diff --git a/src/vitable_connect/auth/raw_client.py b/src/vitable_connect/auth/raw_client.py deleted file mode 100644 index e861566..0000000 --- a/src/vitable_connect/auth/raw_client.py +++ /dev/null @@ -1,338 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..core.serialization import convert_and_respect_annotation_metadata -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..types.access_token_response import AccessTokenResponse -from ..types.bound_entity import BoundEntity -from ..types.error_response import ErrorResponse -from ..types.grant_type import GrantType -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawAuthClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def issue_access_token( - self, - *, - grant_type: GrantType, - bound_entity: typing.Optional[BoundEntity] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[AccessTokenResponse]: - """ - Issues a short-lived access token from the authenticated API key. Access tokens can optionally be bound to a specific employer or employee for scoped access. Tokens expire after 15 minutes. - - Parameters - ---------- - grant_type : GrantType - Token issuance flow. Currently only 'client_credentials' supported. - - * `client_credentials` - client_credentials - - bound_entity : typing.Optional[BoundEntity] - Optional entity to bind the token to for scoped access - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[AccessTokenResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - "v1/auth/access-tokens", - method="POST", - json={ - "grant_type": grant_type, - "bound_entity": convert_and_respect_annotation_metadata( - object_=bound_entity, annotation=typing.Optional[BoundEntity], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - AccessTokenResponse, - parse_obj_as( - type_=AccessTokenResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawAuthClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def issue_access_token( - self, - *, - grant_type: GrantType, - bound_entity: typing.Optional[BoundEntity] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[AccessTokenResponse]: - """ - Issues a short-lived access token from the authenticated API key. Access tokens can optionally be bound to a specific employer or employee for scoped access. Tokens expire after 15 minutes. - - Parameters - ---------- - grant_type : GrantType - Token issuance flow. Currently only 'client_credentials' supported. - - * `client_credentials` - client_credentials - - bound_entity : typing.Optional[BoundEntity] - Optional entity to bind the token to for scoped access - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[AccessTokenResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/auth/access-tokens", - method="POST", - json={ - "grant_type": grant_type, - "bound_entity": convert_and_respect_annotation_metadata( - object_=bound_entity, annotation=typing.Optional[BoundEntity], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - AccessTokenResponse, - parse_obj_as( - type_=AccessTokenResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/client.py b/src/vitable_connect/client.py deleted file mode 100644 index 8b09059..0000000 --- a/src/vitable_connect/client.py +++ /dev/null @@ -1,395 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import os -import typing - -import httpx -from .core.api_error import ApiError -from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from .core.logging import LogConfig, Logger -from .environment import VitableConnectEnvironment - -if typing.TYPE_CHECKING: - from .auth.client import AsyncAuthClient, AuthClient - from .employees.client import AsyncEmployeesClient, EmployeesClient - from .employers.client import AsyncEmployersClient, EmployersClient - from .enrollments.client import AsyncEnrollmentsClient, EnrollmentsClient - from .groups.client import AsyncGroupsClient, GroupsClient - from .members.client import AsyncMembersClient, MembersClient - from .organizations.client import AsyncOrganizationsClient, OrganizationsClient - from .plans.client import AsyncPlansClient, PlansClient - from .webhook_events.client import AsyncWebhookEventsClient, WebhookEventsClient - - -class VitableConnect: - """ - Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. - - Parameters - ---------- - base_url : typing.Optional[str] - The base url to use for requests from the client. - - environment : VitableConnectEnvironment - The environment to use for requests from the client. from .environment import VitableConnectEnvironment - - - - Defaults to VitableConnectEnvironment.PRODUCTION - - - - api_key : typing.Optional[typing.Union[str, typing.Callable[[], str]]] - headers : typing.Optional[typing.Dict[str, str]] - Additional headers to send with every request. - - timeout : typing.Optional[float] - The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. - - max_retries : typing.Optional[int] - The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. - - stream_reconnection_enabled : typing.Optional[bool] - Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. - - max_stream_reconnection_attempts : typing.Optional[int] - The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. - - follow_redirects : typing.Optional[bool] - Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. - - httpx_client : typing.Optional[httpx.Client] - The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. - - logging : typing.Optional[typing.Union[LogConfig, Logger]] - Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - """ - - def __init__( - self, - *, - base_url: typing.Optional[str] = None, - environment: VitableConnectEnvironment = VitableConnectEnvironment.PRODUCTION, - api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("VITABLE_CONNECT_API_KEY"), - headers: typing.Optional[typing.Dict[str, str]] = None, - timeout: typing.Optional[float] = None, - max_retries: typing.Optional[int] = None, - stream_reconnection_enabled: typing.Optional[bool] = None, - max_stream_reconnection_attempts: typing.Optional[int] = None, - follow_redirects: typing.Optional[bool] = True, - httpx_client: typing.Optional[httpx.Client] = None, - logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, - ): - _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None - _defaulted_max_retries = max_retries if max_retries is not None else 2 - if api_key is None: - raise ApiError( - body="The client must be instantiated be either passing in api_key or setting VITABLE_CONNECT_API_KEY" - ) - self._client_wrapper = SyncClientWrapper( - base_url=_get_base_url(base_url=base_url, environment=environment), - api_key=api_key, - headers=headers, - httpx_client=httpx_client - if httpx_client is not None - else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) - if follow_redirects is not None - else httpx.Client(timeout=_defaulted_timeout), - timeout=_defaulted_timeout, - max_retries=_defaulted_max_retries, - stream_reconnection_enabled=stream_reconnection_enabled, - max_stream_reconnection_attempts=max_stream_reconnection_attempts, - logging=logging, - ) - self._auth: typing.Optional[AuthClient] = None - self._employees: typing.Optional[EmployeesClient] = None - self._employers: typing.Optional[EmployersClient] = None - self._enrollments: typing.Optional[EnrollmentsClient] = None - self._groups: typing.Optional[GroupsClient] = None - self._members: typing.Optional[MembersClient] = None - self._organizations: typing.Optional[OrganizationsClient] = None - self._plans: typing.Optional[PlansClient] = None - self._webhook_events: typing.Optional[WebhookEventsClient] = None - - @property - def auth(self): - if self._auth is None: - from .auth.client import AuthClient # noqa: E402 - - self._auth = AuthClient(client_wrapper=self._client_wrapper) - return self._auth - - @property - def employees(self): - if self._employees is None: - from .employees.client import EmployeesClient # noqa: E402 - - self._employees = EmployeesClient(client_wrapper=self._client_wrapper) - return self._employees - - @property - def employers(self): - if self._employers is None: - from .employers.client import EmployersClient # noqa: E402 - - self._employers = EmployersClient(client_wrapper=self._client_wrapper) - return self._employers - - @property - def enrollments(self): - if self._enrollments is None: - from .enrollments.client import EnrollmentsClient # noqa: E402 - - self._enrollments = EnrollmentsClient(client_wrapper=self._client_wrapper) - return self._enrollments - - @property - def groups(self): - if self._groups is None: - from .groups.client import GroupsClient # noqa: E402 - - self._groups = GroupsClient(client_wrapper=self._client_wrapper) - return self._groups - - @property - def members(self): - if self._members is None: - from .members.client import MembersClient # noqa: E402 - - self._members = MembersClient(client_wrapper=self._client_wrapper) - return self._members - - @property - def organizations(self): - if self._organizations is None: - from .organizations.client import OrganizationsClient # noqa: E402 - - self._organizations = OrganizationsClient(client_wrapper=self._client_wrapper) - return self._organizations - - @property - def plans(self): - if self._plans is None: - from .plans.client import PlansClient # noqa: E402 - - self._plans = PlansClient(client_wrapper=self._client_wrapper) - return self._plans - - @property - def webhook_events(self): - if self._webhook_events is None: - from .webhook_events.client import WebhookEventsClient # noqa: E402 - - self._webhook_events = WebhookEventsClient(client_wrapper=self._client_wrapper) - return self._webhook_events - - -def _make_default_async_client( - timeout: typing.Optional[float], - follow_redirects: typing.Optional[bool], -) -> httpx.AsyncClient: - try: - import httpx_aiohttp # type: ignore[import-not-found] - except ImportError: - pass - else: - if follow_redirects is not None: - return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout, follow_redirects=follow_redirects) - return httpx_aiohttp.HttpxAiohttpClient(timeout=timeout) - - if follow_redirects is not None: - return httpx.AsyncClient(timeout=timeout, follow_redirects=follow_redirects) - return httpx.AsyncClient(timeout=timeout) - - -class AsyncVitableConnect: - """ - Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. - - Parameters - ---------- - base_url : typing.Optional[str] - The base url to use for requests from the client. - - environment : VitableConnectEnvironment - The environment to use for requests from the client. from .environment import VitableConnectEnvironment - - - - Defaults to VitableConnectEnvironment.PRODUCTION - - - - api_key : typing.Optional[typing.Union[str, typing.Callable[[], str]]] - headers : typing.Optional[typing.Dict[str, str]] - Additional headers to send with every request. - - async_token : typing.Optional[typing.Callable[[], typing.Awaitable[str]]] - An async callable that returns a bearer token. Use this when token acquisition involves async I/O (e.g., refreshing tokens via an async HTTP client). When provided, this is used instead of the synchronous token for async requests. - - timeout : typing.Optional[float] - The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. - - max_retries : typing.Optional[int] - The default maximum number of retries for failed requests. Defaults to 2. Per-request `max_retries` in `request_options` takes precedence over this value. - - stream_reconnection_enabled : typing.Optional[bool] - Whether to automatically reconnect on stream disconnection for resumable streaming endpoints. Defaults to True. Per-request `stream_reconnection_enabled` in `request_options` takes precedence over this value. - - max_stream_reconnection_attempts : typing.Optional[int] - The maximum number of reconnection attempts for resumable streaming endpoints. Defaults to no limit. Per-request `max_stream_reconnection_attempts` in `request_options` takes precedence over this value. - - follow_redirects : typing.Optional[bool] - Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. - - httpx_client : typing.Optional[httpx.AsyncClient] - The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. - - logging : typing.Optional[typing.Union[LogConfig, Logger]] - Configure logging for the SDK. Accepts a LogConfig dict with 'level' (debug/info/warn/error), 'logger' (custom logger implementation), and 'silent' (boolean, defaults to True) fields. You can also pass a pre-configured Logger instance. - - Examples - -------- - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - """ - - def __init__( - self, - *, - base_url: typing.Optional[str] = None, - environment: VitableConnectEnvironment = VitableConnectEnvironment.PRODUCTION, - api_key: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = os.getenv("VITABLE_CONNECT_API_KEY"), - headers: typing.Optional[typing.Dict[str, str]] = None, - async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, - timeout: typing.Optional[float] = None, - max_retries: typing.Optional[int] = None, - stream_reconnection_enabled: typing.Optional[bool] = None, - max_stream_reconnection_attempts: typing.Optional[int] = None, - follow_redirects: typing.Optional[bool] = True, - httpx_client: typing.Optional[httpx.AsyncClient] = None, - logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, - ): - _defaulted_timeout = timeout if timeout is not None else 60 if httpx_client is None else None - _defaulted_max_retries = max_retries if max_retries is not None else 2 - if api_key is None: - raise ApiError( - body="The client must be instantiated be either passing in api_key or setting VITABLE_CONNECT_API_KEY" - ) - self._client_wrapper = AsyncClientWrapper( - base_url=_get_base_url(base_url=base_url, environment=environment), - api_key=api_key, - headers=headers, - async_token=async_token, - httpx_client=httpx_client - if httpx_client is not None - else _make_default_async_client(timeout=_defaulted_timeout, follow_redirects=follow_redirects), - timeout=_defaulted_timeout, - max_retries=_defaulted_max_retries, - stream_reconnection_enabled=stream_reconnection_enabled, - max_stream_reconnection_attempts=max_stream_reconnection_attempts, - logging=logging, - ) - self._auth: typing.Optional[AsyncAuthClient] = None - self._employees: typing.Optional[AsyncEmployeesClient] = None - self._employers: typing.Optional[AsyncEmployersClient] = None - self._enrollments: typing.Optional[AsyncEnrollmentsClient] = None - self._groups: typing.Optional[AsyncGroupsClient] = None - self._members: typing.Optional[AsyncMembersClient] = None - self._organizations: typing.Optional[AsyncOrganizationsClient] = None - self._plans: typing.Optional[AsyncPlansClient] = None - self._webhook_events: typing.Optional[AsyncWebhookEventsClient] = None - - @property - def auth(self): - if self._auth is None: - from .auth.client import AsyncAuthClient # noqa: E402 - - self._auth = AsyncAuthClient(client_wrapper=self._client_wrapper) - return self._auth - - @property - def employees(self): - if self._employees is None: - from .employees.client import AsyncEmployeesClient # noqa: E402 - - self._employees = AsyncEmployeesClient(client_wrapper=self._client_wrapper) - return self._employees - - @property - def employers(self): - if self._employers is None: - from .employers.client import AsyncEmployersClient # noqa: E402 - - self._employers = AsyncEmployersClient(client_wrapper=self._client_wrapper) - return self._employers - - @property - def enrollments(self): - if self._enrollments is None: - from .enrollments.client import AsyncEnrollmentsClient # noqa: E402 - - self._enrollments = AsyncEnrollmentsClient(client_wrapper=self._client_wrapper) - return self._enrollments - - @property - def groups(self): - if self._groups is None: - from .groups.client import AsyncGroupsClient # noqa: E402 - - self._groups = AsyncGroupsClient(client_wrapper=self._client_wrapper) - return self._groups - - @property - def members(self): - if self._members is None: - from .members.client import AsyncMembersClient # noqa: E402 - - self._members = AsyncMembersClient(client_wrapper=self._client_wrapper) - return self._members - - @property - def organizations(self): - if self._organizations is None: - from .organizations.client import AsyncOrganizationsClient # noqa: E402 - - self._organizations = AsyncOrganizationsClient(client_wrapper=self._client_wrapper) - return self._organizations - - @property - def plans(self): - if self._plans is None: - from .plans.client import AsyncPlansClient # noqa: E402 - - self._plans = AsyncPlansClient(client_wrapper=self._client_wrapper) - return self._plans - - @property - def webhook_events(self): - if self._webhook_events is None: - from .webhook_events.client import AsyncWebhookEventsClient # noqa: E402 - - self._webhook_events = AsyncWebhookEventsClient(client_wrapper=self._client_wrapper) - return self._webhook_events - - -def _get_base_url(*, base_url: typing.Optional[str] = None, environment: VitableConnectEnvironment) -> str: - if base_url is not None: - return base_url - elif environment is not None: - return environment.value - else: - raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/src/vitable_connect/core/__init__.py b/src/vitable_connect/core/__init__.py deleted file mode 100644 index e2be580..0000000 --- a/src/vitable_connect/core/__init__.py +++ /dev/null @@ -1,132 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .api_error import ApiError - from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper - from .datetime_utils import Rfc2822DateTime, parse_rfc2822_datetime, serialize_datetime - from .file import File, convert_file_dict_to_httpx_tuples, with_content_type - from .http_client import AsyncHttpClient, HttpClient - from .http_response import AsyncHttpResponse, HttpResponse - from .jsonable_encoder import encode_path_param, jsonable_encoder - from .logging import ConsoleLogger, ILogger, LogConfig, LogLevel, Logger, create_logger - from .pagination import AsyncPager, SyncPager - from .parse_error import ParsingError - from .pydantic_utilities import ( - IS_PYDANTIC_V2, - UniversalBaseModel, - UniversalRootModel, - parse_obj_as, - universal_field_validator, - universal_root_validator, - update_forward_refs, - ) - from .query_encoder import encode_query - from .remove_none_from_dict import remove_none_from_dict - from .request_options import RequestOptions - from .serialization import FieldMetadata, convert_and_respect_annotation_metadata -_dynamic_imports: typing.Dict[str, str] = { - "ApiError": ".api_error", - "AsyncClientWrapper": ".client_wrapper", - "AsyncHttpClient": ".http_client", - "AsyncHttpResponse": ".http_response", - "AsyncPager": ".pagination", - "BaseClientWrapper": ".client_wrapper", - "ConsoleLogger": ".logging", - "FieldMetadata": ".serialization", - "File": ".file", - "HttpClient": ".http_client", - "HttpResponse": ".http_response", - "ILogger": ".logging", - "IS_PYDANTIC_V2": ".pydantic_utilities", - "LogConfig": ".logging", - "LogLevel": ".logging", - "Logger": ".logging", - "ParsingError": ".parse_error", - "RequestOptions": ".request_options", - "Rfc2822DateTime": ".datetime_utils", - "SyncClientWrapper": ".client_wrapper", - "SyncPager": ".pagination", - "UniversalBaseModel": ".pydantic_utilities", - "UniversalRootModel": ".pydantic_utilities", - "convert_and_respect_annotation_metadata": ".serialization", - "convert_file_dict_to_httpx_tuples": ".file", - "create_logger": ".logging", - "encode_path_param": ".jsonable_encoder", - "encode_query": ".query_encoder", - "jsonable_encoder": ".jsonable_encoder", - "parse_obj_as": ".pydantic_utilities", - "parse_rfc2822_datetime": ".datetime_utils", - "remove_none_from_dict": ".remove_none_from_dict", - "serialize_datetime": ".datetime_utils", - "universal_field_validator": ".pydantic_utilities", - "universal_root_validator": ".pydantic_utilities", - "update_forward_refs": ".pydantic_utilities", - "with_content_type": ".file", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e - except AttributeError as e: - raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = [ - "ApiError", - "AsyncClientWrapper", - "AsyncHttpClient", - "AsyncHttpResponse", - "AsyncPager", - "BaseClientWrapper", - "ConsoleLogger", - "FieldMetadata", - "File", - "HttpClient", - "HttpResponse", - "ILogger", - "IS_PYDANTIC_V2", - "LogConfig", - "LogLevel", - "Logger", - "ParsingError", - "RequestOptions", - "Rfc2822DateTime", - "SyncClientWrapper", - "SyncPager", - "UniversalBaseModel", - "UniversalRootModel", - "convert_and_respect_annotation_metadata", - "convert_file_dict_to_httpx_tuples", - "create_logger", - "encode_path_param", - "encode_query", - "jsonable_encoder", - "parse_obj_as", - "parse_rfc2822_datetime", - "remove_none_from_dict", - "serialize_datetime", - "universal_field_validator", - "universal_root_validator", - "update_forward_refs", - "with_content_type", -] diff --git a/src/vitable_connect/core/api_error.py b/src/vitable_connect/core/api_error.py deleted file mode 100644 index 6f850a6..0000000 --- a/src/vitable_connect/core/api_error.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Any, Dict, Optional - - -class ApiError(Exception): - headers: Optional[Dict[str, str]] - status_code: Optional[int] - body: Any - - def __init__( - self, - *, - headers: Optional[Dict[str, str]] = None, - status_code: Optional[int] = None, - body: Any = None, - ) -> None: - self.headers = headers - self.status_code = status_code - self.body = body - - def __str__(self) -> str: - return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/src/vitable_connect/core/client_wrapper.py b/src/vitable_connect/core/client_wrapper.py deleted file mode 100644 index fce236b..0000000 --- a/src/vitable_connect/core/client_wrapper.py +++ /dev/null @@ -1,147 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import httpx -from .http_client import AsyncHttpClient, HttpClient -from .logging import LogConfig, Logger - - -class BaseClientWrapper: - def __init__( - self, - *, - api_key: typing.Union[str, typing.Callable[[], str]], - headers: typing.Optional[typing.Dict[str, str]] = None, - base_url: str, - timeout: typing.Optional[float] = None, - max_retries: int = 2, - stream_reconnection_enabled: typing.Optional[bool] = None, - max_stream_reconnection_attempts: typing.Optional[int] = None, - logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, - ): - self._api_key = api_key - self._headers = headers - self._base_url = base_url - self._timeout = timeout - self._max_retries = max_retries - self._stream_reconnection_enabled = stream_reconnection_enabled - self._max_stream_reconnection_attempts = max_stream_reconnection_attempts - self._logging = logging - - def get_headers(self) -> typing.Dict[str, str]: - import platform - - headers: typing.Dict[str, str] = { - "User-Agent": "vitable-connect/0.0.0-fern-placeholder", - "X-Fern-Language": "Python", - "X-Fern-Runtime": f"python/{platform.python_version()}", - "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", - "X-Fern-SDK-Name": "vitable-connect", - "X-Fern-SDK-Version": "1.0.1", - **(self.get_custom_headers() or {}), - } - headers["Authorization"] = f"Bearer {self._get_api_key()}" - return headers - - def _get_api_key(self) -> str: - if isinstance(self._api_key, str): - return self._api_key - else: - return self._api_key() - - def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: - return self._headers - - def get_base_url(self) -> str: - return self._base_url - - def get_timeout(self) -> typing.Optional[float]: - return self._timeout - - def get_max_retries(self) -> int: - return self._max_retries - - def get_stream_reconnection_enabled(self) -> bool: - return self._stream_reconnection_enabled if self._stream_reconnection_enabled is not None else True - - def get_max_stream_reconnection_attempts(self) -> typing.Optional[int]: - return self._max_stream_reconnection_attempts - - -class SyncClientWrapper(BaseClientWrapper): - def __init__( - self, - *, - api_key: typing.Union[str, typing.Callable[[], str]], - headers: typing.Optional[typing.Dict[str, str]] = None, - base_url: str, - timeout: typing.Optional[float] = None, - max_retries: int = 2, - stream_reconnection_enabled: typing.Optional[bool] = None, - max_stream_reconnection_attempts: typing.Optional[int] = None, - logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, - httpx_client: httpx.Client, - ): - super().__init__( - api_key=api_key, - headers=headers, - base_url=base_url, - timeout=timeout, - max_retries=max_retries, - stream_reconnection_enabled=stream_reconnection_enabled, - max_stream_reconnection_attempts=max_stream_reconnection_attempts, - logging=logging, - ) - self.httpx_client = HttpClient( - httpx_client=httpx_client, - base_headers=self.get_headers, - base_timeout=self.get_timeout, - base_url=self.get_base_url, - base_max_retries=self.get_max_retries(), - logging_config=self._logging, - ) - - -class AsyncClientWrapper(BaseClientWrapper): - def __init__( - self, - *, - api_key: typing.Union[str, typing.Callable[[], str]], - headers: typing.Optional[typing.Dict[str, str]] = None, - base_url: str, - timeout: typing.Optional[float] = None, - max_retries: int = 2, - stream_reconnection_enabled: typing.Optional[bool] = None, - max_stream_reconnection_attempts: typing.Optional[int] = None, - logging: typing.Optional[typing.Union[LogConfig, Logger]] = None, - async_token: typing.Optional[typing.Callable[[], typing.Awaitable[str]]] = None, - httpx_client: httpx.AsyncClient, - ): - super().__init__( - api_key=api_key, - headers=headers, - base_url=base_url, - timeout=timeout, - max_retries=max_retries, - stream_reconnection_enabled=stream_reconnection_enabled, - max_stream_reconnection_attempts=max_stream_reconnection_attempts, - logging=logging, - ) - self._async_token = async_token - self.httpx_client = AsyncHttpClient( - httpx_client=httpx_client, - base_headers=self.get_headers, - base_timeout=self.get_timeout, - base_url=self.get_base_url, - base_max_retries=self.get_max_retries(), - async_base_headers=self.async_get_headers, - logging_config=self._logging, - ) - - async def async_get_headers(self) -> typing.Dict[str, str]: - headers = self.get_headers() - if self._async_token is not None: - token = await self._async_token() - headers["Authorization"] = f"Bearer {token}" - return headers diff --git a/src/vitable_connect/core/datetime_utils.py b/src/vitable_connect/core/datetime_utils.py deleted file mode 100644 index a12b2ad..0000000 --- a/src/vitable_connect/core/datetime_utils.py +++ /dev/null @@ -1,70 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -from email.utils import parsedate_to_datetime -from typing import Any - -import pydantic - -IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") - - -def parse_rfc2822_datetime(v: Any) -> dt.datetime: - """ - Parse an RFC 2822 datetime string (e.g., "Wed, 02 Oct 2002 13:00:00 GMT") - into a datetime object. If the value is already a datetime, return it as-is. - Falls back to ISO 8601 parsing if RFC 2822 parsing fails. - """ - if isinstance(v, dt.datetime): - return v - if isinstance(v, str): - try: - return parsedate_to_datetime(v) - except Exception: - pass - # Fallback to ISO 8601 parsing - return dt.datetime.fromisoformat(v.replace("Z", "+00:00")) - raise ValueError(f"Expected str or datetime, got {type(v)}") - - -class Rfc2822DateTime(dt.datetime): - """A datetime subclass that parses RFC 2822 date strings. - - On Pydantic V1, uses __get_validators__ for pre-validation. - On Pydantic V2, uses __get_pydantic_core_schema__ for BeforeValidator-style parsing. - """ - - @classmethod - def __get_validators__(cls): # type: ignore[no-untyped-def] - yield parse_rfc2822_datetime - - @classmethod - def __get_pydantic_core_schema__(cls, _source_type: Any, _handler: Any) -> Any: # type: ignore[override] - from pydantic_core import core_schema - - return core_schema.no_info_before_validator_function(parse_rfc2822_datetime, core_schema.datetime_schema()) - - -def serialize_datetime(v: dt.datetime) -> str: - """ - Serialize a datetime including timezone info. - - Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. - - UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. - """ - - def _serialize_zoned_datetime(v: dt.datetime) -> str: - if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): - # UTC is a special case where we use "Z" at the end instead of "+00:00" - return v.isoformat().replace("+00:00", "Z") - else: - # Delegate to the typical +/- offset format - return v.isoformat() - - if v.tzinfo is not None: - return _serialize_zoned_datetime(v) - else: - local_tz = dt.datetime.now().astimezone().tzinfo - localized_dt = v.replace(tzinfo=local_tz) - return _serialize_zoned_datetime(localized_dt) diff --git a/src/vitable_connect/core/file.py b/src/vitable_connect/core/file.py deleted file mode 100644 index 44b0d27..0000000 --- a/src/vitable_connect/core/file.py +++ /dev/null @@ -1,67 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast - -# File typing inspired by the flexibility of types within the httpx library -# https://github.com/encode/httpx/blob/master/httpx/_types.py -FileContent = Union[IO[bytes], bytes, str] -File = Union[ - # file (or bytes) - FileContent, - # (filename, file (or bytes)) - Tuple[Optional[str], FileContent], - # (filename, file (or bytes), content_type) - Tuple[Optional[str], FileContent, Optional[str]], - # (filename, file (or bytes), content_type, headers) - Tuple[ - Optional[str], - FileContent, - Optional[str], - Mapping[str, str], - ], -] - - -def convert_file_dict_to_httpx_tuples( - d: Dict[str, Union[File, List[File]]], -) -> List[Tuple[str, File]]: - """ - The format we use is a list of tuples, where the first element is the - name of the file and the second is the file object. Typically HTTPX wants - a dict, but to be able to send lists of files, you have to use the list - approach (which also works for non-lists) - https://github.com/encode/httpx/pull/1032 - """ - - httpx_tuples = [] - for key, file_like in d.items(): - if isinstance(file_like, list): - for file_like_item in file_like: - httpx_tuples.append((key, file_like_item)) - else: - httpx_tuples.append((key, file_like)) - return httpx_tuples - - -def with_content_type(*, file: File, default_content_type: str) -> File: - """ - This function resolves to the file's content type, if provided, and defaults - to the default_content_type value if not. - """ - if isinstance(file, tuple): - if len(file) == 2: - filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore - return (filename, content, default_content_type) - elif len(file) == 3: - filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore - out_content_type = file_content_type or default_content_type - return (filename, content, out_content_type) - elif len(file) == 4: - filename, content, file_content_type, headers = cast( # type: ignore - Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file - ) - out_content_type = file_content_type or default_content_type - return (filename, content, out_content_type, headers) - else: - raise ValueError(f"Unexpected tuple length: {len(file)}") - return (None, file, default_content_type) diff --git a/src/vitable_connect/core/force_multipart.py b/src/vitable_connect/core/force_multipart.py deleted file mode 100644 index 5440913..0000000 --- a/src/vitable_connect/core/force_multipart.py +++ /dev/null @@ -1,18 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Any, Dict - - -class ForceMultipartDict(Dict[str, Any]): - """ - A dictionary subclass that always evaluates to True in boolean contexts. - - This is used to force multipart/form-data encoding in HTTP requests even when - the dictionary is empty, which would normally evaluate to False. - """ - - def __bool__(self) -> bool: - return True - - -FORCE_MULTIPART = ForceMultipartDict() diff --git a/src/vitable_connect/core/http_client.py b/src/vitable_connect/core/http_client.py deleted file mode 100644 index 3e5a786..0000000 --- a/src/vitable_connect/core/http_client.py +++ /dev/null @@ -1,940 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import asyncio -import email.utils -import re -import socket -import time -import typing -from contextlib import asynccontextmanager, contextmanager -from random import random - -import httpx -from .file import File, convert_file_dict_to_httpx_tuples -from .force_multipart import FORCE_MULTIPART -from .jsonable_encoder import jsonable_encoder -from .logging import LogConfig, Logger, create_logger -from .query_encoder import encode_query -from .remove_none_from_dict import remove_none_from_dict as remove_none_from_dict -from .request_options import RequestOptions -from httpx._types import RequestFiles - -INITIAL_RETRY_DELAY_SECONDS = 1.0 -MAX_RETRY_DELAY_SECONDS = 60.0 -JITTER_FACTOR = 0.2 # 20% random jitter - - -def get_keepalive_socket_options( - idle: int = 60, - intvl: int = 30, - cnt: int = 5, -) -> typing.List[typing.Tuple[int, int, int]]: - """ - Build TCP keepalive socket options for the current platform. - - Keepalive probes keep otherwise-idle connections alive so that long, - non-streaming requests survive idle-connection reaping by a firewall, - load balancer, or NAT. The available socket constants are OS-dependent, - so each option is guarded and only emitted when the platform defines it: - - - ``SO_KEEPALIVE`` is portable (Linux/macOS/Windows). - - The idle-before-first-probe knob is ``TCP_KEEPIDLE`` on Linux and modern - Windows, but ``TCP_KEEPALIVE`` on macOS. - - ``TCP_KEEPINTVL`` / ``TCP_KEEPCNT`` exist on Linux/macOS/modern Windows. - - Passing these tuples to ``httpx.HTTPTransport(socket_options=...)`` / - ``httpx.AsyncHTTPTransport(socket_options=...)`` applies them to every - connection the transport opens. - """ - opts: typing.List[typing.Tuple[int, int, int]] = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)] - idle_const = getattr(socket, "TCP_KEEPIDLE", None) or getattr(socket, "TCP_KEEPALIVE", None) - if idle_const: - opts.append((socket.IPPROTO_TCP, idle_const, idle)) - if hasattr(socket, "TCP_KEEPINTVL"): - opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl)) - if hasattr(socket, "TCP_KEEPCNT"): - opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt)) - return opts - - -def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: - """ - This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. - - Inspired by the urllib3 retry implementation. - """ - retry_after_ms = response_headers.get("retry-after-ms") - if retry_after_ms is not None: - try: - return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 - except Exception: - pass - - retry_after = response_headers.get("retry-after") - if retry_after is None: - return None - - # Attempt to parse the header as an int. - if re.match(r"^\s*[0-9]+\s*$", retry_after): - seconds = float(retry_after) - # Fallback to parsing it as a date. - else: - retry_date_tuple = email.utils.parsedate_tz(retry_after) - if retry_date_tuple is None: - return None - if retry_date_tuple[9] is None: # Python 2 - # Assume UTC if no timezone was specified - # On Python2.7, parsedate_tz returns None for a timezone offset - # instead of 0 if no timezone is given, where mktime_tz treats - # a None timezone offset as local time. - retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] - - retry_date = email.utils.mktime_tz(retry_date_tuple) - seconds = retry_date - time.time() - - if seconds < 0: - seconds = 0 - - return seconds - - -def _add_positive_jitter(delay: float) -> float: - """Add positive jitter (0-20%) to prevent thundering herd.""" - jitter_multiplier = 1 + random() * JITTER_FACTOR - return delay * jitter_multiplier - - -def _add_symmetric_jitter(delay: float) -> float: - """Add symmetric jitter (±10%) for exponential backoff.""" - jitter_multiplier = 1 + (random() - 0.5) * JITTER_FACTOR - return delay * jitter_multiplier - - -def _parse_x_ratelimit_reset(response_headers: httpx.Headers) -> typing.Optional[float]: - """ - Parse the X-RateLimit-Reset header (Unix timestamp in seconds). - Returns seconds to wait, or None if header is missing/invalid. - """ - reset_time_str = response_headers.get("x-ratelimit-reset") - if reset_time_str is None: - return None - - try: - reset_time = int(reset_time_str) - delay = reset_time - time.time() - if delay > 0: - return delay - except (ValueError, TypeError): - pass - - return None - - -def _retry_timeout(response: httpx.Response, retries: int) -> float: - """ - Determine the amount of time to wait before retrying a request. - This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff - with a jitter to determine the number of seconds to wait. - """ - - # 1. Check Retry-After header first - retry_after = _parse_retry_after(response.headers) - if retry_after is not None and retry_after > 0: - return min(retry_after, MAX_RETRY_DELAY_SECONDS) - - # 2. Check X-RateLimit-Reset header (with positive jitter) - ratelimit_reset = _parse_x_ratelimit_reset(response.headers) - if ratelimit_reset is not None: - return _add_positive_jitter(min(ratelimit_reset, MAX_RETRY_DELAY_SECONDS)) - - # 3. Fall back to exponential backoff (with symmetric jitter) - backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) - return _add_symmetric_jitter(backoff) - - -def _retry_timeout_from_retries(retries: int) -> float: - """Determine retry timeout using exponential backoff when no response is available.""" - backoff = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) - return _add_symmetric_jitter(backoff) - - -def _should_retry(response: httpx.Response) -> bool: - return response.status_code >= 500 or response.status_code in [429, 408, 409] - - -_SENSITIVE_HEADERS = frozenset( - { - "authorization", - "www-authenticate", - "x-api-key", - "api-key", - "apikey", - "x-api-token", - "x-auth-token", - "auth-token", - "cookie", - "set-cookie", - "proxy-authorization", - "proxy-authenticate", - "x-csrf-token", - "x-xsrf-token", - "x-session-token", - "x-access-token", - } -) - - -def _redact_headers(headers: typing.Dict[str, str]) -> typing.Dict[str, str]: - return {k: ("[REDACTED]" if k.lower() in _SENSITIVE_HEADERS else v) for k, v in headers.items()} - - -def _build_url(base_url: str, path: typing.Optional[str]) -> str: - """ - Build a full URL by joining a base URL with a path. - - This function correctly handles base URLs that contain path prefixes (e.g., tenant-based URLs) - by using string concatenation instead of urllib.parse.urljoin(), which would incorrectly - strip path components when the path starts with '/'. - - Example: - >>> _build_url("https://cloud.example.com/org/tenant/api", "/users") - 'https://cloud.example.com/org/tenant/api/users' - - Args: - base_url: The base URL, which may contain path prefixes. - path: The path to append. Can be None or empty string. - - Returns: - The full URL with base_url and path properly joined. - """ - if not path: - return base_url - return f"{base_url.rstrip('/')}/{path.lstrip('/')}" - - -def _maybe_filter_none_from_multipart_data( - data: typing.Optional[typing.Any], - request_files: typing.Optional[RequestFiles], - force_multipart: typing.Optional[bool], -) -> typing.Optional[typing.Any]: - """ - Filter None values from data body for multipart/form requests. - This prevents httpx from converting None to empty strings in multipart encoding. - Only applies when files are present or force_multipart is True. - """ - if data is not None and isinstance(data, typing.Mapping) and (request_files or force_multipart): - return remove_none_from_dict(data) - return data - - -def remove_omit_from_dict( - original: typing.Dict[str, typing.Optional[typing.Any]], - omit: typing.Optional[typing.Any], -) -> typing.Dict[str, typing.Any]: - if omit is None: - return original - new: typing.Dict[str, typing.Any] = {} - for key, value in original.items(): - if value is not omit: - new[key] = value - return new - - -def maybe_filter_request_body( - data: typing.Optional[typing.Any], - request_options: typing.Optional[RequestOptions], - omit: typing.Optional[typing.Any], -) -> typing.Optional[typing.Any]: - if data is None: - return ( - jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} - if request_options is not None - else None - ) - elif not isinstance(data, typing.Mapping): - data_content = jsonable_encoder(data) - else: - data_content = { - **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore - **( - jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} - if request_options is not None - else {} - ), - } - return data_content - - -# Abstracted out for testing purposes -def get_request_body( - *, - json: typing.Optional[typing.Any], - data: typing.Optional[typing.Any], - request_options: typing.Optional[RequestOptions], - omit: typing.Optional[typing.Any], - optional_body: bool = False, -) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: - # A whole body left at the sentinel was never passed by the caller, so it is absent - # rather than empty: the request carries no content and no `Content-Type`. - if omit is not None: - if json is omit: - json = None - if data is omit: - data = None - - json_body = None - data_body = None - if data is not None: - data_body = maybe_filter_request_body(data, request_options, omit) - else: - # If both data and json are None, we send json data in the event extra properties are specified - json_body = maybe_filter_request_body(json, request_options, omit) - - has_additional_body_parameters = bool( - request_options is not None and request_options.get("additional_body_parameters") - ) - - # Only collapse empty dict to None when the body was not explicitly provided - # and there are no additional body parameters. This preserves explicit empty - # bodies (e.g., when an endpoint has a request body type but all fields are optional). - # `optional_body` marks an endpoint whose body the API does not require, where a body - # that ends up empty means the caller passed none of its properties, so the request is - # sent with no content and no `Content-Type`. - if json_body == {} and (json is None or optional_body) and not has_additional_body_parameters: - json_body = None - if data_body == {} and (data is None or optional_body) and not has_additional_body_parameters: - data_body = None - - return json_body, data_body - - -def drop_content_type_without_body( - headers: typing.Dict[str, typing.Any], - *, - json_body: typing.Optional[typing.Any], - data_body: typing.Optional[typing.Any], - optional_body: bool, -) -> typing.Dict[str, typing.Any]: - """Strip ``Content-Type`` from a request that carries no body. - - ``get_request_body`` drops the body of an ``optional_body`` endpoint when the caller - supplied none of it, but the endpoint still passes the content type it would have used. - A request that sends nothing must not advertise a media type, so a server that branches - on the header sees a bodyless call for what it is. - """ - if not optional_body or json_body is not None or data_body is not None: - return headers - return {key: value for key, value in headers.items() if key.lower() != "content-type"} - - -class HttpClient: - def __init__( - self, - *, - httpx_client: httpx.Client, - base_timeout: typing.Callable[[], typing.Optional[float]], - base_headers: typing.Callable[[], typing.Dict[str, str]], - base_url: typing.Optional[typing.Callable[[], str]] = None, - base_max_retries: int = 2, - logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, - ): - self.base_url = base_url - self.base_timeout = base_timeout - self.base_headers = base_headers - self.base_max_retries = base_max_retries - self.httpx_client = httpx_client - self.logger = create_logger(logging_config) - - def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: - base_url = maybe_base_url - if self.base_url is not None and base_url is None: - base_url = self.base_url() - - if base_url is None: - raise ValueError("A base_url is required to make this request, please provide one and try again.") - return base_url - - def request( - self, - path: typing.Optional[str] = None, - *, - method: str, - base_url: typing.Optional[str] = None, - params: typing.Optional[typing.Dict[str, typing.Any]] = None, - json: typing.Optional[typing.Any] = None, - data: typing.Optional[typing.Any] = None, - content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, - files: typing.Optional[ - typing.Union[ - typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], - typing.List[typing.Tuple[str, File]], - ] - ] = None, - headers: typing.Optional[typing.Dict[str, typing.Any]] = None, - request_options: typing.Optional[RequestOptions] = None, - retries: int = 0, - omit: typing.Optional[typing.Any] = None, - optional_body: bool = False, - force_multipart: typing.Optional[bool] = None, - ) -> httpx.Response: - base_url = self.get_base_url(base_url) - _timeout = ( - request_options.get("timeout") - if request_options is not None and request_options.get("timeout") is not None - else request_options.get("timeout_in_seconds") - if request_options is not None and request_options.get("timeout_in_seconds") is not None - else self.base_timeout() - ) - timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT - - json_body, data_body = get_request_body( - json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body - ) - - request_files: typing.Optional[RequestFiles] = ( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if (files is not None and files is not omit and isinstance(files, dict)) - else None - ) - - if (request_files is None or len(request_files) == 0) and force_multipart: - request_files = FORCE_MULTIPART - - data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) - - # Compute encoded params separately to avoid passing empty list to httpx - # (httpx strips existing query params from URL when params=[] is passed) - _encoded_params = encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) or {} - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ) - - _request_url = _build_url(base_url, path) - _request_headers = jsonable_encoder( - remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), - } - ) - ) - _request_headers = drop_content_type_without_body( - _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body - ) - - if self.logger.is_debug(): - self.logger.debug( - "Making HTTP request", - method=method, - url=_request_url, - headers=_redact_headers(_request_headers), - has_body=json_body is not None or data_body is not None, - ) - - max_retries: int = ( - request_options.get("max_retries", self.base_max_retries) - if request_options is not None - else self.base_max_retries - ) - - try: - response = self.httpx_client.request( - method=method, - url=_request_url, - headers=_request_headers, - params=_encoded_params if _encoded_params else None, - json=json_body, - data=data_body, - content=content, - files=request_files, - timeout=timeout, - ) - except (httpx.ConnectError, httpx.RemoteProtocolError): - if retries < max_retries: - time.sleep(_retry_timeout_from_retries(retries=retries)) - return self.request( - path=path, - method=method, - base_url=base_url, - params=params, - json=json, - data=data, - content=content, - files=files, - headers=headers, - request_options=request_options, - retries=retries + 1, - omit=omit, - force_multipart=force_multipart, - ) - raise - - if _should_retry(response=response): - if retries < max_retries: - time.sleep(_retry_timeout(response=response, retries=retries)) - return self.request( - path=path, - method=method, - base_url=base_url, - params=params, - json=json, - data=data, - content=content, - files=files, - headers=headers, - request_options=request_options, - retries=retries + 1, - omit=omit, - force_multipart=force_multipart, - ) - - if self.logger.is_debug(): - if 200 <= response.status_code < 400: - self.logger.debug( - "HTTP request succeeded", - method=method, - url=_request_url, - status_code=response.status_code, - ) - - if self.logger.is_error(): - if response.status_code >= 400: - self.logger.error( - "HTTP request failed with error status", - method=method, - url=_request_url, - status_code=response.status_code, - ) - - return response - - @contextmanager - def stream( - self, - path: typing.Optional[str] = None, - *, - method: str, - base_url: typing.Optional[str] = None, - params: typing.Optional[typing.Dict[str, typing.Any]] = None, - json: typing.Optional[typing.Any] = None, - data: typing.Optional[typing.Any] = None, - content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, - files: typing.Optional[ - typing.Union[ - typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], - typing.List[typing.Tuple[str, File]], - ] - ] = None, - headers: typing.Optional[typing.Dict[str, typing.Any]] = None, - request_options: typing.Optional[RequestOptions] = None, - retries: int = 0, - omit: typing.Optional[typing.Any] = None, - optional_body: bool = False, - force_multipart: typing.Optional[bool] = None, - ) -> typing.Iterator[httpx.Response]: - base_url = self.get_base_url(base_url) - _timeout = ( - request_options.get("timeout") - if request_options is not None and request_options.get("timeout") is not None - else request_options.get("timeout_in_seconds") - if request_options is not None and request_options.get("timeout_in_seconds") is not None - else self.base_timeout() - ) - timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT - - request_files: typing.Optional[RequestFiles] = ( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if (files is not None and files is not omit and isinstance(files, dict)) - else None - ) - - if (request_files is None or len(request_files) == 0) and force_multipart: - request_files = FORCE_MULTIPART - - json_body, data_body = get_request_body( - json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body - ) - - data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) - - # Compute encoded params separately to avoid passing empty list to httpx - # (httpx strips existing query params from URL when params=[] is passed) - _encoded_params = encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ) - - _request_url = _build_url(base_url, path) - _request_headers = jsonable_encoder( - remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) if request_options is not None else {}), - } - ) - ) - _request_headers = drop_content_type_without_body( - _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body - ) - - if self.logger.is_debug(): - self.logger.debug( - "Making streaming HTTP request", - method=method, - url=_request_url, - headers=_redact_headers(_request_headers), - ) - - with self.httpx_client.stream( - method=method, - url=_request_url, - headers=_request_headers, - params=_encoded_params if _encoded_params else None, - json=json_body, - data=data_body, - content=content, - files=request_files, - timeout=timeout, - ) as stream: - yield stream - - -class AsyncHttpClient: - def __init__( - self, - *, - httpx_client: httpx.AsyncClient, - base_timeout: typing.Callable[[], typing.Optional[float]], - base_headers: typing.Callable[[], typing.Dict[str, str]], - base_url: typing.Optional[typing.Callable[[], str]] = None, - base_max_retries: int = 2, - async_base_headers: typing.Optional[typing.Callable[[], typing.Awaitable[typing.Dict[str, str]]]] = None, - logging_config: typing.Optional[typing.Union[LogConfig, Logger]] = None, - ): - self.base_url = base_url - self.base_timeout = base_timeout - self.base_headers = base_headers - self.base_max_retries = base_max_retries - self.async_base_headers = async_base_headers - self.httpx_client = httpx_client - self.logger = create_logger(logging_config) - - async def _get_headers(self) -> typing.Dict[str, str]: - if self.async_base_headers is not None: - return await self.async_base_headers() - return self.base_headers() - - def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: - base_url = maybe_base_url - if self.base_url is not None and base_url is None: - base_url = self.base_url() - - if base_url is None: - raise ValueError("A base_url is required to make this request, please provide one and try again.") - return base_url - - async def request( - self, - path: typing.Optional[str] = None, - *, - method: str, - base_url: typing.Optional[str] = None, - params: typing.Optional[typing.Dict[str, typing.Any]] = None, - json: typing.Optional[typing.Any] = None, - data: typing.Optional[typing.Any] = None, - content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, - files: typing.Optional[ - typing.Union[ - typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], - typing.List[typing.Tuple[str, File]], - ] - ] = None, - headers: typing.Optional[typing.Dict[str, typing.Any]] = None, - request_options: typing.Optional[RequestOptions] = None, - retries: int = 0, - omit: typing.Optional[typing.Any] = None, - optional_body: bool = False, - force_multipart: typing.Optional[bool] = None, - ) -> httpx.Response: - base_url = self.get_base_url(base_url) - _timeout = ( - request_options.get("timeout") - if request_options is not None and request_options.get("timeout") is not None - else request_options.get("timeout_in_seconds") - if request_options is not None and request_options.get("timeout_in_seconds") is not None - else self.base_timeout() - ) - timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT - - request_files: typing.Optional[RequestFiles] = ( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if (files is not None and files is not omit and isinstance(files, dict)) - else None - ) - - if (request_files is None or len(request_files) == 0) and force_multipart: - request_files = FORCE_MULTIPART - - json_body, data_body = get_request_body( - json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body - ) - - data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) - - # Get headers (supports async token providers) - _headers = await self._get_headers() - - # Compute encoded params separately to avoid passing empty list to httpx - # (httpx strips existing query params from URL when params=[] is passed) - _encoded_params = encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) or {} - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ) - - _request_url = _build_url(base_url, path) - _request_headers = jsonable_encoder( - remove_none_from_dict( - { - **_headers, - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), - } - ) - ) - _request_headers = drop_content_type_without_body( - _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body - ) - - if self.logger.is_debug(): - self.logger.debug( - "Making HTTP request", - method=method, - url=_request_url, - headers=_redact_headers(_request_headers), - has_body=json_body is not None or data_body is not None, - ) - - max_retries: int = ( - request_options.get("max_retries", self.base_max_retries) - if request_options is not None - else self.base_max_retries - ) - - try: - response = await self.httpx_client.request( - method=method, - url=_request_url, - headers=_request_headers, - params=_encoded_params if _encoded_params else None, - json=json_body, - data=data_body, - content=content, - files=request_files, - timeout=timeout, - ) - except (httpx.ConnectError, httpx.RemoteProtocolError): - if retries < max_retries: - await asyncio.sleep(_retry_timeout_from_retries(retries=retries)) - return await self.request( - path=path, - method=method, - base_url=base_url, - params=params, - json=json, - data=data, - content=content, - files=files, - headers=headers, - request_options=request_options, - retries=retries + 1, - omit=omit, - force_multipart=force_multipart, - ) - raise - - if _should_retry(response=response): - if retries < max_retries: - await asyncio.sleep(_retry_timeout(response=response, retries=retries)) - return await self.request( - path=path, - method=method, - base_url=base_url, - params=params, - json=json, - data=data, - content=content, - files=files, - headers=headers, - request_options=request_options, - retries=retries + 1, - omit=omit, - force_multipart=force_multipart, - ) - - if self.logger.is_debug(): - if 200 <= response.status_code < 400: - self.logger.debug( - "HTTP request succeeded", - method=method, - url=_request_url, - status_code=response.status_code, - ) - - if self.logger.is_error(): - if response.status_code >= 400: - self.logger.error( - "HTTP request failed with error status", - method=method, - url=_request_url, - status_code=response.status_code, - ) - - return response - - @asynccontextmanager - async def stream( - self, - path: typing.Optional[str] = None, - *, - method: str, - base_url: typing.Optional[str] = None, - params: typing.Optional[typing.Dict[str, typing.Any]] = None, - json: typing.Optional[typing.Any] = None, - data: typing.Optional[typing.Any] = None, - content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, - files: typing.Optional[ - typing.Union[ - typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], - typing.List[typing.Tuple[str, File]], - ] - ] = None, - headers: typing.Optional[typing.Dict[str, typing.Any]] = None, - request_options: typing.Optional[RequestOptions] = None, - retries: int = 0, - omit: typing.Optional[typing.Any] = None, - optional_body: bool = False, - force_multipart: typing.Optional[bool] = None, - ) -> typing.AsyncIterator[httpx.Response]: - base_url = self.get_base_url(base_url) - _timeout = ( - request_options.get("timeout") - if request_options is not None and request_options.get("timeout") is not None - else request_options.get("timeout_in_seconds") - if request_options is not None and request_options.get("timeout_in_seconds") is not None - else self.base_timeout() - ) - timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT - - request_files: typing.Optional[RequestFiles] = ( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if (files is not None and files is not omit and isinstance(files, dict)) - else None - ) - - if (request_files is None or len(request_files) == 0) and force_multipart: - request_files = FORCE_MULTIPART - - json_body, data_body = get_request_body( - json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body - ) - - data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart) - - # Get headers (supports async token providers) - _headers = await self._get_headers() - - # Compute encoded params separately to avoid passing empty list to httpx - # (httpx strips existing query params from URL when params=[] is passed) - _encoded_params = encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) - if request_options is not None - else {} - ), - }, - omit=omit, - ) - ) - ) - ) - - _request_url = _build_url(base_url, path) - _request_headers = jsonable_encoder( - remove_none_from_dict( - { - **_headers, - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) if request_options is not None else {}), - } - ) - ) - _request_headers = drop_content_type_without_body( - _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body - ) - - if self.logger.is_debug(): - self.logger.debug( - "Making streaming HTTP request", - method=method, - url=_request_url, - headers=_redact_headers(_request_headers), - ) - - async with self.httpx_client.stream( - method=method, - url=_request_url, - headers=_request_headers, - params=_encoded_params if _encoded_params else None, - json=json_body, - data=data_body, - content=content, - files=request_files, - timeout=timeout, - ) as stream: - yield stream diff --git a/src/vitable_connect/core/http_response.py b/src/vitable_connect/core/http_response.py deleted file mode 100644 index 9aa1e18..0000000 --- a/src/vitable_connect/core/http_response.py +++ /dev/null @@ -1,63 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Dict, Generic, TypeVar - -import httpx - -# Generic to represent the underlying type of the data wrapped by the HTTP response. -T = TypeVar("T") - - -class BaseHttpResponse: - """Minimalist HTTP response wrapper that exposes response headers and status code.""" - - _response: httpx.Response - - def __init__(self, response: httpx.Response): - self._response = response - - @property - def headers(self) -> Dict[str, str]: - return dict(self._response.headers) - - @property - def status_code(self) -> int: - return self._response.status_code - - @property - def response(self) -> httpx.Response: - return self._response - - -class HttpResponse(Generic[T], BaseHttpResponse): - """HTTP response wrapper that exposes response headers and data.""" - - _data: T - - def __init__(self, response: httpx.Response, data: T): - super().__init__(response) - self._data = data - - @property - def data(self) -> T: - return self._data - - def close(self) -> None: - self._response.close() - - -class AsyncHttpResponse(Generic[T], BaseHttpResponse): - """HTTP response wrapper that exposes response headers and data.""" - - _data: T - - def __init__(self, response: httpx.Response, data: T): - super().__init__(response) - self._data = data - - @property - def data(self) -> T: - return self._data - - async def close(self) -> None: - await self._response.aclose() diff --git a/src/vitable_connect/core/http_sse/__init__.py b/src/vitable_connect/core/http_sse/__init__.py deleted file mode 100644 index 730e5a3..0000000 --- a/src/vitable_connect/core/http_sse/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from ._api import EventSource, aconnect_sse, connect_sse - from ._exceptions import SSEError - from ._models import ServerSentEvent -_dynamic_imports: typing.Dict[str, str] = { - "EventSource": "._api", - "SSEError": "._exceptions", - "ServerSentEvent": "._models", - "aconnect_sse": "._api", - "connect_sse": "._api", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e - except AttributeError as e: - raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["EventSource", "SSEError", "ServerSentEvent", "aconnect_sse", "connect_sse"] diff --git a/src/vitable_connect/core/http_sse/_api.py b/src/vitable_connect/core/http_sse/_api.py deleted file mode 100644 index 9ca5602..0000000 --- a/src/vitable_connect/core/http_sse/_api.py +++ /dev/null @@ -1,455 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import codecs -import re -import time -from contextlib import asynccontextmanager, contextmanager -from typing import ( - Any, - AsyncContextManager, - AsyncGenerator, - AsyncIterator, - Callable, - ContextManager, - Iterator, - Optional, -) - -import anyio -import httpx -from ._decoders import SSEDecoder -from ._exceptions import SSEError -from ._models import ServerSentEvent - -MAX_LINE_SIZE: int = 1_048_576 # 1 MiB - -# Reconnection defaults, mirroring the TypeScript SDK's Stream implementation. -DEFAULT_MAX_RECONNECTION_ATTEMPTS: int = 5 -DEFAULT_RECONNECT_DELAY_MS: int = 1_000 -MAX_RECONNECT_DELAY_MS: int = 30_000 - - -# A reconnect callback re-issues the original request (with a ``Last-Event-ID`` -# header set to the supplied event id) and returns a *context manager* yielding -# a fresh streaming ``httpx.Response``. Sync clients supply a sync context -# manager; async clients supply an async one. -class EventSource: - def __init__( - self, - response: httpx.Response, - *, - resumable: bool = False, - stream_reconnection_enabled: bool = True, - max_stream_reconnection_attempts: Optional[int] = None, - stream_terminator: Optional[str] = None, - reconnect: Optional[Callable[[str], Any]] = None, - ) -> None: - self._response = response - self._resumable = resumable - self._stream_reconnection_enabled = stream_reconnection_enabled - self._max_stream_reconnection_attempts = max_stream_reconnection_attempts - self._stream_terminator = stream_terminator - self._reconnect = reconnect - - @staticmethod - def _is_event_stream(response: httpx.Response) -> bool: - content_type = response.headers.get("content-type", "").partition(";")[0] - return "text/event-stream" in content_type - - def _check_content_type(self) -> None: - if not self._is_event_stream(self._response): - content_type = self._response.headers.get("content-type", "").partition(";")[0] - raise SSEError( - f"Expected response header Content-Type to contain 'text/event-stream', got {content_type!r}" - ) - - def _is_reconnect_response_usable(self, response: httpx.Response) -> bool: - """Whether a reconnected response can be resumed as an SSE stream. - - ``httpx.stream`` does not raise on non-success status, so a resume that - returns an error page (e.g. ``200 text/html`` or a ``500`` body) would - otherwise be parsed as SSE and yield garbage/zero events. Such a - response is treated as a failed attempt (back off and retry) instead. - """ - return response.status_code < 400 and self._is_event_stream(response) - - def _get_charset(self, response: Optional[httpx.Response] = None) -> str: - """Extract charset from Content-Type header, fallback to UTF-8.""" - resolved = response if response is not None else self._response - content_type = resolved.headers.get("content-type", "") - - # Parse charset parameter using regex - charset_match = re.search(r"charset=([^;\s]+)", content_type, re.IGNORECASE) - if charset_match: - charset = charset_match.group(1).strip("\"'") - # Validate that it's a known encoding - try: - # Test if the charset is valid by trying to encode/decode - "test".encode(charset).decode(charset) - return charset - except (LookupError, UnicodeError): - # If charset is invalid, fall back to UTF-8 - pass - - # Default to UTF-8 if no charset specified or invalid charset - return "utf-8" - - @property - def response(self) -> httpx.Response: - return self._response - - @staticmethod - def _normalize_sse_line_endings(buf: str) -> str: - """Normalize line endings per the SSE spec (\\r\\n → \\n, bare \\r → \\n). - - A trailing \\r is preserved because it may pair with a leading \\n in - the next chunk to form a single \\r\\n terminator. - """ - buf = buf.replace("\r\n", "\n") - if buf.endswith("\r"): - return buf[:-1].replace("\r", "\n") + "\r" - return buf.replace("\r", "\n") - - def _new_text_decoder(self, response: Optional[httpx.Response] = None) -> "codecs.IncrementalDecoder": - return codecs.getincrementaldecoder(self._get_charset(response))(errors="replace") - - def _reconnect_applicable(self) -> bool: - """Whether reconnection is configured for this stream at all. - - This is the terminator-gating half of the reconnect decision, kept - separate from :meth:`_should_reconnect` (which additionally requires a - last *dispatched* id and an unexhausted attempt budget). The split lets - a mid-stream transport error terminate consistently: - - a stream that can never reconnect (non-resumable, no terminator, - disabled, or no callback) must re-raise the error to the caller, so a - truncated stream is not mistaken for a clean completion; - - a resumable stream that has merely run out of attempts (or has no id - to resume from) ends cleanly — the same way an exhausted empty/error - -body resume already does, matching the TypeScript ``return``. - """ - return ( - self._resumable - and self._stream_terminator is not None - and self._stream_reconnection_enabled - and self._reconnect is not None - ) - - def _should_reconnect(self, last_dispatched_id: Optional[str], reconnect_attempts: int) -> bool: - """Decide whether a prematurely-ended stream should be reconnected. - - Mirrors the TypeScript ``shouldReconnect`` gating: - - only resumable SSE endpoints with a configured terminator, reconnect - enabled, and a reconnect callback are eligible (see - :meth:`_reconnect_applicable`); - - a last *dispatched* event id must exist to resume from; - - the consecutive-failed-attempt cap must not be exceeded. - """ - if not self._reconnect_applicable(): - return False - if not last_dispatched_id: - return False - max_attempts = ( - self._max_stream_reconnection_attempts - if self._max_stream_reconnection_attempts is not None - else DEFAULT_MAX_RECONNECTION_ATTEMPTS - ) - if reconnect_attempts >= max_attempts: - return False - return True - - def _reconnect_delay_seconds(self, last_retry: Optional[int]) -> float: - """Backoff before a reconnect. - - Uses the server's most recent ``retry:`` directive (milliseconds) when - present, otherwise a default of ``DEFAULT_RECONNECT_DELAY_MS``, clamped - to ``MAX_RECONNECT_DELAY_MS``. - """ - base_ms = last_retry if (last_retry is not None and last_retry > 0) else DEFAULT_RECONNECT_DELAY_MS - return min(base_ms, MAX_RECONNECT_DELAY_MS) / 1000.0 - - def _sleep_before_reconnect(self, last_retry: Optional[int]) -> None: - # ``time.sleep`` blocks the calling thread but remains interruptible by - # signals (e.g. ``KeyboardInterrupt``), which propagate out and abort - # the reconnect without issuing another request. - time.sleep(self._reconnect_delay_seconds(last_retry)) - - async def _asleep_before_reconnect(self, last_retry: Optional[int]) -> None: - # ``anyio.sleep`` is cancellation-aware: if the consumer cancels the task - # or closes the async generator mid-delay, this raises (and no further - # request is issued) instead of blocking for the whole interval. - await anyio.sleep(self._reconnect_delay_seconds(last_retry)) - - def _decode_response( - self, - response: httpx.Response, - decoder: SSEDecoder, - text_decoder: "codecs.IncrementalDecoder", - ) -> Iterator[ServerSentEvent]: - buf = "" - for chunk in response.iter_bytes(): - buf += text_decoder.decode(chunk) - buf = self._normalize_sse_line_endings(buf) - - while "\n" in buf: - line, buf = buf.split("\n", 1) - sse = decoder.decode(line) - if sse is not None: - yield sse - - if len(buf) > MAX_LINE_SIZE: - raise SSEError( - f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" - ) - - yield from self._flush_decoder(buf, decoder, text_decoder) - - async def _adecode_response( - self, - response: httpx.Response, - decoder: SSEDecoder, - text_decoder: "codecs.IncrementalDecoder", - ) -> AsyncGenerator[ServerSentEvent, None]: - buf = "" - async for chunk in response.aiter_bytes(): - buf += text_decoder.decode(chunk) - buf = self._normalize_sse_line_endings(buf) - - while "\n" in buf: - line, buf = buf.split("\n", 1) - sse = decoder.decode(line) - if sse is not None: - yield sse - - if len(buf) > MAX_LINE_SIZE: - raise SSEError( - f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" - ) - - for sse in self._flush_decoder(buf, decoder, text_decoder): - yield sse - - def _flush_decoder( - self, - buf: str, - decoder: SSEDecoder, - text_decoder: "codecs.IncrementalDecoder", - ) -> Iterator[ServerSentEvent]: - # Flush any remaining bytes from the incremental decoder - buf += text_decoder.decode(b"", final=True) - buf = buf.replace("\r\n", "\n").replace("\r", "\n") - - if len(buf) > MAX_LINE_SIZE: - raise SSEError( - f"SSE line exceeded maximum size of {MAX_LINE_SIZE} characters without encountering a newline" - ) - - while "\n" in buf: - line, buf = buf.split("\n", 1) - sse = decoder.decode(line) - if sse is not None: - yield sse - - if buf.strip(): - sse = decoder.decode(buf) - if sse is not None: - yield sse - - def iter_sse(self) -> Iterator[ServerSentEvent]: - self._check_content_type() - decoder = SSEDecoder() - text_decoder = self._new_text_decoder() - - last_dispatched_id: Optional[str] = None - last_retry: Optional[int] = None - # Consecutive failed reconnection attempts. Reset to 0 whenever an event - # is successfully dispatched (reset-on-progress) — matching browser - # `EventSource` semantics: a server that emits >=1 event then drops on - # every connection can reconnect indefinitely. - reconnect_attempts = 0 - - # ``None`` means there is no live stream to read this iteration (e.g. a - # failed reconnect); the loop then re-evaluates the reconnect decision - # without re-reading an exhausted response. - response: Optional[httpx.Response] = self._response - # Context manager for a response we opened ourselves and must close. - # The initial response is owned by the caller, so it starts as None. - owned_cm: Optional[ContextManager[httpx.Response]] = None - try: - while True: - if response is not None: - events = self._decode_response(response, decoder, text_decoder) - while True: - try: - sse = next(events) - except StopIteration: - break - except SSEError: - # A protocol violation (e.g. an oversized line) is a - # genuine error, not a dropped connection; propagate it. - # Listed first because ``SSEError`` subclasses - # ``httpx.TransportError``. - raise - except httpx.TransportError: - # A transport error mid-stream (e.g. the server dropped - # the connection: ``ReadError``/``RemoteProtocolError``) - # is a premature end. Only swallow it when reconnection - # is configured for this stream; otherwise re-raise so a - # non-resumable stream still surfaces the error to the - # caller instead of looking like a clean completion. - # When reconnection is applicable but the attempt budget - # is exhausted, we ``break`` and end cleanly below — the - # same way an exhausted empty/error-body resume does, so - # give-up is consistent regardless of failure shape. - # ``next`` is used rather than ``for`` so this cannot - # swallow a ``GeneratorExit`` raised at a ``yield``. - if not self._reconnect_applicable(): - raise - break - yield sse - if sse.id: - last_dispatched_id = sse.id - if sse.retry is not None: - last_retry = sse.retry - reconnect_attempts = 0 - - if not self._should_reconnect(last_dispatched_id, reconnect_attempts): - return - reconnect_attempts += 1 - - self._sleep_before_reconnect(last_retry) - - # Close the previously-opened reconnect response before opening - # a new one so we never hold more than one extra connection. - if owned_cm is not None: - owned_cm.__exit__(None, None, None) - owned_cm = None - - assert self._reconnect is not None # guaranteed by _should_reconnect - try: - cm: ContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") - new_response = cm.__enter__() - except Exception: - # A failed reconnect consumes an attempt; back off and retry. - response = None - continue - owned_cm = cm - if new_response is None or not self._is_reconnect_response_usable(new_response): - # Null/empty body or a non-SSE/error response (e.g. 204/304, - # a 500, or an HTML error page): treat as a failed attempt. - response = None - continue - - response = new_response - # Drop any partial event left over from the dropped stream, but - # keep the last event id (per the SSE spec) and start a fresh - # incremental text decoder for the new connection. - decoder.reset_in_progress_event() - text_decoder = self._new_text_decoder(new_response) - finally: - if owned_cm is not None: - owned_cm.__exit__(None, None, None) - - async def aiter_sse(self) -> AsyncGenerator[ServerSentEvent, None]: - self._check_content_type() - decoder = SSEDecoder() - text_decoder = self._new_text_decoder() - - last_dispatched_id: Optional[str] = None - last_retry: Optional[int] = None - reconnect_attempts = 0 - - response: Optional[httpx.Response] = self._response - owned_cm: Optional[AsyncContextManager[httpx.Response]] = None - try: - while True: - if response is not None: - events = self._adecode_response(response, decoder, text_decoder) - while True: - try: - sse = await events.__anext__() - except StopAsyncIteration: - break - except SSEError: - # A protocol violation (e.g. an oversized line) is a - # genuine error, not a dropped connection; propagate it. - # Listed first because ``SSEError`` subclasses - # ``httpx.TransportError``. - raise - except httpx.TransportError: - # A transport error mid-stream (e.g. the server dropped - # the connection: ``ReadError``/``RemoteProtocolError``) - # is a premature end. Only swallow it when reconnection - # is configured for this stream; otherwise re-raise so a - # non-resumable stream still surfaces the error to the - # caller instead of looking like a clean completion. - # When reconnection is applicable but the attempt budget - # is exhausted, we ``break`` and end cleanly below — the - # same way an exhausted empty/error-body resume does, so - # give-up is consistent regardless of failure shape. - if not self._reconnect_applicable(): - raise - break - yield sse - if sse.id: - last_dispatched_id = sse.id - if sse.retry is not None: - last_retry = sse.retry - reconnect_attempts = 0 - - if not self._should_reconnect(last_dispatched_id, reconnect_attempts): - return - reconnect_attempts += 1 - - await self._asleep_before_reconnect(last_retry) - - if owned_cm is not None: - await owned_cm.__aexit__(None, None, None) - owned_cm = None - - assert self._reconnect is not None # guaranteed by _should_reconnect - try: - cm: AsyncContextManager[httpx.Response] = self._reconnect(last_dispatched_id or "") - new_response = await cm.__aenter__() - except Exception: - response = None - continue - owned_cm = cm - if new_response is None or not self._is_reconnect_response_usable(new_response): - response = None - continue - - response = new_response - decoder.reset_in_progress_event() - text_decoder = self._new_text_decoder(new_response) - finally: - if owned_cm is not None: - # Shield the close so a cancellation delivered while reading a - # reconnected response still fully tears the connection down - # instead of leaking it until the client is closed. - with anyio.CancelScope(shield=True): - await owned_cm.__aexit__(None, None, None) - - -@contextmanager -def connect_sse(client: httpx.Client, method: str, url: str, **kwargs: Any) -> Iterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) - - -@asynccontextmanager -async def aconnect_sse( - client: httpx.AsyncClient, - method: str, - url: str, - **kwargs: Any, -) -> AsyncIterator[EventSource]: - headers = kwargs.pop("headers", {}) - headers["Accept"] = "text/event-stream" - headers["Cache-Control"] = "no-store" - - async with client.stream(method, url, headers=headers, **kwargs) as response: - yield EventSource(response) diff --git a/src/vitable_connect/core/http_sse/_decoders.py b/src/vitable_connect/core/http_sse/_decoders.py deleted file mode 100644 index 1f6b35e..0000000 --- a/src/vitable_connect/core/http_sse/_decoders.py +++ /dev/null @@ -1,74 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import List, Optional - -from ._models import ServerSentEvent - - -class SSEDecoder: - def __init__(self) -> None: - self._event = "" - self._data: List[str] = [] - self._last_event_id = "" - self._retry: Optional[int] = None - - def reset_in_progress_event(self) -> None: - """Discard any partially-parsed (undispatched) event. - - Used when a stream ends mid-event before reconnecting: the buffered - ``event``/``data``/``retry`` fields of the never-dispatched event must - be dropped so they do not corrupt the first event of the reconnected - stream. Per the SSE spec the last event id is *not* reset here — it - persists across connections. - """ - self._event = "" - self._data = [] - self._retry = None - - def decode(self, line: str) -> Optional[ServerSentEvent]: - # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501 - - if not line: - if not self._event and not self._data and not self._last_event_id and self._retry is None: - return None - - sse = ServerSentEvent( - event=self._event, - data="\n".join(self._data), - id=self._last_event_id, - retry=self._retry, - ) - - # NOTE: as per the SSE spec, do not reset last_event_id. - self._event = "" - self._data = [] - self._retry = None - - return sse - - if line.startswith(":"): - return None - - fieldname, _, value = line.partition(":") - - if value.startswith(" "): - value = value[1:] - - if fieldname == "event": - self._event = value - elif fieldname == "data": - self._data.append(value) - elif fieldname == "id": - if "\0" in value: - pass - else: - self._last_event_id = value - elif fieldname == "retry": - try: - self._retry = int(value) - except (TypeError, ValueError): - pass - else: - pass # Field is ignored. - - return None diff --git a/src/vitable_connect/core/http_sse/_exceptions.py b/src/vitable_connect/core/http_sse/_exceptions.py deleted file mode 100644 index 81605a8..0000000 --- a/src/vitable_connect/core/http_sse/_exceptions.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import httpx - - -class SSEError(httpx.TransportError): - pass diff --git a/src/vitable_connect/core/http_sse/_models.py b/src/vitable_connect/core/http_sse/_models.py deleted file mode 100644 index 1af57f8..0000000 --- a/src/vitable_connect/core/http_sse/_models.py +++ /dev/null @@ -1,17 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import json -from dataclasses import dataclass -from typing import Any, Optional - - -@dataclass(frozen=True) -class ServerSentEvent: - event: str = "message" - data: str = "" - id: str = "" - retry: Optional[int] = None - - def json(self) -> Any: - """Parse the data field as JSON.""" - return json.loads(self.data) diff --git a/src/vitable_connect/core/jsonable_encoder.py b/src/vitable_connect/core/jsonable_encoder.py deleted file mode 100644 index f638cc9..0000000 --- a/src/vitable_connect/core/jsonable_encoder.py +++ /dev/null @@ -1,133 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -""" -jsonable_encoder converts a Python object to a JSON-friendly dict -(e.g. datetimes to strings, Pydantic models to dicts). - -Taken from FastAPI, and made a bit simpler -https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py -""" - -import base64 -import dataclasses -import datetime as dt -from enum import Enum -from pathlib import PurePath -from types import GeneratorType -from typing import Any, Callable, Dict, List, Optional, Set, Union -from urllib.parse import quote - -import pydantic -from .datetime_utils import serialize_datetime -from .pydantic_utilities import ( - IS_PYDANTIC_V2, - encode_by_type, - to_jsonable_with_fallback, -) - -SetIntStr = Set[Union[int, str]] -DictIntStrAny = Dict[Union[int, str], Any] - - -def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: - custom_encoder = custom_encoder or {} - # Generated SDKs use Ellipsis (`...`) as the sentinel value for "OMIT". - # OMIT values should be excluded from serialized payloads. - if obj is Ellipsis: - return None - if custom_encoder: - if type(obj) in custom_encoder: - return custom_encoder[type(obj)](obj) - else: - for encoder_type, encoder_instance in custom_encoder.items(): - if isinstance(obj, encoder_type): - return encoder_instance(obj) - if isinstance(obj, pydantic.BaseModel): - if IS_PYDANTIC_V2: - encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 - else: - encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 - if custom_encoder: - encoder.update(custom_encoder) - obj_dict = obj.dict(by_alias=True) - if "__root__" in obj_dict: - obj_dict = obj_dict["__root__"] - if "root" in obj_dict: - obj_dict = obj_dict["root"] - return jsonable_encoder(obj_dict, custom_encoder=encoder) - if dataclasses.is_dataclass(obj): - obj_dict = dataclasses.asdict(obj) # type: ignore - return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) - if isinstance(obj, bytes): - return base64.b64encode(obj).decode("utf-8") - if isinstance(obj, Enum): - return obj.value - if isinstance(obj, PurePath): - return str(obj) - if isinstance(obj, (str, int, float, type(None))): - return obj - if isinstance(obj, dt.datetime): - return serialize_datetime(obj) - if isinstance(obj, dt.date): - return str(obj) - if isinstance(obj, dict): - encoded_dict = {} - allowed_keys = set(obj.keys()) - for key, value in obj.items(): - if key in allowed_keys: - if value is Ellipsis: - continue - encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) - encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) - encoded_dict[encoded_key] = encoded_value - return encoded_dict - if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): - encoded_list = [] - for item in obj: - if item is Ellipsis: - continue - encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) - return encoded_list - - def fallback_serializer(o: Any) -> Any: - attempt_encode = encode_by_type(o) - if attempt_encode is not None: - return attempt_encode - - try: - data = dict(o) - except Exception as e: - errors: List[Exception] = [] - errors.append(e) - try: - data = vars(o) - except Exception as e: - errors.append(e) - raise ValueError(errors) from e - return jsonable_encoder(data, custom_encoder=custom_encoder) - - return to_jsonable_with_fallback(obj, fallback_serializer) - - -def encode_path_param(obj: Any) -> str: - """Encode a value for use in a URL path segment. - - Ensures proper string conversion for all types, including - booleans which need lowercase 'true'/'false' rather than - Python's 'True'/'False'. - """ - if isinstance(obj, bool): - return "true" if obj else "false" - return str(jsonable_encoder(obj)) - - -def quote_path_param(obj: Any) -> str: - """Encode a value for use in a URL path segment, percent-encoding it. - - Same as encode_path_param, except the result is percent-encoded so - that a value containing "/" or ".." cannot change which endpoint - the request resolves to. - """ - if isinstance(obj, bool): - return "true" if obj else "false" - return quote(str(jsonable_encoder(obj)), safe="") diff --git a/src/vitable_connect/core/logging.py b/src/vitable_connect/core/logging.py deleted file mode 100644 index e5e5724..0000000 --- a/src/vitable_connect/core/logging.py +++ /dev/null @@ -1,107 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import logging -import typing - -LogLevel = typing.Literal["debug", "info", "warn", "error"] - -_LOG_LEVEL_MAP: typing.Dict[LogLevel, int] = { - "debug": 1, - "info": 2, - "warn": 3, - "error": 4, -} - - -class ILogger(typing.Protocol): - def debug(self, message: str, **kwargs: typing.Any) -> None: ... - def info(self, message: str, **kwargs: typing.Any) -> None: ... - def warn(self, message: str, **kwargs: typing.Any) -> None: ... - def error(self, message: str, **kwargs: typing.Any) -> None: ... - - -class ConsoleLogger: - _logger: logging.Logger - - def __init__(self) -> None: - self._logger = logging.getLogger("fern") - if not self._logger.handlers: - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter("%(levelname)s - %(message)s")) - self._logger.addHandler(handler) - self._logger.setLevel(logging.DEBUG) - - def debug(self, message: str, **kwargs: typing.Any) -> None: - self._logger.debug(message, extra=kwargs) - - def info(self, message: str, **kwargs: typing.Any) -> None: - self._logger.info(message, extra=kwargs) - - def warn(self, message: str, **kwargs: typing.Any) -> None: - self._logger.warning(message, extra=kwargs) - - def error(self, message: str, **kwargs: typing.Any) -> None: - self._logger.error(message, extra=kwargs) - - -class LogConfig(typing.TypedDict, total=False): - level: LogLevel - logger: ILogger - silent: bool - - -class Logger: - _level: int - _logger: ILogger - _silent: bool - - def __init__(self, *, level: LogLevel, logger: ILogger, silent: bool) -> None: - self._level = _LOG_LEVEL_MAP[level] - self._logger = logger - self._silent = silent - - def _should_log(self, level: LogLevel) -> bool: - return not self._silent and self._level <= _LOG_LEVEL_MAP[level] - - def is_debug(self) -> bool: - return self._should_log("debug") - - def is_info(self) -> bool: - return self._should_log("info") - - def is_warn(self) -> bool: - return self._should_log("warn") - - def is_error(self) -> bool: - return self._should_log("error") - - def debug(self, message: str, **kwargs: typing.Any) -> None: - if self.is_debug(): - self._logger.debug(message, **kwargs) - - def info(self, message: str, **kwargs: typing.Any) -> None: - if self.is_info(): - self._logger.info(message, **kwargs) - - def warn(self, message: str, **kwargs: typing.Any) -> None: - if self.is_warn(): - self._logger.warn(message, **kwargs) - - def error(self, message: str, **kwargs: typing.Any) -> None: - if self.is_error(): - self._logger.error(message, **kwargs) - - -_default_logger: Logger = Logger(level="info", logger=ConsoleLogger(), silent=True) - - -def create_logger(config: typing.Optional[typing.Union[LogConfig, Logger]] = None) -> Logger: - if config is None: - return _default_logger - if isinstance(config, Logger): - return config - return Logger( - level=config.get("level", "info"), - logger=config.get("logger", ConsoleLogger()), - silent=config.get("silent", True), - ) diff --git a/src/vitable_connect/core/pagination.py b/src/vitable_connect/core/pagination.py deleted file mode 100644 index 760b089..0000000 --- a/src/vitable_connect/core/pagination.py +++ /dev/null @@ -1,82 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -from dataclasses import dataclass -from typing import AsyncIterator, Awaitable, Callable, Generic, Iterator, List, Optional, TypeVar - -# Generic to represent the underlying type of the results within a page -T = TypeVar("T") -# Generic to represent the type of the API response -R = TypeVar("R") - - -# SDKs implement a Page ABC per-pagination request, the endpoint then returns a pager that wraps this type -# for example, an endpoint will return SyncPager[UserPage] where UserPage implements the Page ABC. ex: -# -# SyncPager( -# has_next=response.list_metadata.after is not None, -# items=response.data, -# # This should be the outer function that returns the SyncPager again -# get_next=lambda: list(..., cursor: response.cursor) (or list(..., offset: offset + 1)) -# ) - - -@dataclass(frozen=True) -class SyncPager(Generic[T, R]): - get_next: Optional[Callable[[], Optional[SyncPager[T, R]]]] - has_next: bool - items: Optional[List[T]] - response: R - - # Here we type ignore the iterator to avoid a mypy error - # caused by the type conflict with Pydanitc's __iter__ method - # brought in by extending the base model - def __iter__(self) -> Iterator[T]: # type: ignore[override] - for page in self.iter_pages(): - if page.items is not None: - yield from page.items - - def iter_pages(self) -> Iterator[SyncPager[T, R]]: - page: Optional[SyncPager[T, R]] = self - while page is not None: - yield page - - if not page.has_next or page.get_next is None: - return - - page = page.get_next() - if page is None or page.items is None or len(page.items) == 0: - return - - def next_page(self) -> Optional[SyncPager[T, R]]: - return self.get_next() if self.get_next is not None else None - - -@dataclass(frozen=True) -class AsyncPager(Generic[T, R]): - get_next: Optional[Callable[[], Awaitable[Optional[AsyncPager[T, R]]]]] - has_next: bool - items: Optional[List[T]] - response: R - - async def __aiter__(self) -> AsyncIterator[T]: - async for page in self.iter_pages(): - if page.items is not None: - for item in page.items: - yield item - - async def iter_pages(self) -> AsyncIterator[AsyncPager[T, R]]: - page: Optional[AsyncPager[T, R]] = self - while page is not None: - yield page - - if not page.has_next or page.get_next is None: - return - - page = await page.get_next() - if page is None or page.items is None or len(page.items) == 0: - return - - async def next_page(self) -> Optional[AsyncPager[T, R]]: - return await self.get_next() if self.get_next is not None else None diff --git a/src/vitable_connect/core/parse_error.py b/src/vitable_connect/core/parse_error.py deleted file mode 100644 index 4527c6a..0000000 --- a/src/vitable_connect/core/parse_error.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Any, Dict, Optional - - -class ParsingError(Exception): - """ - Raised when the SDK fails to parse/validate a response from the server. - This typically indicates that the server returned a response whose shape - does not match the expected schema. - """ - - headers: Optional[Dict[str, str]] - status_code: Optional[int] - body: Any - cause: Optional[Exception] - - def __init__( - self, - *, - headers: Optional[Dict[str, str]] = None, - status_code: Optional[int] = None, - body: Any = None, - cause: Optional[Exception] = None, - ) -> None: - self.headers = headers - self.status_code = status_code - self.body = body - self.cause = cause - super().__init__() - if cause is not None: - self.__cause__ = cause - - def __str__(self) -> str: - cause_str = f", cause: {self.cause}" if self.cause is not None else "" - return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}{cause_str}" diff --git a/src/vitable_connect/core/pydantic_utilities.py b/src/vitable_connect/core/pydantic_utilities.py deleted file mode 100644 index 70816b9..0000000 --- a/src/vitable_connect/core/pydantic_utilities.py +++ /dev/null @@ -1,486 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# nopycln: file -import datetime as dt -import inspect -import json -import logging -import weakref -from collections import defaultdict -from dataclasses import asdict -from typing import ( - TYPE_CHECKING, - Any, - Callable, - ClassVar, - Dict, - List, - Mapping, - Optional, - Set, - Tuple, - Type, - TypeVar, - Union, - cast, -) - -import pydantic -import typing_extensions -from pydantic.fields import FieldInfo as _FieldInfo - -_logger = logging.getLogger(__name__) - -if TYPE_CHECKING: - from .http_sse._models import ServerSentEvent - -IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") - -if IS_PYDANTIC_V2: - _datetime_adapter = pydantic.TypeAdapter(dt.datetime) # type: ignore[attr-defined] - _date_adapter = pydantic.TypeAdapter(dt.date) # type: ignore[attr-defined] - - def parse_datetime(value: Any) -> dt.datetime: # type: ignore[misc] - if isinstance(value, dt.datetime): - return value - return _datetime_adapter.validate_python(value) - - def parse_date(value: Any) -> dt.date: # type: ignore[misc] - if isinstance(value, dt.datetime): - return value.date() - if isinstance(value, dt.date): - return value - return _date_adapter.validate_python(value) - - # Avoid importing from pydantic.v1 to maintain Python 3.14 compatibility. - from typing import get_args as get_args # type: ignore[assignment] - from typing import get_origin as get_origin # type: ignore[assignment] - - def is_literal_type(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] - return typing_extensions.get_origin(tp) is typing_extensions.Literal - - def is_union(tp: Optional[Type[Any]]) -> bool: # type: ignore[misc] - return tp is Union or typing_extensions.get_origin(tp) is Union # type: ignore[comparison-overlap] - - # Inline encoders_by_type to avoid importing from pydantic.v1.json - import re as _re - from collections import deque as _deque - from decimal import Decimal as _Decimal - from enum import Enum as _Enum - from ipaddress import ( - IPv4Address as _IPv4Address, - ) - from ipaddress import ( - IPv4Interface as _IPv4Interface, - ) - from ipaddress import ( - IPv4Network as _IPv4Network, - ) - from ipaddress import ( - IPv6Address as _IPv6Address, - ) - from ipaddress import ( - IPv6Interface as _IPv6Interface, - ) - from ipaddress import ( - IPv6Network as _IPv6Network, - ) - from pathlib import Path as _Path - from types import GeneratorType as _GeneratorType - from uuid import UUID as _UUID - - from pydantic.fields import FieldInfo as ModelField # type: ignore[no-redef, assignment] - - def _decimal_encoder(dec_value: Any) -> Any: - if dec_value.as_tuple().exponent >= 0: - return int(dec_value) - return float(dec_value) - - encoders_by_type: Dict[Type[Any], Callable[[Any], Any]] = { # type: ignore[no-redef] - bytes: lambda o: o.decode(), - dt.date: lambda o: o.isoformat(), - dt.datetime: lambda o: o.isoformat(), - dt.time: lambda o: o.isoformat(), - dt.timedelta: lambda td: td.total_seconds(), - _Decimal: _decimal_encoder, - _Enum: lambda o: o.value, - frozenset: list, - _deque: list, - _GeneratorType: list, - _IPv4Address: str, - _IPv4Interface: str, - _IPv4Network: str, - _IPv6Address: str, - _IPv6Interface: str, - _IPv6Network: str, - _Path: str, - _re.Pattern: lambda o: o.pattern, - set: list, - _UUID: str, - } -else: - from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] - from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] - from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef, assignment] - from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] - from pydantic.typing import get_args as get_args # type: ignore[no-redef] - from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] - from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef, assignment] - from pydantic.typing import is_union as is_union # type: ignore[no-redef] - -from .datetime_utils import serialize_datetime -from .serialization import convert_and_respect_annotation_metadata -from typing_extensions import TypeAlias - -T = TypeVar("T") -Model = TypeVar("Model", bound=pydantic.BaseModel) - - -def parse_sse_obj(sse: "ServerSentEvent", type_: Type[T]) -> T: - """ - Parse a ServerSentEvent into the appropriate type. - - This function handles data-level discrimination where the discriminator - (e.g., 'type') is inside the 'data' payload. It parses the SSE data field - as JSON and deserializes it into the target type. - - Note: Protocol-level discrimination (where the discriminator comes from - the SSE event: field) is handled at code-generation time and does not - use this function. - - Args: - sse: The ServerSentEvent object to parse - type_: The target type to deserialize into - - Returns: - The parsed object of type T - - Note: - This function is only available in SDK contexts where http_sse module exists. - """ - sse_event = asdict(sse) - data_value = sse_event.get("data") - if isinstance(data_value, str) and data_value: - try: - parsed_data = json.loads(data_value) - return parse_obj_as(type_, parsed_data) - except json.JSONDecodeError as e: - _logger.warning( - "Failed to parse SSE data field as JSON: %s, data: %s", - e, - data_value[:100] if len(data_value) > 100 else data_value, - ) - return parse_obj_as(type_, sse_event) - - -_type_adapter_cache: Dict[int, Any] = {} - - -def _get_type_adapter(type_: Type[Any]) -> Any: - key = id(type_) - adapter = _type_adapter_cache.get(key) - if adapter is None: - adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] - _type_adapter_cache[key] = adapter - return adapter - - -_field_alias_cache: "weakref.WeakKeyDictionary[type, Tuple[Dict[str, str], Tuple[str, ...]]]" = ( - weakref.WeakKeyDictionary() -) - - -def _get_field_aliases(model: type) -> Tuple[Dict[str, str], Tuple[str, ...]]: - """ - Map of field name to Pydantic alias for the fields whose alias differs from their name, together with the - keys that are ambiguous (an alias of one field and the name of another). Computed once per model class. - """ - cached = _field_alias_cache.get(model) - if cached is None: - fields: Mapping[str, Any] = ( - getattr(model, "model_fields", {}) if IS_PYDANTIC_V2 else getattr(model, "__fields__", {}) - ) - name_to_alias: Dict[str, str] = {} - for name, field in fields.items(): - alias = getattr(field, "alias", None) - if alias is not None and alias != name: - name_to_alias[name] = alias - cached = (name_to_alias, tuple(alias for alias in name_to_alias.values() if alias in fields)) - _field_alias_cache[model] = cached - return cached - - -def _coerce_keys_to_aliases(model: type, data: Any) -> Any: - """ - Accept Python field names in input by rewriting them to their Pydantic aliases, - while avoiding silent collisions when a key could refer to multiple fields. - """ - if not isinstance(data, Mapping): - return data - - name_to_alias, ambiguous_keys = _get_field_aliases(model) - for key in ambiguous_keys: - if key in data and name_to_alias.get(key, key) not in data: - raise ValueError( - f"Ambiguous input key '{key}': it is both a field name and an alias. " - "Provide the explicit alias key to disambiguate." - ) - - if not name_to_alias or not any(name in data for name in name_to_alias): - return data if isinstance(data, dict) else dict(data) - - rewritten: Dict[str, Any] = dict(data) - for name, alias in name_to_alias.items(): - if name in data and alias not in rewritten: - rewritten[alias] = rewritten.pop(name) - - return rewritten - - -def parse_obj_as(type_: Type[T], object_: Any) -> T: - # convert_and_respect_annotation_metadata is required for TypedDict aliasing. - # - # For Pydantic models, whether we should pre-dealias depends on how the model encodes aliasing: - # - If the model uses real Pydantic aliases (pydantic.Field(alias=...)), then we must pass wire keys through - # unchanged so Pydantic can validate them. - # - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic - # will not recognize those aliases during validation. - if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel): - has_pydantic_aliases = bool(_get_field_aliases(type_)[0]) - - dealiased_object = ( - object_ - if has_pydantic_aliases - else convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") - ) - else: - dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") - if IS_PYDANTIC_V2: - adapter = _get_type_adapter(type_) - return adapter.validate_python(dealiased_object) # type: ignore[no-any-return] - return pydantic.parse_obj_as(type_, dealiased_object) - - -def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: - if IS_PYDANTIC_V2: - from pydantic_core import to_jsonable_python - - return to_jsonable_python(obj, fallback=fallback_serializer) - return fallback_serializer(obj) - - -class UniversalBaseModel(pydantic.BaseModel): - if IS_PYDANTIC_V2: - model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] - # Allow fields beginning with `model_` to be used in the model - protected_namespaces=(), - ) - - @pydantic.model_validator(mode="before") # type: ignore[attr-defined] - @classmethod - def _coerce_field_names_to_aliases(cls, data: Any) -> Any: - return _coerce_keys_to_aliases(cls, data) - - @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] - def serialize_model(self) -> Any: # type: ignore[name-defined] - serialized = self.dict() # type: ignore[attr-defined] - data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} - return data - - else: - - class Config: - smart_union = True - json_encoders = {dt.datetime: serialize_datetime} - - @pydantic.root_validator(pre=True) - def _coerce_field_names_to_aliases(cls, values: Any) -> Any: - return _coerce_keys_to_aliases(cls, values) # type: ignore[arg-type] - - @classmethod - def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": - dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") - return cls.construct(_fields_set, **dealiased_object) - - @classmethod - def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": - dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") - if IS_PYDANTIC_V2: - return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] - return super().construct(_fields_set, **dealiased_object) - - def json(self, **kwargs: Any) -> str: - kwargs_with_defaults = { - "by_alias": True, - "exclude_unset": True, - **kwargs, - } - if IS_PYDANTIC_V2: - return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] - return super().json(**kwargs_with_defaults) - - def dict(self, **kwargs: Any) -> Dict[str, Any]: - """ - Override the default dict method to `exclude_unset` by default. This function patches - `exclude_unset` to work include fields within non-None default values. - """ - # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 - # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. - # - # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models - # that we have less control over, and this is less intrusive than custom serializers for now. - if IS_PYDANTIC_V2: - kwargs_with_defaults_exclude_unset = { - **kwargs, - "by_alias": True, - "exclude_unset": True, - "exclude_none": False, - } - kwargs_with_defaults_exclude_none = { - **kwargs, - "by_alias": True, - "exclude_none": True, - "exclude_unset": False, - } - dict_dump = deep_union_pydantic_dicts( - super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] - super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] - ) - - else: - _fields_set = self.__fields_set__.copy() - - fields = _get_model_fields(self.__class__) - for name, field in fields.items(): - if name not in _fields_set: - default = _get_field_default(field) - - # If the default values are non-null act like they've been set - # This effectively allows exclude_unset to work like exclude_none where - # the latter passes through intentionally set none values. - if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): - _fields_set.add(name) - - if default is not None: - self.__fields_set__.add(name) - - kwargs_with_defaults_exclude_unset_include_fields = { - "by_alias": True, - "exclude_unset": True, - "include": _fields_set, - **kwargs, - } - - dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) - - return cast( - Dict[str, Any], - convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write"), - ) - - -def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: - converted_list: List[Any] = [] - for i, item in enumerate(source): - destination_value = destination[i] - if isinstance(item, dict): - converted_list.append(deep_union_pydantic_dicts(item, destination_value)) - elif isinstance(item, list): - converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) - else: - converted_list.append(item) - return converted_list - - -def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: - for key, value in source.items(): - node = destination.setdefault(key, {}) - if isinstance(value, dict): - deep_union_pydantic_dicts(value, node) - # Note: we do not do this same processing for sets given we do not have sets of models - # and given the sets are unordered, the processing of the set and matching objects would - # be non-trivial. - elif isinstance(value, list): - destination[key] = _union_list_of_pydantic_dicts(value, node) - else: - destination[key] = value - - return destination - - -if IS_PYDANTIC_V2: - - class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] - pass - - UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] -else: - UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] - - -def encode_by_type(o: Any) -> Any: - encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) - for type_, encoder in encoders_by_type.items(): - encoders_by_class_tuples[encoder] += (type_,) - - if type(o) in encoders_by_type: - return encoders_by_type[type(o)](o) - for encoder, classes_tuple in encoders_by_class_tuples.items(): - if isinstance(o, classes_tuple): - return encoder(o) - - -def update_forward_refs(model: Type["Model"], **localns: Any) -> None: - if IS_PYDANTIC_V2: - model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] - else: - model.update_forward_refs(**localns) - - -# Mirrors Pydantic's internal typing -AnyCallable = Callable[..., Any] - - -def universal_root_validator( - pre: bool = False, -) -> Callable[[AnyCallable], AnyCallable]: - def decorator(func: AnyCallable) -> AnyCallable: - if IS_PYDANTIC_V2: - # In Pydantic v2, for RootModel we always use "before" mode - # The custom validators transform the input value before the model is created - return cast(AnyCallable, pydantic.model_validator(mode="before")(func)) # type: ignore[attr-defined] - return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] - - return decorator - - -def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: - def decorator(func: AnyCallable) -> AnyCallable: - if IS_PYDANTIC_V2: - return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] - return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) - - return decorator - - -PydanticField = Union[ModelField, _FieldInfo] - - -def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: - if IS_PYDANTIC_V2: - return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] - return cast(Mapping[str, PydanticField], model.__fields__) - - -def _get_field_default(field: PydanticField) -> Any: - try: - value = field.get_default() # type: ignore[union-attr] - except: - value = field.default - if IS_PYDANTIC_V2: - from pydantic_core import PydanticUndefined - - if value == PydanticUndefined: - return None - return value - return value diff --git a/src/vitable_connect/core/query_encoder.py b/src/vitable_connect/core/query_encoder.py deleted file mode 100644 index 3183001..0000000 --- a/src/vitable_connect/core/query_encoder.py +++ /dev/null @@ -1,58 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Any, Dict, List, Optional, Tuple - -import pydantic - - -# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict -def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: - result = [] - for k, v in dict_flat.items(): - key = f"{key_prefix}[{k}]" if key_prefix is not None else k - if isinstance(v, dict): - result.extend(traverse_query_dict(v, key)) - elif isinstance(v, list): - for arr_v in v: - if isinstance(arr_v, dict): - result.extend(traverse_query_dict(arr_v, key)) - else: - result.append((key, arr_v)) - else: - result.append((key, v)) - return result - - -def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: - if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): - if isinstance(query_value, pydantic.BaseModel): - obj_dict = query_value.dict(by_alias=True) - else: - obj_dict = query_value - return traverse_query_dict(obj_dict, query_key) - elif isinstance(query_value, list): - encoded_values: List[Tuple[str, Any]] = [] - for value in query_value: - if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): - if isinstance(value, pydantic.BaseModel): - obj_dict = value.dict(by_alias=True) - elif isinstance(value, dict): - obj_dict = value - - encoded_values.extend(single_query_encoder(query_key, obj_dict)) - else: - encoded_values.append((query_key, value)) - - return encoded_values - - return [(query_key, query_value)] - - -def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: - if query is None: - return None - - encoded_query = [] - for k, v in query.items(): - encoded_query.extend(single_query_encoder(k, v)) - return encoded_query diff --git a/src/vitable_connect/core/remove_none_from_dict.py b/src/vitable_connect/core/remove_none_from_dict.py deleted file mode 100644 index c229814..0000000 --- a/src/vitable_connect/core/remove_none_from_dict.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Any, Dict, Mapping, Optional - - -def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: - new: Dict[str, Any] = {} - for key, value in original.items(): - if value is not None: - new[key] = value - return new diff --git a/src/vitable_connect/core/request_options.py b/src/vitable_connect/core/request_options.py deleted file mode 100644 index caa6f66..0000000 --- a/src/vitable_connect/core/request_options.py +++ /dev/null @@ -1,40 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -try: - from typing import NotRequired # type: ignore -except ImportError: - from typing_extensions import NotRequired - - -class RequestOptions(typing.TypedDict, total=False): - """ - Additional options for request-specific configuration when calling APIs via the SDK. - This is used primarily as an optional final parameter for service functions. - - Attributes: - - timeout: float. The number of seconds to await an API call before timing out. - - - timeout_in_seconds: int. Deprecated alias for `timeout`; both are in seconds. Prefer `timeout`. - - - max_retries: int. The max number of retries to attempt if the API call fails. - - - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict - - - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict - - - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict - - - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. - """ - - timeout: NotRequired[float] - timeout_in_seconds: NotRequired[int] - max_retries: NotRequired[int] - additional_headers: NotRequired[typing.Dict[str, typing.Any]] - additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] - additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] - chunk_size: NotRequired[int] - stream_reconnection_enabled: NotRequired[bool] - max_stream_reconnection_attempts: NotRequired[int] diff --git a/src/vitable_connect/core/serialization.py b/src/vitable_connect/core/serialization.py deleted file mode 100644 index 1d753e2..0000000 --- a/src/vitable_connect/core/serialization.py +++ /dev/null @@ -1,347 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import collections -import inspect -import typing - -import pydantic -import typing_extensions - - -class FieldMetadata: - """ - Metadata class used to annotate fields to provide additional information. - - Example: - class MyDict(TypedDict): - field: typing.Annotated[str, FieldMetadata(alias="field_name")] - - Will serialize: `{"field": "value"}` - To: `{"field_name": "value"}` - """ - - alias: str - - def __init__(self, *, alias: str) -> None: - self.alias = alias - - -# Resolving type hints (typing.get_type_hints) is expensive because it eval/compiles -# forward-reference annotations. The result is constant for a given type, so we cache it. -# This is critical for hot paths like SSE event parsing, where the same (often large -# discriminated-union) type is converted on every single event. -_type_hints_cache: typing.Dict[typing.Any, typing.Dict[str, typing.Any]] = {} - - -def _get_cached_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: - try: - cached = _type_hints_cache.get(expected_type) - except TypeError: - # Unhashable type; resolve without caching. - return _resolve_type_hints(expected_type) - if cached is None: - cached = _resolve_type_hints(expected_type) - _type_hints_cache[expected_type] = cached - return cached - - -def _resolve_type_hints(expected_type: typing.Any) -> typing.Dict[str, typing.Any]: - try: - return typing_extensions.get_type_hints(expected_type, include_extras=True) - except NameError: - # The type contains a circular reference, so we use the __annotations__ attribute directly. - return getattr(expected_type, "__annotations__", {}) - - -# Whether convert_and_respect_annotation_metadata can possibly rewrite anything for a given -# annotation, i.e. whether any reachable model/TypedDict field carries a FieldMetadata alias. -# This is constant per type, so we cache it and use it to short-circuit the recursive walk. -_requires_conversion_cache: typing.Dict[typing.Any, bool] = {} - - -def _requires_conversion(type_: typing.Any) -> bool: - try: - cached = _requires_conversion_cache.get(type_) - except TypeError: - # Unhashable annotation; compute without caching. - return _compute_requires_conversion(type_, set()) - if cached is None: - cached = _compute_requires_conversion(type_, set()) - _requires_conversion_cache[type_] = cached - return cached - - -def _compute_requires_conversion(type_: typing.Any, seen: typing.Set[typing.Any]) -> bool: - clean_type = _remove_annotations(type_) - - try: - if clean_type in seen: - return False - seen = seen | {clean_type} - except TypeError: - # Unhashable type; skip cycle tracking (the type graph is finite in practice). - pass - - # Models / TypedDicts: a field alias here means we must dealias; otherwise recurse into fields. - if (inspect.isclass(clean_type) and issubclass(clean_type, pydantic.BaseModel)) or typing_extensions.is_typeddict( - clean_type - ): - annotations = _get_cached_type_hints(clean_type) - if _get_alias_to_field_name(annotations): - return True - return any(_compute_requires_conversion(hint, seen) for hint in annotations.values()) - - # Containers / unions: recurse into the type arguments (List/Set/Sequence/Dict/Union/etc.). - return any(_compute_requires_conversion(arg, seen) for arg in typing_extensions.get_args(clean_type)) - - -def convert_and_respect_annotation_metadata( - *, - object_: typing.Any, - annotation: typing.Any, - inner_type: typing.Optional[typing.Any] = None, - direction: typing.Literal["read", "write"], -) -> typing.Any: - """ - Respect the metadata annotations on a field, such as aliasing. This function effectively - manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for - TypedDicts, which cannot support aliasing out of the box, and can be extended for additional - utilities, such as defaults. - - Parameters - ---------- - object_ : typing.Any - - annotation : type - The type we're looking to apply typing annotations from - - inner_type : typing.Optional[type] - - Returns - ------- - typing.Any - """ - - if object_ is None: - return None - if inner_type is None: - inner_type = annotation - # The only thing this function ever rewrites is keys that carry a FieldMetadata - # alias. If nothing in the (cached) type graph has such an alias, the conversion is - # a content-identity transform, so we can skip the entire recursive walk. This is - # the hot path for SSE streaming, where a large discriminated union would otherwise - # be traversed on every single event. - if not _requires_conversion(annotation): - return object_ - - clean_type = _remove_annotations(inner_type) - # Pydantic models - if ( - inspect.isclass(clean_type) - and issubclass(clean_type, pydantic.BaseModel) - and isinstance(object_, typing.Mapping) - ): - return _convert_mapping(object_, clean_type, direction) - # TypedDicts - if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): - return _convert_mapping(object_, clean_type, direction) - - if ( - typing_extensions.get_origin(clean_type) == typing.Dict - or typing_extensions.get_origin(clean_type) == dict - or clean_type == typing.Dict - ) and isinstance(object_, typing.Dict): - key_type = typing_extensions.get_args(clean_type)[0] - value_type = typing_extensions.get_args(clean_type)[1] - - return { - key: convert_and_respect_annotation_metadata( - object_=value, - annotation=annotation, - inner_type=value_type, - direction=direction, - ) - for key, value in object_.items() - } - - # If you're iterating on a string, do not bother to coerce it to a sequence. - if not isinstance(object_, str): - if ( - typing_extensions.get_origin(clean_type) == typing.Set - or typing_extensions.get_origin(clean_type) == set - or clean_type == typing.Set - ) and isinstance(object_, typing.Set): - inner_type = typing_extensions.get_args(clean_type)[0] - return { - convert_and_respect_annotation_metadata( - object_=item, - annotation=annotation, - inner_type=inner_type, - direction=direction, - ) - for item in object_ - } - elif ( - ( - typing_extensions.get_origin(clean_type) == typing.List - or typing_extensions.get_origin(clean_type) == list - or clean_type == typing.List - ) - and isinstance(object_, typing.List) - ) or ( - ( - typing_extensions.get_origin(clean_type) == typing.Sequence - or typing_extensions.get_origin(clean_type) == collections.abc.Sequence - or clean_type == typing.Sequence - ) - and isinstance(object_, typing.Sequence) - ): - inner_type = typing_extensions.get_args(clean_type)[0] - return [ - convert_and_respect_annotation_metadata( - object_=item, - annotation=annotation, - inner_type=inner_type, - direction=direction, - ) - for item in object_ - ] - - if typing_extensions.get_origin(clean_type) == typing.Union: - # We should be able to ~relatively~ safely try to convert keys against all - # member types in the union, the edge case here is if one member aliases a field - # of the same name to a different name from another member - # Or if another member aliases a field of the same name that another member does not. - for member in typing_extensions.get_args(clean_type): - object_ = convert_and_respect_annotation_metadata( - object_=object_, - annotation=annotation, - inner_type=member, - direction=direction, - ) - return object_ - - annotated_type = _get_annotation(annotation) - if annotated_type is None: - return object_ - - # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) - # Then we can safely call it on the recursive conversion. - return object_ - - -def _convert_mapping( - object_: typing.Mapping[str, object], - expected_type: typing.Any, - direction: typing.Literal["read", "write"], -) -> typing.Mapping[str, object]: - converted_object: typing.Dict[str, object] = {} - annotations = _get_cached_type_hints(expected_type) - aliases_to_field_names = _get_alias_to_field_name(annotations) - for key, value in object_.items(): - if direction == "read" and key in aliases_to_field_names: - dealiased_key = aliases_to_field_names.get(key) - if dealiased_key is not None: - type_ = annotations.get(dealiased_key) - else: - type_ = annotations.get(key) - # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map - # - # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias - # then we can just pass the value through as is - if type_ is None: - converted_object[key] = value - elif direction == "read" and key not in aliases_to_field_names: - converted_object[key] = convert_and_respect_annotation_metadata( - object_=value, annotation=type_, direction=direction - ) - else: - converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( - convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) - ) - return converted_object - - -def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: - maybe_annotated_type = typing_extensions.get_origin(type_) - if maybe_annotated_type is None: - return None - - if maybe_annotated_type == typing_extensions.NotRequired: - type_ = typing_extensions.get_args(type_)[0] - maybe_annotated_type = typing_extensions.get_origin(type_) - - if maybe_annotated_type == typing_extensions.Annotated: - return type_ - - return None - - -def _remove_annotations(type_: typing.Any) -> typing.Any: - maybe_annotated_type = typing_extensions.get_origin(type_) - if maybe_annotated_type is None: - return type_ - - if maybe_annotated_type == typing_extensions.NotRequired: - return _remove_annotations(typing_extensions.get_args(type_)[0]) - - if maybe_annotated_type == typing_extensions.Annotated: - return _remove_annotations(typing_extensions.get_args(type_)[0]) - - return type_ - - -def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: - annotations = _get_cached_type_hints(type_) - return _get_alias_to_field_name(annotations) - - -def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: - annotations = _get_cached_type_hints(type_) - return _get_field_to_alias_name(annotations) - - -def _get_alias_to_field_name( - field_to_hint: typing.Dict[str, typing.Any], -) -> typing.Dict[str, str]: - aliases = {} - for field, hint in field_to_hint.items(): - maybe_alias = _get_alias_from_type(hint) - if maybe_alias is not None: - aliases[maybe_alias] = field - return aliases - - -def _get_field_to_alias_name( - field_to_hint: typing.Dict[str, typing.Any], -) -> typing.Dict[str, str]: - aliases = {} - for field, hint in field_to_hint.items(): - maybe_alias = _get_alias_from_type(hint) - if maybe_alias is not None: - aliases[field] = maybe_alias - return aliases - - -def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: - maybe_annotated_type = _get_annotation(type_) - - if maybe_annotated_type is not None: - # The actual annotations are 1 onward, the first is the annotated type - annotations = typing_extensions.get_args(maybe_annotated_type)[1:] - - for annotation in annotations: - if isinstance(annotation, FieldMetadata) and annotation.alias is not None: - return annotation.alias - return None - - -def _alias_key( - key: str, - type_: typing.Any, - direction: typing.Literal["read", "write"], - aliases_to_field_names: typing.Dict[str, str], -) -> str: - if direction == "read": - return aliases_to_field_names.get(key, key) - return _get_alias_from_type(type_=type_) or key diff --git a/src/vitable_connect/employees/__init__.py b/src/vitable_connect/employees/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/employees/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/employees/client.py b/src/vitable_connect/employees/client.py deleted file mode 100644 index 0266726..0000000 --- a/src/vitable_connect/employees/client.py +++ /dev/null @@ -1,456 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.pagination import AsyncPager, SyncPager -from ..core.request_options import RequestOptions -from ..types.compensation_type import CompensationType -from ..types.employee_address_input import EmployeeAddressInput -from ..types.employee_class import EmployeeClass -from ..types.employee_id import EmployeeId -from ..types.employee_response import EmployeeResponse -from ..types.enrollment import Enrollment -from ..types.enrollment_list_response import EnrollmentListResponse -from ..types.gender import Gender -from ..types.limit import Limit -from ..types.page import Page -from .raw_client import AsyncRawEmployeesClient, RawEmployeesClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class EmployeesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawEmployeesClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawEmployeesClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawEmployeesClient - """ - return self._raw_client - - def get( - self, employee_id: EmployeeId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployeeResponse: - """ - Retrieves detailed information for a specific employee by ID. Returns employee details including personal information, employment status, classification and compensation-type effective dates, compensation type, and payroll deductions from the most recent statement period. Deductions reflect a snapshot of the current period and are replaced when a new statement is generated. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployeeResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employees.get( - employee_id="empl_abc123def456", - ) - """ - _response = self._raw_client.get(employee_id, request_options=request_options) - return _response.data - - def update( - self, - employee_id: EmployeeId, - *, - effective_date: dt.date, - email: typing.Optional[str] = OMIT, - phone: typing.Optional[str] = OMIT, - gender: typing.Optional[Gender] = OMIT, - address: typing.Optional[EmployeeAddressInput] = OMIT, - employee_class: typing.Optional[EmployeeClass] = OMIT, - start_date: typing.Optional[dt.date] = OMIT, - compensation_type: typing.Optional[CompensationType] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployeeResponse: - """ - Updates employee personal, contact, address, and employment fields. This endpoint currently supports email, phone, gender, address, employee_class, start_date, and compensation_type. effective_date is required and applies to employee_class and compensation_type when those fields are included in the request. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - effective_date : dt.date - Past or present date applied to each tracked employment field included in this request - - email : typing.Optional[str] - Email address - - phone : typing.Optional[str] - Phone number - - gender : typing.Optional[Gender] - Gender identity - - * `Male` - Male - * `Female` - Female - * `Transgender` - Transgender - * `Non-binary` - Non-binary - * `Prefer not to respond` - Prefer not to respond - - address : typing.Optional[EmployeeAddressInput] - Employee's residential address - - employee_class : typing.Optional[EmployeeClass] - Employment classification - - * `Full Time` - Full Time - * `Part Time` - Part Time - * `Temporary` - Temporary - * `Intern` - Intern - * `Seasonal` - Seasonal - * `Individual Contractor` - Individual Contractor - - start_date : typing.Optional[dt.date] - Employment start date - - compensation_type : typing.Optional[CompensationType] - Employee compensation type - - * `Salary` - Salary - * `Hourly` - Hourly - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployeeResponse - - - Examples - -------- - import datetime - - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employees.update( - employee_id="empl_abc123def456", - employee_class="Full Time", - start_date=datetime.date.fromisoformat( - "2023-01-15", - ), - compensation_type="Salary", - effective_date=datetime.date.fromisoformat( - "2023-03-01", - ), - ) - """ - _response = self._raw_client.update( - employee_id, - effective_date=effective_date, - email=email, - phone=phone, - gender=gender, - address=address, - employee_class=employee_class, - start_date=start_date, - compensation_type=compensation_type, - request_options=request_options, - ) - return _response.data - - def list_enrollments( - self, - employee_id: EmployeeId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[Enrollment, EnrollmentListResponse]: - """ - Retrieves a paginated list of benefit enrollments for an employee. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[Enrollment, EnrollmentListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.employees.list_enrollments( - employee_id="empl_abc123def456", - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list_enrollments(employee_id, limit=limit, page=page, request_options=request_options) - - -class AsyncEmployeesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawEmployeesClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawEmployeesClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawEmployeesClient - """ - return self._raw_client - - async def get( - self, employee_id: EmployeeId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployeeResponse: - """ - Retrieves detailed information for a specific employee by ID. Returns employee details including personal information, employment status, classification and compensation-type effective dates, compensation type, and payroll deductions from the most recent statement period. Deductions reflect a snapshot of the current period and are replaced when a new statement is generated. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployeeResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employees.get( - employee_id="empl_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get(employee_id, request_options=request_options) - return _response.data - - async def update( - self, - employee_id: EmployeeId, - *, - effective_date: dt.date, - email: typing.Optional[str] = OMIT, - phone: typing.Optional[str] = OMIT, - gender: typing.Optional[Gender] = OMIT, - address: typing.Optional[EmployeeAddressInput] = OMIT, - employee_class: typing.Optional[EmployeeClass] = OMIT, - start_date: typing.Optional[dt.date] = OMIT, - compensation_type: typing.Optional[CompensationType] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployeeResponse: - """ - Updates employee personal, contact, address, and employment fields. This endpoint currently supports email, phone, gender, address, employee_class, start_date, and compensation_type. effective_date is required and applies to employee_class and compensation_type when those fields are included in the request. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - effective_date : dt.date - Past or present date applied to each tracked employment field included in this request - - email : typing.Optional[str] - Email address - - phone : typing.Optional[str] - Phone number - - gender : typing.Optional[Gender] - Gender identity - - * `Male` - Male - * `Female` - Female - * `Transgender` - Transgender - * `Non-binary` - Non-binary - * `Prefer not to respond` - Prefer not to respond - - address : typing.Optional[EmployeeAddressInput] - Employee's residential address - - employee_class : typing.Optional[EmployeeClass] - Employment classification - - * `Full Time` - Full Time - * `Part Time` - Part Time - * `Temporary` - Temporary - * `Intern` - Intern - * `Seasonal` - Seasonal - * `Individual Contractor` - Individual Contractor - - start_date : typing.Optional[dt.date] - Employment start date - - compensation_type : typing.Optional[CompensationType] - Employee compensation type - - * `Salary` - Salary - * `Hourly` - Hourly - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployeeResponse - - - Examples - -------- - import asyncio - import datetime - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employees.update( - employee_id="empl_abc123def456", - employee_class="Full Time", - start_date=datetime.date.fromisoformat( - "2023-01-15", - ), - compensation_type="Salary", - effective_date=datetime.date.fromisoformat( - "2023-03-01", - ), - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.update( - employee_id, - effective_date=effective_date, - email=email, - phone=phone, - gender=gender, - address=address, - employee_class=employee_class, - start_date=start_date, - compensation_type=compensation_type, - request_options=request_options, - ) - return _response.data - - async def list_enrollments( - self, - employee_id: EmployeeId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[Enrollment, EnrollmentListResponse]: - """ - Retrieves a paginated list of benefit enrollments for an employee. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[Enrollment, EnrollmentListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.employees.list_enrollments( - employee_id="empl_abc123def456", - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list_enrollments( - employee_id, limit=limit, page=page, request_options=request_options - ) diff --git a/src/vitable_connect/employees/raw_client.py b/src/vitable_connect/employees/raw_client.py deleted file mode 100644 index e3f6e32..0000000 --- a/src/vitable_connect/employees/raw_client.py +++ /dev/null @@ -1,1021 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.pagination import AsyncPager, SyncPager -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..core.serialization import convert_and_respect_annotation_metadata -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..types.compensation_type import CompensationType -from ..types.employee_address_input import EmployeeAddressInput -from ..types.employee_class import EmployeeClass -from ..types.employee_id import EmployeeId -from ..types.employee_response import EmployeeResponse -from ..types.enrollment import Enrollment -from ..types.enrollment_list_response import EnrollmentListResponse -from ..types.error_response import ErrorResponse -from ..types.gender import Gender -from ..types.limit import Limit -from ..types.page import Page -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawEmployeesClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get( - self, employee_id: EmployeeId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[EmployeeResponse]: - """ - Retrieves detailed information for a specific employee by ID. Returns employee details including personal information, employment status, classification and compensation-type effective dates, compensation type, and payroll deductions from the most recent statement period. Deductions reflect a snapshot of the current period and are replaced when a new statement is generated. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployeeResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employees/{encode_path_param(employee_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployeeResponse, - parse_obj_as( - type_=EmployeeResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def update( - self, - employee_id: EmployeeId, - *, - effective_date: dt.date, - email: typing.Optional[str] = OMIT, - phone: typing.Optional[str] = OMIT, - gender: typing.Optional[Gender] = OMIT, - address: typing.Optional[EmployeeAddressInput] = OMIT, - employee_class: typing.Optional[EmployeeClass] = OMIT, - start_date: typing.Optional[dt.date] = OMIT, - compensation_type: typing.Optional[CompensationType] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[EmployeeResponse]: - """ - Updates employee personal, contact, address, and employment fields. This endpoint currently supports email, phone, gender, address, employee_class, start_date, and compensation_type. effective_date is required and applies to employee_class and compensation_type when those fields are included in the request. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - effective_date : dt.date - Past or present date applied to each tracked employment field included in this request - - email : typing.Optional[str] - Email address - - phone : typing.Optional[str] - Phone number - - gender : typing.Optional[Gender] - Gender identity - - * `Male` - Male - * `Female` - Female - * `Transgender` - Transgender - * `Non-binary` - Non-binary - * `Prefer not to respond` - Prefer not to respond - - address : typing.Optional[EmployeeAddressInput] - Employee's residential address - - employee_class : typing.Optional[EmployeeClass] - Employment classification - - * `Full Time` - Full Time - * `Part Time` - Part Time - * `Temporary` - Temporary - * `Intern` - Intern - * `Seasonal` - Seasonal - * `Individual Contractor` - Individual Contractor - - start_date : typing.Optional[dt.date] - Employment start date - - compensation_type : typing.Optional[CompensationType] - Employee compensation type - - * `Salary` - Salary - * `Hourly` - Hourly - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployeeResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employees/{encode_path_param(employee_id)}", - method="PATCH", - json={ - "email": email, - "phone": phone, - "gender": gender, - "address": convert_and_respect_annotation_metadata( - object_=address, annotation=typing.Optional[EmployeeAddressInput], direction="write" - ), - "employee_class": employee_class, - "start_date": start_date, - "compensation_type": compensation_type, - "effective_date": effective_date, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployeeResponse, - parse_obj_as( - type_=EmployeeResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_enrollments( - self, - employee_id: EmployeeId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[Enrollment, EnrollmentListResponse]: - """ - Retrieves a paginated list of benefit enrollments for an employee. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[Enrollment, EnrollmentListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - f"v1/employees/{encode_path_param(employee_id)}/enrollments", - method="GET", - params={ - "limit": limit, - "page": page, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - EnrollmentListResponse, - parse_obj_as( - type_=EnrollmentListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list_enrollments( - employee_id, - limit=limit, - page=page + 1, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawEmployeesClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get( - self, employee_id: EmployeeId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[EmployeeResponse]: - """ - Retrieves detailed information for a specific employee by ID. Returns employee details including personal information, employment status, classification and compensation-type effective dates, compensation type, and payroll deductions from the most recent statement period. Deductions reflect a snapshot of the current period and are replaced when a new statement is generated. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployeeResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employees/{encode_path_param(employee_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployeeResponse, - parse_obj_as( - type_=EmployeeResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def update( - self, - employee_id: EmployeeId, - *, - effective_date: dt.date, - email: typing.Optional[str] = OMIT, - phone: typing.Optional[str] = OMIT, - gender: typing.Optional[Gender] = OMIT, - address: typing.Optional[EmployeeAddressInput] = OMIT, - employee_class: typing.Optional[EmployeeClass] = OMIT, - start_date: typing.Optional[dt.date] = OMIT, - compensation_type: typing.Optional[CompensationType] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[EmployeeResponse]: - """ - Updates employee personal, contact, address, and employment fields. This endpoint currently supports email, phone, gender, address, employee_class, start_date, and compensation_type. effective_date is required and applies to employee_class and compensation_type when those fields are included in the request. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - effective_date : dt.date - Past or present date applied to each tracked employment field included in this request - - email : typing.Optional[str] - Email address - - phone : typing.Optional[str] - Phone number - - gender : typing.Optional[Gender] - Gender identity - - * `Male` - Male - * `Female` - Female - * `Transgender` - Transgender - * `Non-binary` - Non-binary - * `Prefer not to respond` - Prefer not to respond - - address : typing.Optional[EmployeeAddressInput] - Employee's residential address - - employee_class : typing.Optional[EmployeeClass] - Employment classification - - * `Full Time` - Full Time - * `Part Time` - Part Time - * `Temporary` - Temporary - * `Intern` - Intern - * `Seasonal` - Seasonal - * `Individual Contractor` - Individual Contractor - - start_date : typing.Optional[dt.date] - Employment start date - - compensation_type : typing.Optional[CompensationType] - Employee compensation type - - * `Salary` - Salary - * `Hourly` - Hourly - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployeeResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employees/{encode_path_param(employee_id)}", - method="PATCH", - json={ - "email": email, - "phone": phone, - "gender": gender, - "address": convert_and_respect_annotation_metadata( - object_=address, annotation=typing.Optional[EmployeeAddressInput], direction="write" - ), - "employee_class": employee_class, - "start_date": start_date, - "compensation_type": compensation_type, - "effective_date": effective_date, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployeeResponse, - parse_obj_as( - type_=EmployeeResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_enrollments( - self, - employee_id: EmployeeId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[Enrollment, EnrollmentListResponse]: - """ - Retrieves a paginated list of benefit enrollments for an employee. - - Parameters - ---------- - employee_id : EmployeeId - Unique employee identifier (empl_*) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[Enrollment, EnrollmentListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - f"v1/employees/{encode_path_param(employee_id)}/enrollments", - method="GET", - params={ - "limit": limit, - "page": page, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - EnrollmentListResponse, - parse_obj_as( - type_=EnrollmentListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list_enrollments( - employee_id, - limit=limit, - page=page + 1, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/employers/__init__.py b/src/vitable_connect/employers/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/employers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/employers/client.py b/src/vitable_connect/employers/client.py deleted file mode 100644 index 85cc5a1..0000000 --- a/src/vitable_connect/employers/client.py +++ /dev/null @@ -1,2267 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.pagination import AsyncPager, SyncPager -from ..core.request_options import RequestOptions -from ..types.access_method import AccessMethod -from ..types.additional_access_method import AdditionalAccessMethod -from ..types.benefit_family_param_item import BenefitFamilyParamItem -from ..types.benefit_lifecycle_stage_item import BenefitLifecycleStageItem -from ..types.benefit_plan_year_id import BenefitPlanYearId -from ..types.census_sync_detail_response import CensusSyncDetailResponse -from ..types.census_sync_employee_request import CensusSyncEmployeeRequest -from ..types.classification_correction_source import ClassificationCorrectionSource -from ..types.deduction_frequency import DeductionFrequency -from ..types.election_status_item import ElectionStatusItem -from ..types.employee import Employee -from ..types.employee_list_response import EmployeeListResponse -from ..types.employee_search import EmployeeSearch -from ..types.employee_status import EmployeeStatus -from ..types.employer_address_input import EmployerAddressInput -from ..types.employer_benefit_plan_year_response import EmployerBenefitPlanYearResponse -from ..types.employer_benefit_plan_years_list_response import EmployerBenefitPlanYearsListResponse -from ..types.employer_hris_response import EmployerHrisResponse -from ..types.employer_id import EmployerId -from ..types.employer_invoice_pdf_response import EmployerInvoicePdfResponse -from ..types.employer_invoices_list_response import EmployerInvoicesListResponse -from ..types.employer_payroll_deduction_statement_list_response import EmployerPayrollDeductionStatementListResponse -from ..types.employer_response import EmployerResponse -from ..types.employer_search import EmployerSearch -from ..types.employer_settings_response import EmployerSettingsResponse -from ..types.hris_status_item import HrisStatusItem -from ..types.include_cancelled import IncludeCancelled -from ..types.invoice_id import InvoiceId -from ..types.limit import Limit -from ..types.missing_employee_resolution import MissingEmployeeResolution -from ..types.offset import Offset -from ..types.organization_employer import OrganizationEmployer -from ..types.organization_employer_list_response import OrganizationEmployerListResponse -from ..types.organization_hris_providers_response import OrganizationHrisProvidersResponse -from ..types.page import Page -from ..types.payroll_access_setup_status_response import PayrollAccessSetupStatusResponse -from ..types.payroll_deduction_statement import PayrollDeductionStatement -from ..types.payroll_integration_email_response import PayrollIntegrationEmailResponse -from ..types.plan_year_enrollment import PlanYearEnrollment -from ..types.plan_year_enrollment_list_response import PlanYearEnrollmentListResponse -from ..types.remaining_employee_action import RemainingEmployeeAction -from ..types.search import Search -from ..types.update_employer_address_input import UpdateEmployerAddressInput -from .raw_client import AsyncRawEmployersClient, RawEmployersClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class EmployersClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawEmployersClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawEmployersClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawEmployersClient - """ - return self._raw_client - - def list( - self, - *, - benefit_family: typing.Optional[ - typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]] - ] = None, - benefit_lifecycle_stage: typing.Optional[ - typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]] - ] = None, - hris_provider: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - hris_status: typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]] = None, - include_cancelled: typing.Optional[IncludeCancelled] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[Search] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[OrganizationEmployer, OrganizationEmployerListResponse]: - """ - Returns the caller's employer book — every employer with its computed columns (enrollment-rate summary, benefit-family tags, HRIS connection, benefit-lifecycle stage) merged with the employer's flat CRM fields (legal name, EIN, contact, address, timestamps). The book is derived from the authenticated principal: one organization's employers, or every organization's for a caller whose reach is not a single organization. Supports search by display name, legal name, or exact EIN, employer id or contact email, benefit-family/lifecycle/HRIS filters, and page/limit pagination. - - Parameters - ---------- - benefit_family : typing.Optional[typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]]] - Filter to employers with at least one active benefit in these families. - - benefit_lifecycle_stage : typing.Optional[typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]]] - Filter to employers in one of these computed benefit-lifecycle stages. - - hris_provider : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Filter to employers whose HRIS connection is with one of these payroll providers (e.g. `ADP RUN`). Matched case-insensitively; free text, so read the available values from the HRIS-providers endpoint rather than assuming a fixed set. - - hris_status : typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]] - Filter to employers whose HRIS connection is in one of these statuses. - - include_cancelled : typing.Optional[IncludeCancelled] - Include cancelled employers (hidden by default unless their stage is explicitly requested). - - limit : typing.Optional[Limit] - Items per page. - - page : typing.Optional[Page] - Page number. - - search : typing.Optional[Search] - Employer filter. Matches the display name or the legal name case-insensitively as a substring, or one of these exactly: the EIN (with or without its dash), the employer id, or the contact email of one of the employer's non-disabled admins. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[OrganizationEmployer, OrganizationEmployerListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.employers.list( - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list( - benefit_family=benefit_family, - benefit_lifecycle_stage=benefit_lifecycle_stage, - hris_provider=hris_provider, - hris_status=hris_status, - include_cancelled=include_cancelled, - limit=limit, - page=page, - search=search, - request_options=request_options, - ) - - def create( - self, - *, - name: str, - legal_name: str, - ein: str, - email: str, - address: EmployerAddressInput, - phone_number: typing.Optional[str] = OMIT, - reference_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerResponse: - """ - Creates a new employer for the authenticated organization. Requires employer name, legal name, EIN, email, and address information. Returns the created employer with its assigned ID. - - Parameters - ---------- - name : str - Employer display name - - legal_name : str - Legal business name - - ein : str - Employer Identification Number (format: XX-XXXXXXX) - - email : str - Email address for billing and communications - - address : EmployerAddressInput - Employer address - - phone_number : typing.Optional[str] - Employer phone number (10-digit US format, e.g. 5551234567) - - reference_id : typing.Optional[str] - External reference ID for this employer - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerResponse - - - Examples - -------- - from vitable_connect import EmployerAddressInput, VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.create( - name="NewCo Industries", - legal_name="NewCo Industries LLC", - ein="12-3456789", - email="hr@newco.com", - address=EmployerAddressInput( - address_line1="789 Business Blvd", - address_line2="Floor 5", - city="Seattle", - state="WA", - zipcode="98101", - ), - phone_number="2065550100", - reference_id="partner-emp-001", - ) - """ - _response = self._raw_client.create( - name=name, - legal_name=legal_name, - ein=ein, - email=email, - address=address, - phone_number=phone_number, - reference_id=reference_id, - request_options=request_options, - ) - return _response.data - - def get( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployerResponse: - """ - Retrieves detailed information for a specific employer by ID. The employer must belong to the authenticated organization. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.get( - employer_id="empr_abc123def456", - ) - """ - _response = self._raw_client.get(employer_id, request_options=request_options) - return _response.data - - def update( - self, - employer_id: EmployerId, - *, - name: typing.Optional[str] = OMIT, - legal_name: typing.Optional[str] = OMIT, - address: typing.Optional[UpdateEmployerAddressInput] = OMIT, - active: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerResponse: - """ - Updates an existing employer. All fields are optional — only provided fields are updated. PO Box addresses are rejected. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - name : typing.Optional[str] - Employer display name - - legal_name : typing.Optional[str] - Legal business name - - address : typing.Optional[UpdateEmployerAddressInput] - Employer address - - active : typing.Optional[bool] - Whether the employer is active - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.update( - employer_id="empr_abc123def456", - ) - """ - _response = self._raw_client.update( - employer_id, - name=name, - legal_name=legal_name, - address=address, - active=active, - request_options=request_options, - ) - return _response.data - - def list_benefit_plan_years( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployerBenefitPlanYearsListResponse: - """ - Returns the employer's benefit plan years (all years, or one when `year` is given), each with its benefits, offered states, benefit families, and the year-level enrollment roll-up. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerBenefitPlanYearsListResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.list_benefit_plan_years( - employer_id="empr_abc123def456", - ) - """ - _response = self._raw_client.list_benefit_plan_years(employer_id, request_options=request_options) - return _response.data - - def get_benefit_plan_year( - self, - employer_id: EmployerId, - benefit_plan_year_id: BenefitPlanYearId, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerBenefitPlanYearResponse: - """ - Returns one benefit plan year in full — its benefit details plus the per-benefit enrollment rate and SPD link — addressed by its `benefit_plan_year_id`. The caller must be authorized for the employer; an unknown or unauthorized plan year returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - benefit_plan_year_id : BenefitPlanYearId - Unique benefit-plan-year identifier (plyr_*). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerBenefitPlanYearResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.get_benefit_plan_year( - employer_id="empr_abc123def456", - benefit_plan_year_id="plyr_abc123def456", - ) - """ - _response = self._raw_client.get_benefit_plan_year( - employer_id, benefit_plan_year_id, request_options=request_options - ) - return _response.data - - def list_benefit_plan_year_enrollments( - self, - employer_id: EmployerId, - benefit_plan_year_id: BenefitPlanYearId, - *, - election_status: typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[EmployerSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[PlanYearEnrollment, PlanYearEnrollmentListResponse]: - """ - Returns a paginated list of every member with an enrollment in one of an employer's plan years, any election status: what they elected, where their coverage stands, dependent count, carrier, plan, tier, and the plan's total monthly cost. The caller must be authorized for the employer `empr_<...>`; an unknown or unauthorized employer, or an unknown plan year `plyr_<...>`, returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*). - - benefit_plan_year_id : BenefitPlanYearId - Unique benefit-plan-year identifier (plyr_*). - - election_status : typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]] - Filter by election status. Repeat the parameter to match several. - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[EmployerSearch] - Case-insensitive search. Matches member name partially, and the `member_id` exactly — either your own reference id or the prefixed `grpmbr_<...>` id. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[PlanYearEnrollment, PlanYearEnrollmentListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.employers.list_benefit_plan_year_enrollments( - employer_id="empr_abc123def456", - benefit_plan_year_id="plyr_abc123def456", - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list_benefit_plan_year_enrollments( - employer_id, - benefit_plan_year_id, - election_status=election_status, - limit=limit, - page=page, - search=search, - request_options=request_options, - ) - - def submit_census_sync( - self, - employer_id: EmployerId, - *, - employees: typing.Sequence[CensusSyncEmployeeRequest], - request_options: typing.Optional[RequestOptions] = None, - ) -> CensusSyncDetailResponse: - """ - Submits a census sync payload for the specified employer. The employees in the payload will be queued for processing. Returns an accepted response with the timestamp of acceptance. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - employees : typing.Sequence[CensusSyncEmployeeRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CensusSyncDetailResponse - - - Examples - -------- - import datetime - - from vitable_connect import ( - CensusSyncEmployeeAddressRequest, - CensusSyncEmployeeRequest, - VitableConnect, - ) - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.submit_census_sync( - employer_id="empr_abc123def456", - employees=[ - CensusSyncEmployeeRequest( - reference_id="EMP-001", - first_name="Jane", - last_name="Doe", - date_of_birth=datetime.date.fromisoformat( - "1990-05-15", - ), - email="jane.doe@acme.com", - phone="4155550100", - address=CensusSyncEmployeeAddressRequest( - address_line1="123 Main Street", - address_line2="Apt 4B", - city="San Francisco", - state="CA", - zipcode="94102", - ), - start_date=datetime.date.fromisoformat( - "2024-01-15", - ), - employee_class="Full Time", - compensation_type="Salary", - ), - CensusSyncEmployeeRequest( - first_name="John", - last_name="Smith", - date_of_birth=datetime.date.fromisoformat( - "1985-11-20", - ), - email="john.smith@acme.com", - phone="4155550101", - start_date=datetime.date.fromisoformat( - "2024-03-01", - ), - employee_class="Part Time", - compensation_type="Hourly", - ), - ], - ) - """ - _response = self._raw_client.submit_census_sync( - employer_id, employees=employees, request_options=request_options - ) - return _response.data - - def list_employees( - self, - employer_id: EmployerId, - *, - employment_status: typing.Optional[EmployeeStatus] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[EmployeeSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[Employee, EmployeeListResponse]: - """ - Retrieves a paginated list of employees for a specific employer. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. Results are paginated using page and limit parameters and can be narrowed with a case-insensitive `search` (first name, last name, or email) and an `employment_status` filter (active or terminated). Each employee includes payroll deductions from the most recent statement period. When a new deduction statement is generated, previous period deductions are replaced. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - employment_status : typing.Optional[EmployeeStatus] - Filter by employment status (active or terminated) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[EmployeeSearch] - Case-insensitive search across employee first name, last name, and email - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[Employee, EmployeeListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.employers.list_employees( - employer_id="empr_abc123def456", - limit=20, - page=1, - search="jane", - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list_employees( - employer_id, - employment_status=employment_status, - limit=limit, - page=page, - search=search, - request_options=request_options, - ) - - def get_hris( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployerHrisResponse: - """ - Returns the employer's HRIS connection — provider, status, last sync, and synced row count — or null when the employer has no integration. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerHrisResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.get_hris( - employer_id="empr_abc123def456", - ) - """ - _response = self._raw_client.get_hris(employer_id, request_options=request_options) - return _response.data - - def list_invoices( - self, - employer_id: EmployerId, - *, - limit: typing.Optional[Limit] = None, - offset: typing.Optional[Offset] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerInvoicesListResponse: - """ - Returns a cursor-paginated page of the employer's billing invoices, newest first. Pass the `next_offset` from a previous page as `offset` to fetch the next page. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - limit : typing.Optional[Limit] - Maximum number of invoices per page - - offset : typing.Optional[Offset] - Opaque cursor from a previous page's next_offset - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerInvoicesListResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.list_invoices( - employer_id="empr_abc123def456", - limit=20, - ) - """ - _response = self._raw_client.list_invoices( - employer_id, limit=limit, offset=offset, request_options=request_options - ) - return _response.data - - def get_invoice_pdf( - self, employer_id: EmployerId, invoice_id: InvoiceId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployerInvoicePdfResponse: - """ - Returns the time-limited PDF download link for a single invoice belonging to the employer's billing customer. `invoice_id` is the external Chargebee id (not a prefixed UUID). The caller must be authorized for the employer; an unknown or unauthorized employer or invoice returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - invoice_id : InvoiceId - External Chargebee invoice id (not a prefixed UUID). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerInvoicePdfResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.get_invoice_pdf( - employer_id="empr_abc123def456", - invoice_id="INV-00042", - ) - """ - _response = self._raw_client.get_invoice_pdf(employer_id, invoice_id, request_options=request_options) - return _response.data - - def get_payroll_access_setup( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> PayrollAccessSetupStatusResponse: - """ - Return whether the employer has submitted payroll access setup. - - Parameters - ---------- - employer_id : EmployerId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PayrollAccessSetupStatusResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.get_payroll_access_setup( - employer_id="empr_abc123def456", - ) - """ - _response = self._raw_client.get_payroll_access_setup(employer_id, request_options=request_options) - return _response.data - - def submit_payroll_access_setup( - self, - employer_id: EmployerId, - *, - employees_in_payroll_acknowledged: bool, - payroll_data_impacts_eligibility_acknowledged: bool, - classifications_accurate: bool, - all_benefit_eligible_employees_present: bool, - is_controlled_group: bool, - access_method: AccessMethod, - has_additional_payroll_system: bool, - classification_correction_source: typing.Optional[ClassificationCorrectionSource] = OMIT, - misclassified_employee_names: typing.Optional[typing.Sequence[str]] = OMIT, - remaining_employee_action: typing.Optional[RemainingEmployeeAction] = OMIT, - missing_employee_resolution: typing.Optional[MissingEmployeeResolution] = OMIT, - same_payroll_covers_other_eins: typing.Optional[bool] = OMIT, - login_url: typing.Optional[str] = OMIT, - username: typing.Optional[str] = OMIT, - phone: typing.Optional[str] = OMIT, - password: typing.Optional[str] = OMIT, - integration_confirmed: typing.Optional[bool] = OMIT, - additional_access_method: typing.Optional[AdditionalAccessMethod] = OMIT, - additional_login_url: typing.Optional[str] = OMIT, - additional_username: typing.Optional[str] = OMIT, - additional_phone: typing.Optional[str] = OMIT, - additional_password: typing.Optional[str] = OMIT, - additional_integration_confirmed: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> PayrollAccessSetupStatusResponse: - """ - Submit the employer's payroll access setup answers. - - Parameters - ---------- - employer_id : EmployerId - - employees_in_payroll_acknowledged : bool - Attestation that all benefit-eligible employees appear in the payroll system. - - payroll_data_impacts_eligibility_acknowledged : bool - Attestation that changes to payroll data affect benefit eligibility. - - classifications_accurate : bool - Attestation that employee classifications in payroll are accurate. Set `false` to report corrections in the fields below. - - all_benefit_eligible_employees_present : bool - Attestation that every benefit-eligible employee is present in payroll. - - is_controlled_group : bool - Whether this employer belongs to a controlled group of related entities. - - access_method : AccessMethod - - has_additional_payroll_system : bool - Whether a second payroll system is in use. When `true`, supply the `additional_*` fields below. - - classification_correction_source : typing.Optional[ClassificationCorrectionSource] - Where corrected classifications come from, when `classifications_accurate` is `false`. - - misclassified_employee_names : typing.Optional[typing.Sequence[str]] - Names of employees whose payroll classification needs correcting. - - remaining_employee_action : typing.Optional[RemainingEmployeeAction] - How to handle employees still missing from the payroll system. - - missing_employee_resolution : typing.Optional[MissingEmployeeResolution] - How any missing employees will be added, when some are absent. - - same_payroll_covers_other_eins : typing.Optional[bool] - Whether this payroll system also covers other EINs in the controlled group. - - login_url : typing.Optional[str] - Sign-in URL for the payroll system. - - username : typing.Optional[str] - Username Vitable should use to access the payroll system. - - phone : typing.Optional[str] - Phone number used for payroll-system verification codes. - - password : typing.Optional[str] - Password Vitable should use to access the payroll system. - - integration_confirmed : typing.Optional[bool] - Whether the payroll integration has been confirmed as working. - - additional_access_method : typing.Optional[AdditionalAccessMethod] - How Vitable will access the second payroll system. - - additional_login_url : typing.Optional[str] - Sign-in URL for the second payroll system. - - additional_username : typing.Optional[str] - Username Vitable should use for the second payroll system. - - additional_phone : typing.Optional[str] - Phone number used for second payroll-system verification codes. - - additional_password : typing.Optional[str] - Password Vitable should use for the second payroll system. - - additional_integration_confirmed : typing.Optional[bool] - Whether the second payroll integration has been confirmed as working. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PayrollAccessSetupStatusResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.submit_payroll_access_setup( - employer_id="empr_abc123def456", - employees_in_payroll_acknowledged=True, - payroll_data_impacts_eligibility_acknowledged=True, - classifications_accurate=True, - all_benefit_eligible_employees_present=True, - is_controlled_group=True, - access_method="SELF_SETUP", - has_additional_payroll_system=True, - ) - """ - _response = self._raw_client.submit_payroll_access_setup( - employer_id, - employees_in_payroll_acknowledged=employees_in_payroll_acknowledged, - payroll_data_impacts_eligibility_acknowledged=payroll_data_impacts_eligibility_acknowledged, - classifications_accurate=classifications_accurate, - all_benefit_eligible_employees_present=all_benefit_eligible_employees_present, - is_controlled_group=is_controlled_group, - access_method=access_method, - has_additional_payroll_system=has_additional_payroll_system, - classification_correction_source=classification_correction_source, - misclassified_employee_names=misclassified_employee_names, - remaining_employee_action=remaining_employee_action, - missing_employee_resolution=missing_employee_resolution, - same_payroll_covers_other_eins=same_payroll_covers_other_eins, - login_url=login_url, - username=username, - phone=phone, - password=password, - integration_confirmed=integration_confirmed, - additional_access_method=additional_access_method, - additional_login_url=additional_login_url, - additional_username=additional_username, - additional_phone=additional_phone, - additional_password=additional_password, - additional_integration_confirmed=additional_integration_confirmed, - request_options=request_options, - ) - return _response.data - - def list_payroll_deduction_statements( - self, - employer_id: EmployerId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[PayrollDeductionStatement, EmployerPayrollDeductionStatementListResponse]: - """ - Returns a paginated list of the employer's payroll-deduction statements, newest period first, each with its period, generation date, distinct employee count, total deduction, change-file link, and deduction frequency. Statements superseded by a later correction are excluded. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - limit : typing.Optional[Limit] - Maximum number of statements per page - - page : typing.Optional[Page] - Page number to retrieve (starts at 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[PayrollDeductionStatement, EmployerPayrollDeductionStatementListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.employers.list_payroll_deduction_statements( - employer_id="empr_abc123def456", - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list_payroll_deduction_statements( - employer_id, limit=limit, page=page, request_options=request_options - ) - - def ensure_payroll_integration_email( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> PayrollIntegrationEmailResponse: - """ - Provision and return the employer's payroll integration email. - - Parameters - ---------- - employer_id : EmployerId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PayrollIntegrationEmailResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.ensure_payroll_integration_email( - employer_id="empr_abc123def456", - ) - """ - _response = self._raw_client.ensure_payroll_integration_email(employer_id, request_options=request_options) - return _response.data - - def update_settings( - self, - employer_id: EmployerId, - *, - pay_frequency: DeductionFrequency, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerSettingsResponse: - """ - Updates configuration settings for a specific employer. The employer must belong to the authenticated organization. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - pay_frequency : DeductionFrequency - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerSettingsResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.update_settings( - employer_id="empr_abc123def456", - pay_frequency="bi_weekly", - ) - """ - _response = self._raw_client.update_settings( - employer_id, pay_frequency=pay_frequency, request_options=request_options - ) - return _response.data - - def list_hris_providers( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> OrganizationHrisProvidersResponse: - """ - Returns the distinct HRIS/payroll providers across the same book `GET /v1/employers` returns, sorted for display. Use these as the values for the employers list's `hris_provider` filter — filter on `provider`, show `provider_label`. The stored providers are free text, so they cannot be enumerated in advance. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - OrganizationHrisProvidersResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.employers.list_hris_providers() - """ - _response = self._raw_client.list_hris_providers(request_options=request_options) - return _response.data - - -class AsyncEmployersClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawEmployersClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawEmployersClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawEmployersClient - """ - return self._raw_client - - async def list( - self, - *, - benefit_family: typing.Optional[ - typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]] - ] = None, - benefit_lifecycle_stage: typing.Optional[ - typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]] - ] = None, - hris_provider: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - hris_status: typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]] = None, - include_cancelled: typing.Optional[IncludeCancelled] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[Search] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[OrganizationEmployer, OrganizationEmployerListResponse]: - """ - Returns the caller's employer book — every employer with its computed columns (enrollment-rate summary, benefit-family tags, HRIS connection, benefit-lifecycle stage) merged with the employer's flat CRM fields (legal name, EIN, contact, address, timestamps). The book is derived from the authenticated principal: one organization's employers, or every organization's for a caller whose reach is not a single organization. Supports search by display name, legal name, or exact EIN, employer id or contact email, benefit-family/lifecycle/HRIS filters, and page/limit pagination. - - Parameters - ---------- - benefit_family : typing.Optional[typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]]] - Filter to employers with at least one active benefit in these families. - - benefit_lifecycle_stage : typing.Optional[typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]]] - Filter to employers in one of these computed benefit-lifecycle stages. - - hris_provider : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Filter to employers whose HRIS connection is with one of these payroll providers (e.g. `ADP RUN`). Matched case-insensitively; free text, so read the available values from the HRIS-providers endpoint rather than assuming a fixed set. - - hris_status : typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]] - Filter to employers whose HRIS connection is in one of these statuses. - - include_cancelled : typing.Optional[IncludeCancelled] - Include cancelled employers (hidden by default unless their stage is explicitly requested). - - limit : typing.Optional[Limit] - Items per page. - - page : typing.Optional[Page] - Page number. - - search : typing.Optional[Search] - Employer filter. Matches the display name or the legal name case-insensitively as a substring, or one of these exactly: the EIN (with or without its dash), the employer id, or the contact email of one of the employer's non-disabled admins. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[OrganizationEmployer, OrganizationEmployerListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.employers.list( - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list( - benefit_family=benefit_family, - benefit_lifecycle_stage=benefit_lifecycle_stage, - hris_provider=hris_provider, - hris_status=hris_status, - include_cancelled=include_cancelled, - limit=limit, - page=page, - search=search, - request_options=request_options, - ) - - async def create( - self, - *, - name: str, - legal_name: str, - ein: str, - email: str, - address: EmployerAddressInput, - phone_number: typing.Optional[str] = OMIT, - reference_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerResponse: - """ - Creates a new employer for the authenticated organization. Requires employer name, legal name, EIN, email, and address information. Returns the created employer with its assigned ID. - - Parameters - ---------- - name : str - Employer display name - - legal_name : str - Legal business name - - ein : str - Employer Identification Number (format: XX-XXXXXXX) - - email : str - Email address for billing and communications - - address : EmployerAddressInput - Employer address - - phone_number : typing.Optional[str] - Employer phone number (10-digit US format, e.g. 5551234567) - - reference_id : typing.Optional[str] - External reference ID for this employer - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect, EmployerAddressInput - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.create( - name="NewCo Industries", - legal_name="NewCo Industries LLC", - ein="12-3456789", - email="hr@newco.com", - address=EmployerAddressInput( - address_line1="789 Business Blvd", - address_line2="Floor 5", - city="Seattle", - state="WA", - zipcode="98101", - ), - phone_number="2065550100", - reference_id="partner-emp-001", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.create( - name=name, - legal_name=legal_name, - ein=ein, - email=email, - address=address, - phone_number=phone_number, - reference_id=reference_id, - request_options=request_options, - ) - return _response.data - - async def get( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployerResponse: - """ - Retrieves detailed information for a specific employer by ID. The employer must belong to the authenticated organization. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.get( - employer_id="empr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get(employer_id, request_options=request_options) - return _response.data - - async def update( - self, - employer_id: EmployerId, - *, - name: typing.Optional[str] = OMIT, - legal_name: typing.Optional[str] = OMIT, - address: typing.Optional[UpdateEmployerAddressInput] = OMIT, - active: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerResponse: - """ - Updates an existing employer. All fields are optional — only provided fields are updated. PO Box addresses are rejected. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - name : typing.Optional[str] - Employer display name - - legal_name : typing.Optional[str] - Legal business name - - address : typing.Optional[UpdateEmployerAddressInput] - Employer address - - active : typing.Optional[bool] - Whether the employer is active - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.update( - employer_id="empr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.update( - employer_id, - name=name, - legal_name=legal_name, - address=address, - active=active, - request_options=request_options, - ) - return _response.data - - async def list_benefit_plan_years( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployerBenefitPlanYearsListResponse: - """ - Returns the employer's benefit plan years (all years, or one when `year` is given), each with its benefits, offered states, benefit families, and the year-level enrollment roll-up. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerBenefitPlanYearsListResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.list_benefit_plan_years( - employer_id="empr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_benefit_plan_years(employer_id, request_options=request_options) - return _response.data - - async def get_benefit_plan_year( - self, - employer_id: EmployerId, - benefit_plan_year_id: BenefitPlanYearId, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerBenefitPlanYearResponse: - """ - Returns one benefit plan year in full — its benefit details plus the per-benefit enrollment rate and SPD link — addressed by its `benefit_plan_year_id`. The caller must be authorized for the employer; an unknown or unauthorized plan year returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - benefit_plan_year_id : BenefitPlanYearId - Unique benefit-plan-year identifier (plyr_*). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerBenefitPlanYearResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.get_benefit_plan_year( - employer_id="empr_abc123def456", - benefit_plan_year_id="plyr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_benefit_plan_year( - employer_id, benefit_plan_year_id, request_options=request_options - ) - return _response.data - - async def list_benefit_plan_year_enrollments( - self, - employer_id: EmployerId, - benefit_plan_year_id: BenefitPlanYearId, - *, - election_status: typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[EmployerSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[PlanYearEnrollment, PlanYearEnrollmentListResponse]: - """ - Returns a paginated list of every member with an enrollment in one of an employer's plan years, any election status: what they elected, where their coverage stands, dependent count, carrier, plan, tier, and the plan's total monthly cost. The caller must be authorized for the employer `empr_<...>`; an unknown or unauthorized employer, or an unknown plan year `plyr_<...>`, returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*). - - benefit_plan_year_id : BenefitPlanYearId - Unique benefit-plan-year identifier (plyr_*). - - election_status : typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]] - Filter by election status. Repeat the parameter to match several. - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[EmployerSearch] - Case-insensitive search. Matches member name partially, and the `member_id` exactly — either your own reference id or the prefixed `grpmbr_<...>` id. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[PlanYearEnrollment, PlanYearEnrollmentListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.employers.list_benefit_plan_year_enrollments( - employer_id="empr_abc123def456", - benefit_plan_year_id="plyr_abc123def456", - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list_benefit_plan_year_enrollments( - employer_id, - benefit_plan_year_id, - election_status=election_status, - limit=limit, - page=page, - search=search, - request_options=request_options, - ) - - async def submit_census_sync( - self, - employer_id: EmployerId, - *, - employees: typing.Sequence[CensusSyncEmployeeRequest], - request_options: typing.Optional[RequestOptions] = None, - ) -> CensusSyncDetailResponse: - """ - Submits a census sync payload for the specified employer. The employees in the payload will be queued for processing. Returns an accepted response with the timestamp of acceptance. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - employees : typing.Sequence[CensusSyncEmployeeRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - CensusSyncDetailResponse - - - Examples - -------- - import asyncio - import datetime - - from vitable_connect import ( - AsyncVitableConnect, - CensusSyncEmployeeAddressRequest, - CensusSyncEmployeeRequest, - ) - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.submit_census_sync( - employer_id="empr_abc123def456", - employees=[ - CensusSyncEmployeeRequest( - reference_id="EMP-001", - first_name="Jane", - last_name="Doe", - date_of_birth=datetime.date.fromisoformat( - "1990-05-15", - ), - email="jane.doe@acme.com", - phone="4155550100", - address=CensusSyncEmployeeAddressRequest( - address_line1="123 Main Street", - address_line2="Apt 4B", - city="San Francisco", - state="CA", - zipcode="94102", - ), - start_date=datetime.date.fromisoformat( - "2024-01-15", - ), - employee_class="Full Time", - compensation_type="Salary", - ), - CensusSyncEmployeeRequest( - first_name="John", - last_name="Smith", - date_of_birth=datetime.date.fromisoformat( - "1985-11-20", - ), - email="john.smith@acme.com", - phone="4155550101", - start_date=datetime.date.fromisoformat( - "2024-03-01", - ), - employee_class="Part Time", - compensation_type="Hourly", - ), - ], - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.submit_census_sync( - employer_id, employees=employees, request_options=request_options - ) - return _response.data - - async def list_employees( - self, - employer_id: EmployerId, - *, - employment_status: typing.Optional[EmployeeStatus] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[EmployeeSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[Employee, EmployeeListResponse]: - """ - Retrieves a paginated list of employees for a specific employer. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. Results are paginated using page and limit parameters and can be narrowed with a case-insensitive `search` (first name, last name, or email) and an `employment_status` filter (active or terminated). Each employee includes payroll deductions from the most recent statement period. When a new deduction statement is generated, previous period deductions are replaced. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - employment_status : typing.Optional[EmployeeStatus] - Filter by employment status (active or terminated) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[EmployeeSearch] - Case-insensitive search across employee first name, last name, and email - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[Employee, EmployeeListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.employers.list_employees( - employer_id="empr_abc123def456", - limit=20, - page=1, - search="jane", - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list_employees( - employer_id, - employment_status=employment_status, - limit=limit, - page=page, - search=search, - request_options=request_options, - ) - - async def get_hris( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployerHrisResponse: - """ - Returns the employer's HRIS connection — provider, status, last sync, and synced row count — or null when the employer has no integration. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerHrisResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.get_hris( - employer_id="empr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_hris(employer_id, request_options=request_options) - return _response.data - - async def list_invoices( - self, - employer_id: EmployerId, - *, - limit: typing.Optional[Limit] = None, - offset: typing.Optional[Offset] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerInvoicesListResponse: - """ - Returns a cursor-paginated page of the employer's billing invoices, newest first. Pass the `next_offset` from a previous page as `offset` to fetch the next page. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - limit : typing.Optional[Limit] - Maximum number of invoices per page - - offset : typing.Optional[Offset] - Opaque cursor from a previous page's next_offset - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerInvoicesListResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.list_invoices( - employer_id="empr_abc123def456", - limit=20, - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_invoices( - employer_id, limit=limit, offset=offset, request_options=request_options - ) - return _response.data - - async def get_invoice_pdf( - self, employer_id: EmployerId, invoice_id: InvoiceId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EmployerInvoicePdfResponse: - """ - Returns the time-limited PDF download link for a single invoice belonging to the employer's billing customer. `invoice_id` is the external Chargebee id (not a prefixed UUID). The caller must be authorized for the employer; an unknown or unauthorized employer or invoice returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - invoice_id : InvoiceId - External Chargebee invoice id (not a prefixed UUID). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerInvoicePdfResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.get_invoice_pdf( - employer_id="empr_abc123def456", - invoice_id="INV-00042", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_invoice_pdf(employer_id, invoice_id, request_options=request_options) - return _response.data - - async def get_payroll_access_setup( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> PayrollAccessSetupStatusResponse: - """ - Return whether the employer has submitted payroll access setup. - - Parameters - ---------- - employer_id : EmployerId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PayrollAccessSetupStatusResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.get_payroll_access_setup( - employer_id="empr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_payroll_access_setup(employer_id, request_options=request_options) - return _response.data - - async def submit_payroll_access_setup( - self, - employer_id: EmployerId, - *, - employees_in_payroll_acknowledged: bool, - payroll_data_impacts_eligibility_acknowledged: bool, - classifications_accurate: bool, - all_benefit_eligible_employees_present: bool, - is_controlled_group: bool, - access_method: AccessMethod, - has_additional_payroll_system: bool, - classification_correction_source: typing.Optional[ClassificationCorrectionSource] = OMIT, - misclassified_employee_names: typing.Optional[typing.Sequence[str]] = OMIT, - remaining_employee_action: typing.Optional[RemainingEmployeeAction] = OMIT, - missing_employee_resolution: typing.Optional[MissingEmployeeResolution] = OMIT, - same_payroll_covers_other_eins: typing.Optional[bool] = OMIT, - login_url: typing.Optional[str] = OMIT, - username: typing.Optional[str] = OMIT, - phone: typing.Optional[str] = OMIT, - password: typing.Optional[str] = OMIT, - integration_confirmed: typing.Optional[bool] = OMIT, - additional_access_method: typing.Optional[AdditionalAccessMethod] = OMIT, - additional_login_url: typing.Optional[str] = OMIT, - additional_username: typing.Optional[str] = OMIT, - additional_phone: typing.Optional[str] = OMIT, - additional_password: typing.Optional[str] = OMIT, - additional_integration_confirmed: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> PayrollAccessSetupStatusResponse: - """ - Submit the employer's payroll access setup answers. - - Parameters - ---------- - employer_id : EmployerId - - employees_in_payroll_acknowledged : bool - Attestation that all benefit-eligible employees appear in the payroll system. - - payroll_data_impacts_eligibility_acknowledged : bool - Attestation that changes to payroll data affect benefit eligibility. - - classifications_accurate : bool - Attestation that employee classifications in payroll are accurate. Set `false` to report corrections in the fields below. - - all_benefit_eligible_employees_present : bool - Attestation that every benefit-eligible employee is present in payroll. - - is_controlled_group : bool - Whether this employer belongs to a controlled group of related entities. - - access_method : AccessMethod - - has_additional_payroll_system : bool - Whether a second payroll system is in use. When `true`, supply the `additional_*` fields below. - - classification_correction_source : typing.Optional[ClassificationCorrectionSource] - Where corrected classifications come from, when `classifications_accurate` is `false`. - - misclassified_employee_names : typing.Optional[typing.Sequence[str]] - Names of employees whose payroll classification needs correcting. - - remaining_employee_action : typing.Optional[RemainingEmployeeAction] - How to handle employees still missing from the payroll system. - - missing_employee_resolution : typing.Optional[MissingEmployeeResolution] - How any missing employees will be added, when some are absent. - - same_payroll_covers_other_eins : typing.Optional[bool] - Whether this payroll system also covers other EINs in the controlled group. - - login_url : typing.Optional[str] - Sign-in URL for the payroll system. - - username : typing.Optional[str] - Username Vitable should use to access the payroll system. - - phone : typing.Optional[str] - Phone number used for payroll-system verification codes. - - password : typing.Optional[str] - Password Vitable should use to access the payroll system. - - integration_confirmed : typing.Optional[bool] - Whether the payroll integration has been confirmed as working. - - additional_access_method : typing.Optional[AdditionalAccessMethod] - How Vitable will access the second payroll system. - - additional_login_url : typing.Optional[str] - Sign-in URL for the second payroll system. - - additional_username : typing.Optional[str] - Username Vitable should use for the second payroll system. - - additional_phone : typing.Optional[str] - Phone number used for second payroll-system verification codes. - - additional_password : typing.Optional[str] - Password Vitable should use for the second payroll system. - - additional_integration_confirmed : typing.Optional[bool] - Whether the second payroll integration has been confirmed as working. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PayrollAccessSetupStatusResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.submit_payroll_access_setup( - employer_id="empr_abc123def456", - employees_in_payroll_acknowledged=True, - payroll_data_impacts_eligibility_acknowledged=True, - classifications_accurate=True, - all_benefit_eligible_employees_present=True, - is_controlled_group=True, - access_method="SELF_SETUP", - has_additional_payroll_system=True, - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.submit_payroll_access_setup( - employer_id, - employees_in_payroll_acknowledged=employees_in_payroll_acknowledged, - payroll_data_impacts_eligibility_acknowledged=payroll_data_impacts_eligibility_acknowledged, - classifications_accurate=classifications_accurate, - all_benefit_eligible_employees_present=all_benefit_eligible_employees_present, - is_controlled_group=is_controlled_group, - access_method=access_method, - has_additional_payroll_system=has_additional_payroll_system, - classification_correction_source=classification_correction_source, - misclassified_employee_names=misclassified_employee_names, - remaining_employee_action=remaining_employee_action, - missing_employee_resolution=missing_employee_resolution, - same_payroll_covers_other_eins=same_payroll_covers_other_eins, - login_url=login_url, - username=username, - phone=phone, - password=password, - integration_confirmed=integration_confirmed, - additional_access_method=additional_access_method, - additional_login_url=additional_login_url, - additional_username=additional_username, - additional_phone=additional_phone, - additional_password=additional_password, - additional_integration_confirmed=additional_integration_confirmed, - request_options=request_options, - ) - return _response.data - - async def list_payroll_deduction_statements( - self, - employer_id: EmployerId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[PayrollDeductionStatement, EmployerPayrollDeductionStatementListResponse]: - """ - Returns a paginated list of the employer's payroll-deduction statements, newest period first, each with its period, generation date, distinct employee count, total deduction, change-file link, and deduction frequency. Statements superseded by a later correction are excluded. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - limit : typing.Optional[Limit] - Maximum number of statements per page - - page : typing.Optional[Page] - Page number to retrieve (starts at 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[PayrollDeductionStatement, EmployerPayrollDeductionStatementListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.employers.list_payroll_deduction_statements( - employer_id="empr_abc123def456", - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list_payroll_deduction_statements( - employer_id, limit=limit, page=page, request_options=request_options - ) - - async def ensure_payroll_integration_email( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> PayrollIntegrationEmailResponse: - """ - Provision and return the employer's payroll integration email. - - Parameters - ---------- - employer_id : EmployerId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - PayrollIntegrationEmailResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.ensure_payroll_integration_email( - employer_id="empr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.ensure_payroll_integration_email( - employer_id, request_options=request_options - ) - return _response.data - - async def update_settings( - self, - employer_id: EmployerId, - *, - pay_frequency: DeductionFrequency, - request_options: typing.Optional[RequestOptions] = None, - ) -> EmployerSettingsResponse: - """ - Updates configuration settings for a specific employer. The employer must belong to the authenticated organization. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - pay_frequency : DeductionFrequency - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EmployerSettingsResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.update_settings( - employer_id="empr_abc123def456", - pay_frequency="bi_weekly", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.update_settings( - employer_id, pay_frequency=pay_frequency, request_options=request_options - ) - return _response.data - - async def list_hris_providers( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> OrganizationHrisProvidersResponse: - """ - Returns the distinct HRIS/payroll providers across the same book `GET /v1/employers` returns, sorted for display. Use these as the values for the employers list's `hris_provider` filter — filter on `provider`, show `provider_label`. The stored providers are free text, so they cannot be enumerated in advance. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - OrganizationHrisProvidersResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.employers.list_hris_providers() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_hris_providers(request_options=request_options) - return _response.data diff --git a/src/vitable_connect/employers/raw_client.py b/src/vitable_connect/employers/raw_client.py deleted file mode 100644 index 9474fa4..0000000 --- a/src/vitable_connect/employers/raw_client.py +++ /dev/null @@ -1,5733 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.pagination import AsyncPager, SyncPager -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..core.serialization import convert_and_respect_annotation_metadata -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.access_method import AccessMethod -from ..types.additional_access_method import AdditionalAccessMethod -from ..types.benefit_family_param_item import BenefitFamilyParamItem -from ..types.benefit_lifecycle_stage_item import BenefitLifecycleStageItem -from ..types.benefit_plan_year_id import BenefitPlanYearId -from ..types.census_sync_detail_response import CensusSyncDetailResponse -from ..types.census_sync_employee_request import CensusSyncEmployeeRequest -from ..types.classification_correction_source import ClassificationCorrectionSource -from ..types.deduction_frequency import DeductionFrequency -from ..types.election_status_item import ElectionStatusItem -from ..types.employee import Employee -from ..types.employee_list_response import EmployeeListResponse -from ..types.employee_search import EmployeeSearch -from ..types.employee_status import EmployeeStatus -from ..types.employer_address_input import EmployerAddressInput -from ..types.employer_benefit_plan_year_response import EmployerBenefitPlanYearResponse -from ..types.employer_benefit_plan_years_list_response import EmployerBenefitPlanYearsListResponse -from ..types.employer_hris_response import EmployerHrisResponse -from ..types.employer_id import EmployerId -from ..types.employer_invoice_pdf_response import EmployerInvoicePdfResponse -from ..types.employer_invoices_list_response import EmployerInvoicesListResponse -from ..types.employer_payroll_deduction_statement_list_response import EmployerPayrollDeductionStatementListResponse -from ..types.employer_response import EmployerResponse -from ..types.employer_search import EmployerSearch -from ..types.employer_settings_response import EmployerSettingsResponse -from ..types.error_response import ErrorResponse -from ..types.hris_status_item import HrisStatusItem -from ..types.include_cancelled import IncludeCancelled -from ..types.invoice_id import InvoiceId -from ..types.limit import Limit -from ..types.missing_employee_resolution import MissingEmployeeResolution -from ..types.offset import Offset -from ..types.organization_employer import OrganizationEmployer -from ..types.organization_employer_list_response import OrganizationEmployerListResponse -from ..types.organization_hris_providers_response import OrganizationHrisProvidersResponse -from ..types.page import Page -from ..types.payroll_access_setup_status_response import PayrollAccessSetupStatusResponse -from ..types.payroll_deduction_statement import PayrollDeductionStatement -from ..types.payroll_integration_email_response import PayrollIntegrationEmailResponse -from ..types.plan_year_enrollment import PlanYearEnrollment -from ..types.plan_year_enrollment_list_response import PlanYearEnrollmentListResponse -from ..types.remaining_employee_action import RemainingEmployeeAction -from ..types.search import Search -from ..types.update_employer_address_input import UpdateEmployerAddressInput -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawEmployersClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list( - self, - *, - benefit_family: typing.Optional[ - typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]] - ] = None, - benefit_lifecycle_stage: typing.Optional[ - typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]] - ] = None, - hris_provider: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - hris_status: typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]] = None, - include_cancelled: typing.Optional[IncludeCancelled] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[Search] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[OrganizationEmployer, OrganizationEmployerListResponse]: - """ - Returns the caller's employer book — every employer with its computed columns (enrollment-rate summary, benefit-family tags, HRIS connection, benefit-lifecycle stage) merged with the employer's flat CRM fields (legal name, EIN, contact, address, timestamps). The book is derived from the authenticated principal: one organization's employers, or every organization's for a caller whose reach is not a single organization. Supports search by display name, legal name, or exact EIN, employer id or contact email, benefit-family/lifecycle/HRIS filters, and page/limit pagination. - - Parameters - ---------- - benefit_family : typing.Optional[typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]]] - Filter to employers with at least one active benefit in these families. - - benefit_lifecycle_stage : typing.Optional[typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]]] - Filter to employers in one of these computed benefit-lifecycle stages. - - hris_provider : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Filter to employers whose HRIS connection is with one of these payroll providers (e.g. `ADP RUN`). Matched case-insensitively; free text, so read the available values from the HRIS-providers endpoint rather than assuming a fixed set. - - hris_status : typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]] - Filter to employers whose HRIS connection is in one of these statuses. - - include_cancelled : typing.Optional[IncludeCancelled] - Include cancelled employers (hidden by default unless their stage is explicitly requested). - - limit : typing.Optional[Limit] - Items per page. - - page : typing.Optional[Page] - Page number. - - search : typing.Optional[Search] - Employer filter. Matches the display name or the legal name case-insensitively as a substring, or one of these exactly: the EIN (with or without its dash), the employer id, or the contact email of one of the employer's non-disabled admins. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[OrganizationEmployer, OrganizationEmployerListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - "v1/employers", - method="GET", - params={ - "benefit_family": benefit_family, - "benefit_lifecycle_stage": benefit_lifecycle_stage, - "hris_provider": hris_provider, - "hris_status": hris_status, - "include_cancelled": include_cancelled, - "limit": limit, - "page": page, - "search": search, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - OrganizationEmployerListResponse, - parse_obj_as( - type_=OrganizationEmployerListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list( - benefit_family=benefit_family, - benefit_lifecycle_stage=benefit_lifecycle_stage, - hris_provider=hris_provider, - hris_status=hris_status, - include_cancelled=include_cancelled, - limit=limit, - page=page + 1, - search=search, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def create( - self, - *, - name: str, - legal_name: str, - ein: str, - email: str, - address: EmployerAddressInput, - phone_number: typing.Optional[str] = OMIT, - reference_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[EmployerResponse]: - """ - Creates a new employer for the authenticated organization. Requires employer name, legal name, EIN, email, and address information. Returns the created employer with its assigned ID. - - Parameters - ---------- - name : str - Employer display name - - legal_name : str - Legal business name - - ein : str - Employer Identification Number (format: XX-XXXXXXX) - - email : str - Email address for billing and communications - - address : EmployerAddressInput - Employer address - - phone_number : typing.Optional[str] - Employer phone number (10-digit US format, e.g. 5551234567) - - reference_id : typing.Optional[str] - External reference ID for this employer - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - "v1/employers", - method="POST", - json={ - "name": name, - "legal_name": legal_name, - "ein": ein, - "email": email, - "address": convert_and_respect_annotation_metadata( - object_=address, annotation=EmployerAddressInput, direction="write" - ), - "phone_number": phone_number, - "reference_id": reference_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerResponse, - parse_obj_as( - type_=EmployerResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[EmployerResponse]: - """ - Retrieves detailed information for a specific employer by ID. The employer must belong to the authenticated organization. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerResponse, - parse_obj_as( - type_=EmployerResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def update( - self, - employer_id: EmployerId, - *, - name: typing.Optional[str] = OMIT, - legal_name: typing.Optional[str] = OMIT, - address: typing.Optional[UpdateEmployerAddressInput] = OMIT, - active: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[EmployerResponse]: - """ - Updates an existing employer. All fields are optional — only provided fields are updated. PO Box addresses are rejected. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - name : typing.Optional[str] - Employer display name - - legal_name : typing.Optional[str] - Legal business name - - address : typing.Optional[UpdateEmployerAddressInput] - Employer address - - active : typing.Optional[bool] - Whether the employer is active - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}", - method="PUT", - json={ - "name": name, - "legal_name": legal_name, - "address": convert_and_respect_annotation_metadata( - object_=address, annotation=typing.Optional[UpdateEmployerAddressInput], direction="write" - ), - "active": active, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerResponse, - parse_obj_as( - type_=EmployerResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_benefit_plan_years( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[EmployerBenefitPlanYearsListResponse]: - """ - Returns the employer's benefit plan years (all years, or one when `year` is given), each with its benefits, offered states, benefit families, and the year-level enrollment roll-up. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerBenefitPlanYearsListResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/benefit-plan-years", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerBenefitPlanYearsListResponse, - parse_obj_as( - type_=EmployerBenefitPlanYearsListResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get_benefit_plan_year( - self, - employer_id: EmployerId, - benefit_plan_year_id: BenefitPlanYearId, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[EmployerBenefitPlanYearResponse]: - """ - Returns one benefit plan year in full — its benefit details plus the per-benefit enrollment rate and SPD link — addressed by its `benefit_plan_year_id`. The caller must be authorized for the employer; an unknown or unauthorized plan year returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - benefit_plan_year_id : BenefitPlanYearId - Unique benefit-plan-year identifier (plyr_*). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerBenefitPlanYearResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/benefit-plan-years/{encode_path_param(benefit_plan_year_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerBenefitPlanYearResponse, - parse_obj_as( - type_=EmployerBenefitPlanYearResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_benefit_plan_year_enrollments( - self, - employer_id: EmployerId, - benefit_plan_year_id: BenefitPlanYearId, - *, - election_status: typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[EmployerSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[PlanYearEnrollment, PlanYearEnrollmentListResponse]: - """ - Returns a paginated list of every member with an enrollment in one of an employer's plan years, any election status: what they elected, where their coverage stands, dependent count, carrier, plan, tier, and the plan's total monthly cost. The caller must be authorized for the employer `empr_<...>`; an unknown or unauthorized employer, or an unknown plan year `plyr_<...>`, returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*). - - benefit_plan_year_id : BenefitPlanYearId - Unique benefit-plan-year identifier (plyr_*). - - election_status : typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]] - Filter by election status. Repeat the parameter to match several. - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[EmployerSearch] - Case-insensitive search. Matches member name partially, and the `member_id` exactly — either your own reference id or the prefixed `grpmbr_<...>` id. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[PlanYearEnrollment, PlanYearEnrollmentListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/benefit-plan-years/{encode_path_param(benefit_plan_year_id)}/enrollments", - method="GET", - params={ - "election_status": election_status, - "limit": limit, - "page": page, - "search": search, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - PlanYearEnrollmentListResponse, - parse_obj_as( - type_=PlanYearEnrollmentListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list_benefit_plan_year_enrollments( - employer_id, - benefit_plan_year_id, - election_status=election_status, - limit=limit, - page=page + 1, - search=search, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def submit_census_sync( - self, - employer_id: EmployerId, - *, - employees: typing.Sequence[CensusSyncEmployeeRequest], - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[CensusSyncDetailResponse]: - """ - Submits a census sync payload for the specified employer. The employees in the payload will be queued for processing. Returns an accepted response with the timestamp of acceptance. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - employees : typing.Sequence[CensusSyncEmployeeRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[CensusSyncDetailResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/census-sync", - method="POST", - json={ - "employees": convert_and_respect_annotation_metadata( - object_=employees, annotation=typing.Sequence[CensusSyncEmployeeRequest], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - CensusSyncDetailResponse, - parse_obj_as( - type_=CensusSyncDetailResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_employees( - self, - employer_id: EmployerId, - *, - employment_status: typing.Optional[EmployeeStatus] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[EmployeeSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[Employee, EmployeeListResponse]: - """ - Retrieves a paginated list of employees for a specific employer. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. Results are paginated using page and limit parameters and can be narrowed with a case-insensitive `search` (first name, last name, or email) and an `employment_status` filter (active or terminated). Each employee includes payroll deductions from the most recent statement period. When a new deduction statement is generated, previous period deductions are replaced. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - employment_status : typing.Optional[EmployeeStatus] - Filter by employment status (active or terminated) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[EmployeeSearch] - Case-insensitive search across employee first name, last name, and email - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[Employee, EmployeeListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/employees", - method="GET", - params={ - "employment_status": employment_status, - "limit": limit, - "page": page, - "search": search, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - EmployeeListResponse, - parse_obj_as( - type_=EmployeeListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list_employees( - employer_id, - employment_status=employment_status, - limit=limit, - page=page + 1, - search=search, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get_hris( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[EmployerHrisResponse]: - """ - Returns the employer's HRIS connection — provider, status, last sync, and synced row count — or null when the employer has no integration. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerHrisResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/hris", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerHrisResponse, - parse_obj_as( - type_=EmployerHrisResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_invoices( - self, - employer_id: EmployerId, - *, - limit: typing.Optional[Limit] = None, - offset: typing.Optional[Offset] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[EmployerInvoicesListResponse]: - """ - Returns a cursor-paginated page of the employer's billing invoices, newest first. Pass the `next_offset` from a previous page as `offset` to fetch the next page. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - limit : typing.Optional[Limit] - Maximum number of invoices per page - - offset : typing.Optional[Offset] - Opaque cursor from a previous page's next_offset - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerInvoicesListResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/invoices", - method="GET", - params={ - "limit": limit, - "offset": offset, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerInvoicesListResponse, - parse_obj_as( - type_=EmployerInvoicesListResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get_invoice_pdf( - self, employer_id: EmployerId, invoice_id: InvoiceId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[EmployerInvoicePdfResponse]: - """ - Returns the time-limited PDF download link for a single invoice belonging to the employer's billing customer. `invoice_id` is the external Chargebee id (not a prefixed UUID). The caller must be authorized for the employer; an unknown or unauthorized employer or invoice returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - invoice_id : InvoiceId - External Chargebee invoice id (not a prefixed UUID). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerInvoicePdfResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/invoices/{encode_path_param(invoice_id)}/pdf", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerInvoicePdfResponse, - parse_obj_as( - type_=EmployerInvoicePdfResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get_payroll_access_setup( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[PayrollAccessSetupStatusResponse]: - """ - Return whether the employer has submitted payroll access setup. - - Parameters - ---------- - employer_id : EmployerId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[PayrollAccessSetupStatusResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/payroll-access-setup", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PayrollAccessSetupStatusResponse, - parse_obj_as( - type_=PayrollAccessSetupStatusResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def submit_payroll_access_setup( - self, - employer_id: EmployerId, - *, - employees_in_payroll_acknowledged: bool, - payroll_data_impacts_eligibility_acknowledged: bool, - classifications_accurate: bool, - all_benefit_eligible_employees_present: bool, - is_controlled_group: bool, - access_method: AccessMethod, - has_additional_payroll_system: bool, - classification_correction_source: typing.Optional[ClassificationCorrectionSource] = OMIT, - misclassified_employee_names: typing.Optional[typing.Sequence[str]] = OMIT, - remaining_employee_action: typing.Optional[RemainingEmployeeAction] = OMIT, - missing_employee_resolution: typing.Optional[MissingEmployeeResolution] = OMIT, - same_payroll_covers_other_eins: typing.Optional[bool] = OMIT, - login_url: typing.Optional[str] = OMIT, - username: typing.Optional[str] = OMIT, - phone: typing.Optional[str] = OMIT, - password: typing.Optional[str] = OMIT, - integration_confirmed: typing.Optional[bool] = OMIT, - additional_access_method: typing.Optional[AdditionalAccessMethod] = OMIT, - additional_login_url: typing.Optional[str] = OMIT, - additional_username: typing.Optional[str] = OMIT, - additional_phone: typing.Optional[str] = OMIT, - additional_password: typing.Optional[str] = OMIT, - additional_integration_confirmed: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[PayrollAccessSetupStatusResponse]: - """ - Submit the employer's payroll access setup answers. - - Parameters - ---------- - employer_id : EmployerId - - employees_in_payroll_acknowledged : bool - Attestation that all benefit-eligible employees appear in the payroll system. - - payroll_data_impacts_eligibility_acknowledged : bool - Attestation that changes to payroll data affect benefit eligibility. - - classifications_accurate : bool - Attestation that employee classifications in payroll are accurate. Set `false` to report corrections in the fields below. - - all_benefit_eligible_employees_present : bool - Attestation that every benefit-eligible employee is present in payroll. - - is_controlled_group : bool - Whether this employer belongs to a controlled group of related entities. - - access_method : AccessMethod - - has_additional_payroll_system : bool - Whether a second payroll system is in use. When `true`, supply the `additional_*` fields below. - - classification_correction_source : typing.Optional[ClassificationCorrectionSource] - Where corrected classifications come from, when `classifications_accurate` is `false`. - - misclassified_employee_names : typing.Optional[typing.Sequence[str]] - Names of employees whose payroll classification needs correcting. - - remaining_employee_action : typing.Optional[RemainingEmployeeAction] - How to handle employees still missing from the payroll system. - - missing_employee_resolution : typing.Optional[MissingEmployeeResolution] - How any missing employees will be added, when some are absent. - - same_payroll_covers_other_eins : typing.Optional[bool] - Whether this payroll system also covers other EINs in the controlled group. - - login_url : typing.Optional[str] - Sign-in URL for the payroll system. - - username : typing.Optional[str] - Username Vitable should use to access the payroll system. - - phone : typing.Optional[str] - Phone number used for payroll-system verification codes. - - password : typing.Optional[str] - Password Vitable should use to access the payroll system. - - integration_confirmed : typing.Optional[bool] - Whether the payroll integration has been confirmed as working. - - additional_access_method : typing.Optional[AdditionalAccessMethod] - How Vitable will access the second payroll system. - - additional_login_url : typing.Optional[str] - Sign-in URL for the second payroll system. - - additional_username : typing.Optional[str] - Username Vitable should use for the second payroll system. - - additional_phone : typing.Optional[str] - Phone number used for second payroll-system verification codes. - - additional_password : typing.Optional[str] - Password Vitable should use for the second payroll system. - - additional_integration_confirmed : typing.Optional[bool] - Whether the second payroll integration has been confirmed as working. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[PayrollAccessSetupStatusResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/payroll-access-setup", - method="PUT", - json={ - "employees_in_payroll_acknowledged": employees_in_payroll_acknowledged, - "payroll_data_impacts_eligibility_acknowledged": payroll_data_impacts_eligibility_acknowledged, - "classifications_accurate": classifications_accurate, - "classification_correction_source": classification_correction_source, - "misclassified_employee_names": misclassified_employee_names, - "remaining_employee_action": remaining_employee_action, - "all_benefit_eligible_employees_present": all_benefit_eligible_employees_present, - "missing_employee_resolution": missing_employee_resolution, - "is_controlled_group": is_controlled_group, - "same_payroll_covers_other_eins": same_payroll_covers_other_eins, - "access_method": access_method, - "login_url": login_url, - "username": username, - "phone": phone, - "password": password, - "integration_confirmed": integration_confirmed, - "has_additional_payroll_system": has_additional_payroll_system, - "additional_access_method": additional_access_method, - "additional_login_url": additional_login_url, - "additional_username": additional_username, - "additional_phone": additional_phone, - "additional_password": additional_password, - "additional_integration_confirmed": additional_integration_confirmed, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PayrollAccessSetupStatusResponse, - parse_obj_as( - type_=PayrollAccessSetupStatusResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_payroll_deduction_statements( - self, - employer_id: EmployerId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[PayrollDeductionStatement, EmployerPayrollDeductionStatementListResponse]: - """ - Returns a paginated list of the employer's payroll-deduction statements, newest period first, each with its period, generation date, distinct employee count, total deduction, change-file link, and deduction frequency. Statements superseded by a later correction are excluded. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - limit : typing.Optional[Limit] - Maximum number of statements per page - - page : typing.Optional[Page] - Page number to retrieve (starts at 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[PayrollDeductionStatement, EmployerPayrollDeductionStatementListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/payroll-deduction-statements", - method="GET", - params={ - "limit": limit, - "page": page, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - EmployerPayrollDeductionStatementListResponse, - parse_obj_as( - type_=EmployerPayrollDeductionStatementListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list_payroll_deduction_statements( - employer_id, - limit=limit, - page=page + 1, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def ensure_payroll_integration_email( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[PayrollIntegrationEmailResponse]: - """ - Provision and return the employer's payroll integration email. - - Parameters - ---------- - employer_id : EmployerId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[PayrollIntegrationEmailResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/payroll-integration-email", - method="PUT", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PayrollIntegrationEmailResponse, - parse_obj_as( - type_=PayrollIntegrationEmailResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def update_settings( - self, - employer_id: EmployerId, - *, - pay_frequency: DeductionFrequency, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[EmployerSettingsResponse]: - """ - Updates configuration settings for a specific employer. The employer must belong to the authenticated organization. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - pay_frequency : DeductionFrequency - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EmployerSettingsResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/settings", - method="PUT", - json={ - "pay_frequency": pay_frequency, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerSettingsResponse, - parse_obj_as( - type_=EmployerSettingsResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_hris_providers( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[OrganizationHrisProvidersResponse]: - """ - Returns the distinct HRIS/payroll providers across the same book `GET /v1/employers` returns, sorted for display. Use these as the values for the employers list's `hris_provider` filter — filter on `provider`, show `provider_label`. The stored providers are free text, so they cannot be enumerated in advance. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[OrganizationHrisProvidersResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - "v1/employers/hris-providers", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - OrganizationHrisProvidersResponse, - parse_obj_as( - type_=OrganizationHrisProvidersResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawEmployersClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list( - self, - *, - benefit_family: typing.Optional[ - typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]] - ] = None, - benefit_lifecycle_stage: typing.Optional[ - typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]] - ] = None, - hris_provider: typing.Optional[typing.Union[str, typing.Sequence[str]]] = None, - hris_status: typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]] = None, - include_cancelled: typing.Optional[IncludeCancelled] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[Search] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[OrganizationEmployer, OrganizationEmployerListResponse]: - """ - Returns the caller's employer book — every employer with its computed columns (enrollment-rate summary, benefit-family tags, HRIS connection, benefit-lifecycle stage) merged with the employer's flat CRM fields (legal name, EIN, contact, address, timestamps). The book is derived from the authenticated principal: one organization's employers, or every organization's for a caller whose reach is not a single organization. Supports search by display name, legal name, or exact EIN, employer id or contact email, benefit-family/lifecycle/HRIS filters, and page/limit pagination. - - Parameters - ---------- - benefit_family : typing.Optional[typing.Union[BenefitFamilyParamItem, typing.Sequence[BenefitFamilyParamItem]]] - Filter to employers with at least one active benefit in these families. - - benefit_lifecycle_stage : typing.Optional[typing.Union[BenefitLifecycleStageItem, typing.Sequence[BenefitLifecycleStageItem]]] - Filter to employers in one of these computed benefit-lifecycle stages. - - hris_provider : typing.Optional[typing.Union[str, typing.Sequence[str]]] - Filter to employers whose HRIS connection is with one of these payroll providers (e.g. `ADP RUN`). Matched case-insensitively; free text, so read the available values from the HRIS-providers endpoint rather than assuming a fixed set. - - hris_status : typing.Optional[typing.Union[HrisStatusItem, typing.Sequence[HrisStatusItem]]] - Filter to employers whose HRIS connection is in one of these statuses. - - include_cancelled : typing.Optional[IncludeCancelled] - Include cancelled employers (hidden by default unless their stage is explicitly requested). - - limit : typing.Optional[Limit] - Items per page. - - page : typing.Optional[Page] - Page number. - - search : typing.Optional[Search] - Employer filter. Matches the display name or the legal name case-insensitively as a substring, or one of these exactly: the EIN (with or without its dash), the employer id, or the contact email of one of the employer's non-disabled admins. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[OrganizationEmployer, OrganizationEmployerListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - "v1/employers", - method="GET", - params={ - "benefit_family": benefit_family, - "benefit_lifecycle_stage": benefit_lifecycle_stage, - "hris_provider": hris_provider, - "hris_status": hris_status, - "include_cancelled": include_cancelled, - "limit": limit, - "page": page, - "search": search, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - OrganizationEmployerListResponse, - parse_obj_as( - type_=OrganizationEmployerListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list( - benefit_family=benefit_family, - benefit_lifecycle_stage=benefit_lifecycle_stage, - hris_provider=hris_provider, - hris_status=hris_status, - include_cancelled=include_cancelled, - limit=limit, - page=page + 1, - search=search, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def create( - self, - *, - name: str, - legal_name: str, - ein: str, - email: str, - address: EmployerAddressInput, - phone_number: typing.Optional[str] = OMIT, - reference_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[EmployerResponse]: - """ - Creates a new employer for the authenticated organization. Requires employer name, legal name, EIN, email, and address information. Returns the created employer with its assigned ID. - - Parameters - ---------- - name : str - Employer display name - - legal_name : str - Legal business name - - ein : str - Employer Identification Number (format: XX-XXXXXXX) - - email : str - Email address for billing and communications - - address : EmployerAddressInput - Employer address - - phone_number : typing.Optional[str] - Employer phone number (10-digit US format, e.g. 5551234567) - - reference_id : typing.Optional[str] - External reference ID for this employer - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/employers", - method="POST", - json={ - "name": name, - "legal_name": legal_name, - "ein": ein, - "email": email, - "address": convert_and_respect_annotation_metadata( - object_=address, annotation=EmployerAddressInput, direction="write" - ), - "phone_number": phone_number, - "reference_id": reference_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerResponse, - parse_obj_as( - type_=EmployerResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[EmployerResponse]: - """ - Retrieves detailed information for a specific employer by ID. The employer must belong to the authenticated organization. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerResponse, - parse_obj_as( - type_=EmployerResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def update( - self, - employer_id: EmployerId, - *, - name: typing.Optional[str] = OMIT, - legal_name: typing.Optional[str] = OMIT, - address: typing.Optional[UpdateEmployerAddressInput] = OMIT, - active: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[EmployerResponse]: - """ - Updates an existing employer. All fields are optional — only provided fields are updated. PO Box addresses are rejected. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - name : typing.Optional[str] - Employer display name - - legal_name : typing.Optional[str] - Legal business name - - address : typing.Optional[UpdateEmployerAddressInput] - Employer address - - active : typing.Optional[bool] - Whether the employer is active - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}", - method="PUT", - json={ - "name": name, - "legal_name": legal_name, - "address": convert_and_respect_annotation_metadata( - object_=address, annotation=typing.Optional[UpdateEmployerAddressInput], direction="write" - ), - "active": active, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerResponse, - parse_obj_as( - type_=EmployerResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_benefit_plan_years( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[EmployerBenefitPlanYearsListResponse]: - """ - Returns the employer's benefit plan years (all years, or one when `year` is given), each with its benefits, offered states, benefit families, and the year-level enrollment roll-up. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerBenefitPlanYearsListResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/benefit-plan-years", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerBenefitPlanYearsListResponse, - parse_obj_as( - type_=EmployerBenefitPlanYearsListResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get_benefit_plan_year( - self, - employer_id: EmployerId, - benefit_plan_year_id: BenefitPlanYearId, - *, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[EmployerBenefitPlanYearResponse]: - """ - Returns one benefit plan year in full — its benefit details plus the per-benefit enrollment rate and SPD link — addressed by its `benefit_plan_year_id`. The caller must be authorized for the employer; an unknown or unauthorized plan year returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - benefit_plan_year_id : BenefitPlanYearId - Unique benefit-plan-year identifier (plyr_*). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerBenefitPlanYearResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/benefit-plan-years/{encode_path_param(benefit_plan_year_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerBenefitPlanYearResponse, - parse_obj_as( - type_=EmployerBenefitPlanYearResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_benefit_plan_year_enrollments( - self, - employer_id: EmployerId, - benefit_plan_year_id: BenefitPlanYearId, - *, - election_status: typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[EmployerSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[PlanYearEnrollment, PlanYearEnrollmentListResponse]: - """ - Returns a paginated list of every member with an enrollment in one of an employer's plan years, any election status: what they elected, where their coverage stands, dependent count, carrier, plan, tier, and the plan's total monthly cost. The caller must be authorized for the employer `empr_<...>`; an unknown or unauthorized employer, or an unknown plan year `plyr_<...>`, returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*). - - benefit_plan_year_id : BenefitPlanYearId - Unique benefit-plan-year identifier (plyr_*). - - election_status : typing.Optional[typing.Union[ElectionStatusItem, typing.Sequence[ElectionStatusItem]]] - Filter by election status. Repeat the parameter to match several. - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[EmployerSearch] - Case-insensitive search. Matches member name partially, and the `member_id` exactly — either your own reference id or the prefixed `grpmbr_<...>` id. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[PlanYearEnrollment, PlanYearEnrollmentListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/benefit-plan-years/{encode_path_param(benefit_plan_year_id)}/enrollments", - method="GET", - params={ - "election_status": election_status, - "limit": limit, - "page": page, - "search": search, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - PlanYearEnrollmentListResponse, - parse_obj_as( - type_=PlanYearEnrollmentListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list_benefit_plan_year_enrollments( - employer_id, - benefit_plan_year_id, - election_status=election_status, - limit=limit, - page=page + 1, - search=search, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def submit_census_sync( - self, - employer_id: EmployerId, - *, - employees: typing.Sequence[CensusSyncEmployeeRequest], - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[CensusSyncDetailResponse]: - """ - Submits a census sync payload for the specified employer. The employees in the payload will be queued for processing. Returns an accepted response with the timestamp of acceptance. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - employees : typing.Sequence[CensusSyncEmployeeRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[CensusSyncDetailResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/census-sync", - method="POST", - json={ - "employees": convert_and_respect_annotation_metadata( - object_=employees, annotation=typing.Sequence[CensusSyncEmployeeRequest], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - CensusSyncDetailResponse, - parse_obj_as( - type_=CensusSyncDetailResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_employees( - self, - employer_id: EmployerId, - *, - employment_status: typing.Optional[EmployeeStatus] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[EmployeeSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[Employee, EmployeeListResponse]: - """ - Retrieves a paginated list of employees for a specific employer. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. Results are paginated using page and limit parameters and can be narrowed with a case-insensitive `search` (first name, last name, or email) and an `employment_status` filter (active or terminated). Each employee includes payroll deductions from the most recent statement period. When a new deduction statement is generated, previous period deductions are replaced. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - employment_status : typing.Optional[EmployeeStatus] - Filter by employment status (active or terminated) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[EmployeeSearch] - Case-insensitive search across employee first name, last name, and email - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[Employee, EmployeeListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/employees", - method="GET", - params={ - "employment_status": employment_status, - "limit": limit, - "page": page, - "search": search, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - EmployeeListResponse, - parse_obj_as( - type_=EmployeeListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list_employees( - employer_id, - employment_status=employment_status, - limit=limit, - page=page + 1, - search=search, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get_hris( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[EmployerHrisResponse]: - """ - Returns the employer's HRIS connection — provider, status, last sync, and synced row count — or null when the employer has no integration. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerHrisResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/hris", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerHrisResponse, - parse_obj_as( - type_=EmployerHrisResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_invoices( - self, - employer_id: EmployerId, - *, - limit: typing.Optional[Limit] = None, - offset: typing.Optional[Offset] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[EmployerInvoicesListResponse]: - """ - Returns a cursor-paginated page of the employer's billing invoices, newest first. Pass the `next_offset` from a previous page as `offset` to fetch the next page. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - limit : typing.Optional[Limit] - Maximum number of invoices per page - - offset : typing.Optional[Offset] - Opaque cursor from a previous page's next_offset - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerInvoicesListResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/invoices", - method="GET", - params={ - "limit": limit, - "offset": offset, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerInvoicesListResponse, - parse_obj_as( - type_=EmployerInvoicesListResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get_invoice_pdf( - self, employer_id: EmployerId, invoice_id: InvoiceId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[EmployerInvoicePdfResponse]: - """ - Returns the time-limited PDF download link for a single invoice belonging to the employer's billing customer. `invoice_id` is the external Chargebee id (not a prefixed UUID). The caller must be authorized for the employer; an unknown or unauthorized employer or invoice returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - invoice_id : InvoiceId - External Chargebee invoice id (not a prefixed UUID). - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerInvoicePdfResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/invoices/{encode_path_param(invoice_id)}/pdf", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerInvoicePdfResponse, - parse_obj_as( - type_=EmployerInvoicePdfResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get_payroll_access_setup( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[PayrollAccessSetupStatusResponse]: - """ - Return whether the employer has submitted payroll access setup. - - Parameters - ---------- - employer_id : EmployerId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[PayrollAccessSetupStatusResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/payroll-access-setup", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PayrollAccessSetupStatusResponse, - parse_obj_as( - type_=PayrollAccessSetupStatusResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def submit_payroll_access_setup( - self, - employer_id: EmployerId, - *, - employees_in_payroll_acknowledged: bool, - payroll_data_impacts_eligibility_acknowledged: bool, - classifications_accurate: bool, - all_benefit_eligible_employees_present: bool, - is_controlled_group: bool, - access_method: AccessMethod, - has_additional_payroll_system: bool, - classification_correction_source: typing.Optional[ClassificationCorrectionSource] = OMIT, - misclassified_employee_names: typing.Optional[typing.Sequence[str]] = OMIT, - remaining_employee_action: typing.Optional[RemainingEmployeeAction] = OMIT, - missing_employee_resolution: typing.Optional[MissingEmployeeResolution] = OMIT, - same_payroll_covers_other_eins: typing.Optional[bool] = OMIT, - login_url: typing.Optional[str] = OMIT, - username: typing.Optional[str] = OMIT, - phone: typing.Optional[str] = OMIT, - password: typing.Optional[str] = OMIT, - integration_confirmed: typing.Optional[bool] = OMIT, - additional_access_method: typing.Optional[AdditionalAccessMethod] = OMIT, - additional_login_url: typing.Optional[str] = OMIT, - additional_username: typing.Optional[str] = OMIT, - additional_phone: typing.Optional[str] = OMIT, - additional_password: typing.Optional[str] = OMIT, - additional_integration_confirmed: typing.Optional[bool] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[PayrollAccessSetupStatusResponse]: - """ - Submit the employer's payroll access setup answers. - - Parameters - ---------- - employer_id : EmployerId - - employees_in_payroll_acknowledged : bool - Attestation that all benefit-eligible employees appear in the payroll system. - - payroll_data_impacts_eligibility_acknowledged : bool - Attestation that changes to payroll data affect benefit eligibility. - - classifications_accurate : bool - Attestation that employee classifications in payroll are accurate. Set `false` to report corrections in the fields below. - - all_benefit_eligible_employees_present : bool - Attestation that every benefit-eligible employee is present in payroll. - - is_controlled_group : bool - Whether this employer belongs to a controlled group of related entities. - - access_method : AccessMethod - - has_additional_payroll_system : bool - Whether a second payroll system is in use. When `true`, supply the `additional_*` fields below. - - classification_correction_source : typing.Optional[ClassificationCorrectionSource] - Where corrected classifications come from, when `classifications_accurate` is `false`. - - misclassified_employee_names : typing.Optional[typing.Sequence[str]] - Names of employees whose payroll classification needs correcting. - - remaining_employee_action : typing.Optional[RemainingEmployeeAction] - How to handle employees still missing from the payroll system. - - missing_employee_resolution : typing.Optional[MissingEmployeeResolution] - How any missing employees will be added, when some are absent. - - same_payroll_covers_other_eins : typing.Optional[bool] - Whether this payroll system also covers other EINs in the controlled group. - - login_url : typing.Optional[str] - Sign-in URL for the payroll system. - - username : typing.Optional[str] - Username Vitable should use to access the payroll system. - - phone : typing.Optional[str] - Phone number used for payroll-system verification codes. - - password : typing.Optional[str] - Password Vitable should use to access the payroll system. - - integration_confirmed : typing.Optional[bool] - Whether the payroll integration has been confirmed as working. - - additional_access_method : typing.Optional[AdditionalAccessMethod] - How Vitable will access the second payroll system. - - additional_login_url : typing.Optional[str] - Sign-in URL for the second payroll system. - - additional_username : typing.Optional[str] - Username Vitable should use for the second payroll system. - - additional_phone : typing.Optional[str] - Phone number used for second payroll-system verification codes. - - additional_password : typing.Optional[str] - Password Vitable should use for the second payroll system. - - additional_integration_confirmed : typing.Optional[bool] - Whether the second payroll integration has been confirmed as working. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[PayrollAccessSetupStatusResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/payroll-access-setup", - method="PUT", - json={ - "employees_in_payroll_acknowledged": employees_in_payroll_acknowledged, - "payroll_data_impacts_eligibility_acknowledged": payroll_data_impacts_eligibility_acknowledged, - "classifications_accurate": classifications_accurate, - "classification_correction_source": classification_correction_source, - "misclassified_employee_names": misclassified_employee_names, - "remaining_employee_action": remaining_employee_action, - "all_benefit_eligible_employees_present": all_benefit_eligible_employees_present, - "missing_employee_resolution": missing_employee_resolution, - "is_controlled_group": is_controlled_group, - "same_payroll_covers_other_eins": same_payroll_covers_other_eins, - "access_method": access_method, - "login_url": login_url, - "username": username, - "phone": phone, - "password": password, - "integration_confirmed": integration_confirmed, - "has_additional_payroll_system": has_additional_payroll_system, - "additional_access_method": additional_access_method, - "additional_login_url": additional_login_url, - "additional_username": additional_username, - "additional_phone": additional_phone, - "additional_password": additional_password, - "additional_integration_confirmed": additional_integration_confirmed, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PayrollAccessSetupStatusResponse, - parse_obj_as( - type_=PayrollAccessSetupStatusResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_payroll_deduction_statements( - self, - employer_id: EmployerId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[PayrollDeductionStatement, EmployerPayrollDeductionStatementListResponse]: - """ - Returns a paginated list of the employer's payroll-deduction statements, newest period first, each with its period, generation date, distinct employee count, total deduction, change-file link, and deduction frequency. Statements superseded by a later correction are excluded. The caller must be authorized for the employer; an unknown or unauthorized employer returns 404. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - limit : typing.Optional[Limit] - Maximum number of statements per page - - page : typing.Optional[Page] - Page number to retrieve (starts at 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[PayrollDeductionStatement, EmployerPayrollDeductionStatementListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/payroll-deduction-statements", - method="GET", - params={ - "limit": limit, - "page": page, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - EmployerPayrollDeductionStatementListResponse, - parse_obj_as( - type_=EmployerPayrollDeductionStatementListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list_payroll_deduction_statements( - employer_id, - limit=limit, - page=page + 1, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def ensure_payroll_integration_email( - self, employer_id: EmployerId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[PayrollIntegrationEmailResponse]: - """ - Provision and return the employer's payroll integration email. - - Parameters - ---------- - employer_id : EmployerId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[PayrollIntegrationEmailResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/payroll-integration-email", - method="PUT", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - PayrollIntegrationEmailResponse, - parse_obj_as( - type_=PayrollIntegrationEmailResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def update_settings( - self, - employer_id: EmployerId, - *, - pay_frequency: DeductionFrequency, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[EmployerSettingsResponse]: - """ - Updates configuration settings for a specific employer. The employer must belong to the authenticated organization. - - Parameters - ---------- - employer_id : EmployerId - Unique employer identifier (empr_*) - - pay_frequency : DeductionFrequency - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EmployerSettingsResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/employers/{encode_path_param(employer_id)}/settings", - method="PUT", - json={ - "pay_frequency": pay_frequency, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EmployerSettingsResponse, - parse_obj_as( - type_=EmployerSettingsResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_hris_providers( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[OrganizationHrisProvidersResponse]: - """ - Returns the distinct HRIS/payroll providers across the same book `GET /v1/employers` returns, sorted for display. Use these as the values for the employers list's `hris_provider` filter — filter on `provider`, show `provider_label`. The stored providers are free text, so they cannot be enumerated in advance. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[OrganizationHrisProvidersResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/employers/hris-providers", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - OrganizationHrisProvidersResponse, - parse_obj_as( - type_=OrganizationHrisProvidersResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/enrollments/__init__.py b/src/vitable_connect/enrollments/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/enrollments/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/enrollments/client.py b/src/vitable_connect/enrollments/client.py deleted file mode 100644 index d357cd6..0000000 --- a/src/vitable_connect/enrollments/client.py +++ /dev/null @@ -1,361 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from ..types.enrollment_id import EnrollmentId -from ..types.enrollment_response import EnrollmentResponse -from ..types.reissue_enrollment_response import ReissueEnrollmentResponse -from .raw_client import AsyncRawEnrollmentsClient, RawEnrollmentsClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class EnrollmentsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawEnrollmentsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawEnrollmentsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawEnrollmentsClient - """ - return self._raw_client - - def get( - self, enrollment_id: EnrollmentId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EnrollmentResponse: - """ - Retrieves a single enrollment: the employee and employer it belongs to, the benefit product, its status, the coverage period, the employee payroll deduction and employer contribution, and the enrolled plan's Summary of Benefits and Coverage document when one is on file. An enrollment the caller cannot reach is indistinguishable from one that does not exist. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EnrollmentResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.enrollments.get( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", - ) - """ - _response = self._raw_client.get(enrollment_id, request_options=request_options) - return _response.data - - def reissue( - self, - enrollment_id: EnrollmentId, - *, - reason: typing.Optional[str] = OMIT, - ticket_number: typing.Optional[str] = OMIT, - qualifying_life_event_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ReissueEnrollmentResponse: - """ - Closes the targeted enrollment and creates a new unanswered enrollment for the same member and plan year. VPC never requires a qualifying life event; other products require an accepted, member-owned event outside open enrollment. User-backed callers must provide a reason; it is optional for organization API-key callers. Tenant mismatches return a non-disclosing 404 before the request body is validated. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - reason : typing.Optional[str] - Audit reason for the reissue; required for user-backed callers and optional for long-lived organization API-key callers - - ticket_number : typing.Optional[str] - Optional support or operational ticket number - - qualifying_life_event_id : typing.Optional[str] - Accepted member qualifying life event identifier (qle_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ReissueEnrollmentResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.enrollments.reissue( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", - reason="Member needs a new election after a qualifying event.", - ticket_number="BPT-1234", - qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", - ) - """ - _response = self._raw_client.reissue( - enrollment_id, - reason=reason, - ticket_number=ticket_number, - qualifying_life_event_id=qualifying_life_event_id, - request_options=request_options, - ) - return _response.data - - def terminate( - self, - enrollment_id: EnrollmentId, - *, - reason: typing.Optional[str] = OMIT, - ticket_number: typing.Optional[str] = OMIT, - qualifying_life_event_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> None: - """ - Terminates enrolled coverage immediately. An accepted qualifying life event owned by the enrollment member is required unless the plan is VPC or ICHRA. User-backed callers must provide a reason; it is optional for organization API-key callers. API keys may act across the caller organization's book. Tenant mismatches return the same non-disclosing 404 before the request body is validated. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - reason : typing.Optional[str] - Audit reason for the termination; required for user-backed callers and optional for long-lived organization API-key callers - - ticket_number : typing.Optional[str] - Optional support or operational ticket number - - qualifying_life_event_id : typing.Optional[str] - Accepted member qualifying life event identifier (qle_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.enrollments.terminate( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", - reason="Member requested coverage termination after a qualifying event.", - ticket_number="BPT-1234", - qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", - ) - """ - _response = self._raw_client.terminate( - enrollment_id, - reason=reason, - ticket_number=ticket_number, - qualifying_life_event_id=qualifying_life_event_id, - request_options=request_options, - ) - return _response.data - - -class AsyncEnrollmentsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawEnrollmentsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawEnrollmentsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawEnrollmentsClient - """ - return self._raw_client - - async def get( - self, enrollment_id: EnrollmentId, *, request_options: typing.Optional[RequestOptions] = None - ) -> EnrollmentResponse: - """ - Retrieves a single enrollment: the employee and employer it belongs to, the benefit product, its status, the coverage period, the employee payroll deduction and employer contribution, and the enrolled plan's Summary of Benefits and Coverage document when one is on file. An enrollment the caller cannot reach is indistinguishable from one that does not exist. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - EnrollmentResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.enrollments.get( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get(enrollment_id, request_options=request_options) - return _response.data - - async def reissue( - self, - enrollment_id: EnrollmentId, - *, - reason: typing.Optional[str] = OMIT, - ticket_number: typing.Optional[str] = OMIT, - qualifying_life_event_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> ReissueEnrollmentResponse: - """ - Closes the targeted enrollment and creates a new unanswered enrollment for the same member and plan year. VPC never requires a qualifying life event; other products require an accepted, member-owned event outside open enrollment. User-backed callers must provide a reason; it is optional for organization API-key callers. Tenant mismatches return a non-disclosing 404 before the request body is validated. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - reason : typing.Optional[str] - Audit reason for the reissue; required for user-backed callers and optional for long-lived organization API-key callers - - ticket_number : typing.Optional[str] - Optional support or operational ticket number - - qualifying_life_event_id : typing.Optional[str] - Accepted member qualifying life event identifier (qle_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ReissueEnrollmentResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.enrollments.reissue( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", - reason="Member needs a new election after a qualifying event.", - ticket_number="BPT-1234", - qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.reissue( - enrollment_id, - reason=reason, - ticket_number=ticket_number, - qualifying_life_event_id=qualifying_life_event_id, - request_options=request_options, - ) - return _response.data - - async def terminate( - self, - enrollment_id: EnrollmentId, - *, - reason: typing.Optional[str] = OMIT, - ticket_number: typing.Optional[str] = OMIT, - qualifying_life_event_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> None: - """ - Terminates enrolled coverage immediately. An accepted qualifying life event owned by the enrollment member is required unless the plan is VPC or ICHRA. User-backed callers must provide a reason; it is optional for organization API-key callers. API keys may act across the caller organization's book. Tenant mismatches return the same non-disclosing 404 before the request body is validated. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - reason : typing.Optional[str] - Audit reason for the termination; required for user-backed callers and optional for long-lived organization API-key callers - - ticket_number : typing.Optional[str] - Optional support or operational ticket number - - qualifying_life_event_id : typing.Optional[str] - Accepted member qualifying life event identifier (qle_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - None - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.enrollments.terminate( - enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", - reason="Member requested coverage termination after a qualifying event.", - ticket_number="BPT-1234", - qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.terminate( - enrollment_id, - reason=reason, - ticket_number=ticket_number, - qualifying_life_event_id=qualifying_life_event_id, - request_options=request_options, - ) - return _response.data diff --git a/src/vitable_connect/enrollments/raw_client.py b/src/vitable_connect/enrollments/raw_client.py deleted file mode 100644 index a41ad02..0000000 --- a/src/vitable_connect/enrollments/raw_client.py +++ /dev/null @@ -1,949 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.enrollment_id import EnrollmentId -from ..types.enrollment_response import EnrollmentResponse -from ..types.error_response import ErrorResponse -from ..types.reissue_enrollment_response import ReissueEnrollmentResponse -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawEnrollmentsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get( - self, enrollment_id: EnrollmentId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[EnrollmentResponse]: - """ - Retrieves a single enrollment: the employee and employer it belongs to, the benefit product, its status, the coverage period, the employee payroll deduction and employer contribution, and the enrolled plan's Summary of Benefits and Coverage document when one is on file. An enrollment the caller cannot reach is indistinguishable from one that does not exist. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[EnrollmentResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/enrollments/{encode_path_param(enrollment_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EnrollmentResponse, - parse_obj_as( - type_=EnrollmentResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def reissue( - self, - enrollment_id: EnrollmentId, - *, - reason: typing.Optional[str] = OMIT, - ticket_number: typing.Optional[str] = OMIT, - qualifying_life_event_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[ReissueEnrollmentResponse]: - """ - Closes the targeted enrollment and creates a new unanswered enrollment for the same member and plan year. VPC never requires a qualifying life event; other products require an accepted, member-owned event outside open enrollment. User-backed callers must provide a reason; it is optional for organization API-key callers. Tenant mismatches return a non-disclosing 404 before the request body is validated. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - reason : typing.Optional[str] - Audit reason for the reissue; required for user-backed callers and optional for long-lived organization API-key callers - - ticket_number : typing.Optional[str] - Optional support or operational ticket number - - qualifying_life_event_id : typing.Optional[str] - Accepted member qualifying life event identifier (qle_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ReissueEnrollmentResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/enrollments/{encode_path_param(enrollment_id)}/reissue", - method="POST", - json={ - "reason": reason, - "ticket_number": ticket_number, - "qualifying_life_event_id": qualifying_life_event_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ReissueEnrollmentResponse, - parse_obj_as( - type_=ReissueEnrollmentResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def terminate( - self, - enrollment_id: EnrollmentId, - *, - reason: typing.Optional[str] = OMIT, - ticket_number: typing.Optional[str] = OMIT, - qualifying_life_event_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[None]: - """ - Terminates enrolled coverage immediately. An accepted qualifying life event owned by the enrollment member is required unless the plan is VPC or ICHRA. User-backed callers must provide a reason; it is optional for organization API-key callers. API keys may act across the caller organization's book. Tenant mismatches return the same non-disclosing 404 before the request body is validated. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - reason : typing.Optional[str] - Audit reason for the termination; required for user-backed callers and optional for long-lived organization API-key callers - - ticket_number : typing.Optional[str] - Optional support or operational ticket number - - qualifying_life_event_id : typing.Optional[str] - Accepted member qualifying life event identifier (qle_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[None] - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/enrollments/{encode_path_param(enrollment_id)}/terminate", - method="POST", - json={ - "reason": reason, - "ticket_number": ticket_number, - "qualifying_life_event_id": qualifying_life_event_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return HttpResponse(response=_response, data=None) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawEnrollmentsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get( - self, enrollment_id: EnrollmentId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[EnrollmentResponse]: - """ - Retrieves a single enrollment: the employee and employer it belongs to, the benefit product, its status, the coverage period, the employee payroll deduction and employer contribution, and the enrolled plan's Summary of Benefits and Coverage document when one is on file. An enrollment the caller cannot reach is indistinguishable from one that does not exist. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[EnrollmentResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/enrollments/{encode_path_param(enrollment_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - EnrollmentResponse, - parse_obj_as( - type_=EnrollmentResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def reissue( - self, - enrollment_id: EnrollmentId, - *, - reason: typing.Optional[str] = OMIT, - ticket_number: typing.Optional[str] = OMIT, - qualifying_life_event_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[ReissueEnrollmentResponse]: - """ - Closes the targeted enrollment and creates a new unanswered enrollment for the same member and plan year. VPC never requires a qualifying life event; other products require an accepted, member-owned event outside open enrollment. User-backed callers must provide a reason; it is optional for organization API-key callers. Tenant mismatches return a non-disclosing 404 before the request body is validated. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - reason : typing.Optional[str] - Audit reason for the reissue; required for user-backed callers and optional for long-lived organization API-key callers - - ticket_number : typing.Optional[str] - Optional support or operational ticket number - - qualifying_life_event_id : typing.Optional[str] - Accepted member qualifying life event identifier (qle_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ReissueEnrollmentResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/enrollments/{encode_path_param(enrollment_id)}/reissue", - method="POST", - json={ - "reason": reason, - "ticket_number": ticket_number, - "qualifying_life_event_id": qualifying_life_event_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ReissueEnrollmentResponse, - parse_obj_as( - type_=ReissueEnrollmentResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def terminate( - self, - enrollment_id: EnrollmentId, - *, - reason: typing.Optional[str] = OMIT, - ticket_number: typing.Optional[str] = OMIT, - qualifying_life_event_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[None]: - """ - Terminates enrolled coverage immediately. An accepted qualifying life event owned by the enrollment member is required unless the plan is VPC or ICHRA. User-backed callers must provide a reason; it is optional for organization API-key callers. API keys may act across the caller organization's book. Tenant mismatches return the same non-disclosing 404 before the request body is validated. - - Parameters - ---------- - enrollment_id : EnrollmentId - Unique enrollment identifier (enrl_*) - - reason : typing.Optional[str] - Audit reason for the termination; required for user-backed callers and optional for long-lived organization API-key callers - - ticket_number : typing.Optional[str] - Optional support or operational ticket number - - qualifying_life_event_id : typing.Optional[str] - Accepted member qualifying life event identifier (qle_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[None] - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/enrollments/{encode_path_param(enrollment_id)}/terminate", - method="POST", - json={ - "reason": reason, - "ticket_number": ticket_number, - "qualifying_life_event_id": qualifying_life_event_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - return AsyncHttpResponse(response=_response, data=None) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/environment.py b/src/vitable_connect/environment.py deleted file mode 100644 index 169e7a7..0000000 --- a/src/vitable_connect/environment.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import enum - - -class VitableConnectEnvironment(enum.Enum): - PRODUCTION = "https://api.vitablehealth.com" - UAT = "https://api.uat.vitablehealth.com" diff --git a/src/vitable_connect/errors/__init__.py b/src/vitable_connect/errors/__init__.py deleted file mode 100644 index f057d36..0000000 --- a/src/vitable_connect/errors/__init__.py +++ /dev/null @@ -1,62 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .bad_gateway_error import BadGatewayError - from .bad_request_error import BadRequestError - from .conflict_error import ConflictError - from .forbidden_error import ForbiddenError - from .internal_server_error import InternalServerError - from .not_found_error import NotFoundError - from .too_many_requests_error import TooManyRequestsError - from .unauthorized_error import UnauthorizedError - from .unprocessable_entity_error import UnprocessableEntityError -_dynamic_imports: typing.Dict[str, str] = { - "BadGatewayError": ".bad_gateway_error", - "BadRequestError": ".bad_request_error", - "ConflictError": ".conflict_error", - "ForbiddenError": ".forbidden_error", - "InternalServerError": ".internal_server_error", - "NotFoundError": ".not_found_error", - "TooManyRequestsError": ".too_many_requests_error", - "UnauthorizedError": ".unauthorized_error", - "UnprocessableEntityError": ".unprocessable_entity_error", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e - except AttributeError as e: - raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = [ - "BadGatewayError", - "BadRequestError", - "ConflictError", - "ForbiddenError", - "InternalServerError", - "NotFoundError", - "TooManyRequestsError", - "UnauthorizedError", - "UnprocessableEntityError", -] diff --git a/src/vitable_connect/errors/bad_gateway_error.py b/src/vitable_connect/errors/bad_gateway_error.py deleted file mode 100644 index ee036fd..0000000 --- a/src/vitable_connect/errors/bad_gateway_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class BadGatewayError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=502, headers=headers, body=body) diff --git a/src/vitable_connect/errors/bad_request_error.py b/src/vitable_connect/errors/bad_request_error.py deleted file mode 100644 index 9244991..0000000 --- a/src/vitable_connect/errors/bad_request_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class BadRequestError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=400, headers=headers, body=body) diff --git a/src/vitable_connect/errors/conflict_error.py b/src/vitable_connect/errors/conflict_error.py deleted file mode 100644 index a53ae50..0000000 --- a/src/vitable_connect/errors/conflict_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class ConflictError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=409, headers=headers, body=body) diff --git a/src/vitable_connect/errors/forbidden_error.py b/src/vitable_connect/errors/forbidden_error.py deleted file mode 100644 index 0841085..0000000 --- a/src/vitable_connect/errors/forbidden_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class ForbiddenError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=403, headers=headers, body=body) diff --git a/src/vitable_connect/errors/internal_server_error.py b/src/vitable_connect/errors/internal_server_error.py deleted file mode 100644 index d7a796c..0000000 --- a/src/vitable_connect/errors/internal_server_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class InternalServerError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=500, headers=headers, body=body) diff --git a/src/vitable_connect/errors/not_found_error.py b/src/vitable_connect/errors/not_found_error.py deleted file mode 100644 index 3b2a2d1..0000000 --- a/src/vitable_connect/errors/not_found_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class NotFoundError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=404, headers=headers, body=body) diff --git a/src/vitable_connect/errors/too_many_requests_error.py b/src/vitable_connect/errors/too_many_requests_error.py deleted file mode 100644 index befc9d6..0000000 --- a/src/vitable_connect/errors/too_many_requests_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class TooManyRequestsError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=429, headers=headers, body=body) diff --git a/src/vitable_connect/errors/unauthorized_error.py b/src/vitable_connect/errors/unauthorized_error.py deleted file mode 100644 index 00a614a..0000000 --- a/src/vitable_connect/errors/unauthorized_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class UnauthorizedError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=401, headers=headers, body=body) diff --git a/src/vitable_connect/errors/unprocessable_entity_error.py b/src/vitable_connect/errors/unprocessable_entity_error.py deleted file mode 100644 index 070bc43..0000000 --- a/src/vitable_connect/errors/unprocessable_entity_error.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.api_error import ApiError -from ..types.error_response import ErrorResponse - - -class UnprocessableEntityError(ApiError): - def __init__(self, body: ErrorResponse, headers: typing.Optional[typing.Dict[str, str]] = None): - super().__init__(status_code=422, headers=headers, body=body) diff --git a/src/vitable_connect/groups/__init__.py b/src/vitable_connect/groups/__init__.py deleted file mode 100644 index 6184be4..0000000 --- a/src/vitable_connect/groups/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from . import members -_dynamic_imports: typing.Dict[str, str] = {"members": ".members"} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e - except AttributeError as e: - raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["members"] diff --git a/src/vitable_connect/groups/client.py b/src/vitable_connect/groups/client.py deleted file mode 100644 index 10bb8ca..0000000 --- a/src/vitable_connect/groups/client.py +++ /dev/null @@ -1,432 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.pagination import AsyncPager, SyncPager -from ..core.request_options import RequestOptions -from ..types.group import Group -from ..types.group_id import GroupId -from ..types.group_list_response import GroupListResponse -from ..types.group_response import GroupResponse -from ..types.limit import Limit -from ..types.page import Page -from .raw_client import AsyncRawGroupsClient, RawGroupsClient - -if typing.TYPE_CHECKING: - from .members.client import AsyncMembersClient, MembersClient -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class GroupsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawGroupsClient(client_wrapper=client_wrapper) - self._client_wrapper = client_wrapper - self._members: typing.Optional[MembersClient] = None - - @property - def with_raw_response(self) -> RawGroupsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawGroupsClient - """ - return self._raw_client - - def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[Group, GroupListResponse]: - """ - Returns a paginated list of groups belonging to the authenticated organization. - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[Group, GroupListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.groups.list( - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list(limit=limit, page=page, request_options=request_options) - - def create( - self, *, name: str, external_reference_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> GroupResponse: - """ - Creates a new group scoped to the authenticated organization. - - Parameters - ---------- - name : str - Display name for the group. - - external_reference_id : str - Your own identifier for this group. Use it to correlate the group with a record in your system; it must be unique within your organization. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.groups.create( - name="Tier 1", - external_reference_id="mol_seg_001", - ) - """ - _response = self._raw_client.create( - name=name, external_reference_id=external_reference_id, request_options=request_options - ) - return _response.data - - def get(self, group_id: GroupId, *, request_options: typing.Optional[RequestOptions] = None) -> GroupResponse: - """ - Retrieves a single group by its prefixed ID. Returns 404 if the group does not belong to the authenticated organization. - - Parameters - ---------- - group_id : GroupId - Unique group identifier (grp_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.groups.get( - group_id="grp_abc123def456", - ) - """ - _response = self._raw_client.get(group_id, request_options=request_options) - return _response.data - - def update( - self, - group_id: GroupId, - *, - name: typing.Optional[str] = OMIT, - external_reference_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> GroupResponse: - """ - Partially updates a group's name or external reference ID. Returns 404 if the group does not belong to the authenticated organization. - - Parameters - ---------- - group_id : GroupId - Unique group identifier (grp_*) - - name : typing.Optional[str] - New display name for the group. Omit to leave unchanged. - - external_reference_id : typing.Optional[str] - New external reference ID for the group. Omit to leave unchanged. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.groups.update( - group_id="grp_abc123def456", - name="Tier 1 (renamed)", - external_reference_id="mol_seg_001_v2", - ) - """ - _response = self._raw_client.update( - group_id, name=name, external_reference_id=external_reference_id, request_options=request_options - ) - return _response.data - - @property - def members(self): - if self._members is None: - from .members.client import MembersClient # noqa: E402 - - self._members = MembersClient(client_wrapper=self._client_wrapper) - return self._members - - -class AsyncGroupsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawGroupsClient(client_wrapper=client_wrapper) - self._client_wrapper = client_wrapper - self._members: typing.Optional[AsyncMembersClient] = None - - @property - def with_raw_response(self) -> AsyncRawGroupsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawGroupsClient - """ - return self._raw_client - - async def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[Group, GroupListResponse]: - """ - Returns a paginated list of groups belonging to the authenticated organization. - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[Group, GroupListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.groups.list( - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list(limit=limit, page=page, request_options=request_options) - - async def create( - self, *, name: str, external_reference_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> GroupResponse: - """ - Creates a new group scoped to the authenticated organization. - - Parameters - ---------- - name : str - Display name for the group. - - external_reference_id : str - Your own identifier for this group. Use it to correlate the group with a record in your system; it must be unique within your organization. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.groups.create( - name="Tier 1", - external_reference_id="mol_seg_001", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.create( - name=name, external_reference_id=external_reference_id, request_options=request_options - ) - return _response.data - - async def get(self, group_id: GroupId, *, request_options: typing.Optional[RequestOptions] = None) -> GroupResponse: - """ - Retrieves a single group by its prefixed ID. Returns 404 if the group does not belong to the authenticated organization. - - Parameters - ---------- - group_id : GroupId - Unique group identifier (grp_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.groups.get( - group_id="grp_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get(group_id, request_options=request_options) - return _response.data - - async def update( - self, - group_id: GroupId, - *, - name: typing.Optional[str] = OMIT, - external_reference_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> GroupResponse: - """ - Partially updates a group's name or external reference ID. Returns 404 if the group does not belong to the authenticated organization. - - Parameters - ---------- - group_id : GroupId - Unique group identifier (grp_*) - - name : typing.Optional[str] - New display name for the group. Omit to leave unchanged. - - external_reference_id : typing.Optional[str] - New external reference ID for the group. Omit to leave unchanged. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.groups.update( - group_id="grp_abc123def456", - name="Tier 1 (renamed)", - external_reference_id="mol_seg_001_v2", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.update( - group_id, name=name, external_reference_id=external_reference_id, request_options=request_options - ) - return _response.data - - @property - def members(self): - if self._members is None: - from .members.client import AsyncMembersClient # noqa: E402 - - self._members = AsyncMembersClient(client_wrapper=self._client_wrapper) - return self._members diff --git a/src/vitable_connect/groups/members/__init__.py b/src/vitable_connect/groups/members/__init__.py deleted file mode 100644 index 194ddc8..0000000 --- a/src/vitable_connect/groups/members/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from . import sync -_dynamic_imports: typing.Dict[str, str] = {"sync": ".sync"} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e - except AttributeError as e: - raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = ["sync"] diff --git a/src/vitable_connect/groups/members/client.py b/src/vitable_connect/groups/members/client.py deleted file mode 100644 index b600730..0000000 --- a/src/vitable_connect/groups/members/client.py +++ /dev/null @@ -1,63 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import typing - -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from .raw_client import AsyncRawMembersClient, RawMembersClient - -if typing.TYPE_CHECKING: - from .sync.client import AsyncSyncClient, SyncClient - - -class MembersClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawMembersClient(client_wrapper=client_wrapper) - self._client_wrapper = client_wrapper - self._sync: typing.Optional[SyncClient] = None - - @property - def with_raw_response(self) -> RawMembersClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawMembersClient - """ - return self._raw_client - - @property - def sync(self): - if self._sync is None: - from .sync.client import SyncClient # noqa: E402 - - self._sync = SyncClient(client_wrapper=self._client_wrapper) - return self._sync - - -class AsyncMembersClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawMembersClient(client_wrapper=client_wrapper) - self._client_wrapper = client_wrapper - self._sync: typing.Optional[AsyncSyncClient] = None - - @property - def with_raw_response(self) -> AsyncRawMembersClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawMembersClient - """ - return self._raw_client - - @property - def sync(self): - if self._sync is None: - from .sync.client import AsyncSyncClient # noqa: E402 - - self._sync = AsyncSyncClient(client_wrapper=self._client_wrapper) - return self._sync diff --git a/src/vitable_connect/groups/members/raw_client.py b/src/vitable_connect/groups/members/raw_client.py deleted file mode 100644 index cd9dd79..0000000 --- a/src/vitable_connect/groups/members/raw_client.py +++ /dev/null @@ -1,13 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper - - -class RawMembersClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - -class AsyncRawMembersClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper diff --git a/src/vitable_connect/groups/members/sync/__init__.py b/src/vitable_connect/groups/members/sync/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/groups/members/sync/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/groups/members/sync/client.py b/src/vitable_connect/groups/members/sync/client.py deleted file mode 100644 index acbbfaa..0000000 --- a/src/vitable_connect/groups/members/sync/client.py +++ /dev/null @@ -1,260 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ....core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ....core.request_options import RequestOptions -from ....types.group_id import GroupId -from ....types.group_member_sync_detail_response import GroupMemberSyncDetailResponse -from ....types.group_member_sync_member_request import GroupMemberSyncMemberRequest -from ....types.group_member_sync_request_detail_response import GroupMemberSyncRequestDetailResponse -from ....types.request_id import RequestId -from .raw_client import AsyncRawSyncClient, RawSyncClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class SyncClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawSyncClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawSyncClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawSyncClient - """ - return self._raw_client - - def submit( - self, - group_id: GroupId, - *, - members: typing.Sequence[GroupMemberSyncMemberRequest], - request_options: typing.Optional[RequestOptions] = None, - ) -> GroupMemberSyncDetailResponse: - """ - Submits a member sync payload for the specified group. Members in the payload will be queued for processing asynchronously. Returns HTTP 202 with the batch ID and acceptance timestamp. - - Parameters - ---------- - group_id : GroupId - - members : typing.Sequence[GroupMemberSyncMemberRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupMemberSyncDetailResponse - - - Examples - -------- - import datetime - - from vitable_connect import ( - AddressRequest, - GroupMemberSyncMemberRequest, - VitableConnect, - ) - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.groups.members.sync.submit( - group_id="grp_abc123def456", - members=[ - GroupMemberSyncMemberRequest( - reference_id="EMP-001", - first_name="Jane", - last_name="Doe", - date_of_birth=datetime.date.fromisoformat( - "1990-05-15", - ), - phone="4155550100", - plan_id="pln_abc123def456", - address=AddressRequest( - address_line1="123 Main Street", - address_line2="Apt 4B", - city="San Francisco", - state="CA", - zipcode="94102", - ), - email="jane.doe@acme.com", - ) - ], - ) - """ - _response = self._raw_client.submit(group_id, members=members, request_options=request_options) - return _response.data - - def get( - self, group_id: GroupId, request_id: RequestId, *, request_options: typing.Optional[RequestOptions] = None - ) -> GroupMemberSyncRequestDetailResponse: - """ - Retrieves a previously-submitted group member sync request by its `grpmsr_` ID. Returns the acceptance timestamp, completion timestamp (if processing has finished), and the per-member `results` once available. While processing is in flight, `completed_at` and `results` are `null`. - - Parameters - ---------- - group_id : GroupId - - request_id : RequestId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupMemberSyncRequestDetailResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.groups.members.sync.get( - group_id="grp_abc123def456", - request_id="request_id", - ) - """ - _response = self._raw_client.get(group_id, request_id, request_options=request_options) - return _response.data - - -class AsyncSyncClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawSyncClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawSyncClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawSyncClient - """ - return self._raw_client - - async def submit( - self, - group_id: GroupId, - *, - members: typing.Sequence[GroupMemberSyncMemberRequest], - request_options: typing.Optional[RequestOptions] = None, - ) -> GroupMemberSyncDetailResponse: - """ - Submits a member sync payload for the specified group. Members in the payload will be queued for processing asynchronously. Returns HTTP 202 with the batch ID and acceptance timestamp. - - Parameters - ---------- - group_id : GroupId - - members : typing.Sequence[GroupMemberSyncMemberRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupMemberSyncDetailResponse - - - Examples - -------- - import asyncio - import datetime - - from vitable_connect import ( - AddressRequest, - AsyncVitableConnect, - GroupMemberSyncMemberRequest, - ) - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.groups.members.sync.submit( - group_id="grp_abc123def456", - members=[ - GroupMemberSyncMemberRequest( - reference_id="EMP-001", - first_name="Jane", - last_name="Doe", - date_of_birth=datetime.date.fromisoformat( - "1990-05-15", - ), - phone="4155550100", - plan_id="pln_abc123def456", - address=AddressRequest( - address_line1="123 Main Street", - address_line2="Apt 4B", - city="San Francisco", - state="CA", - zipcode="94102", - ), - email="jane.doe@acme.com", - ) - ], - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.submit(group_id, members=members, request_options=request_options) - return _response.data - - async def get( - self, group_id: GroupId, request_id: RequestId, *, request_options: typing.Optional[RequestOptions] = None - ) -> GroupMemberSyncRequestDetailResponse: - """ - Retrieves a previously-submitted group member sync request by its `grpmsr_` ID. Returns the acceptance timestamp, completion timestamp (if processing has finished), and the per-member `results` once available. While processing is in flight, `completed_at` and `results` are `null`. - - Parameters - ---------- - group_id : GroupId - - request_id : RequestId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - GroupMemberSyncRequestDetailResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.groups.members.sync.get( - group_id="grp_abc123def456", - request_id="request_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get(group_id, request_id, request_options=request_options) - return _response.data diff --git a/src/vitable_connect/groups/members/sync/raw_client.py b/src/vitable_connect/groups/members/sync/raw_client.py deleted file mode 100644 index 907d504..0000000 --- a/src/vitable_connect/groups/members/sync/raw_client.py +++ /dev/null @@ -1,618 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ....core.api_error import ApiError -from ....core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ....core.http_response import AsyncHttpResponse, HttpResponse -from ....core.jsonable_encoder import encode_path_param -from ....core.parse_error import ParsingError -from ....core.pydantic_utilities import parse_obj_as -from ....core.request_options import RequestOptions -from ....core.serialization import convert_and_respect_annotation_metadata -from ....errors.bad_gateway_error import BadGatewayError -from ....errors.bad_request_error import BadRequestError -from ....errors.conflict_error import ConflictError -from ....errors.forbidden_error import ForbiddenError -from ....errors.internal_server_error import InternalServerError -from ....errors.not_found_error import NotFoundError -from ....errors.too_many_requests_error import TooManyRequestsError -from ....errors.unauthorized_error import UnauthorizedError -from ....errors.unprocessable_entity_error import UnprocessableEntityError -from ....types.error_response import ErrorResponse -from ....types.group_id import GroupId -from ....types.group_member_sync_detail_response import GroupMemberSyncDetailResponse -from ....types.group_member_sync_member_request import GroupMemberSyncMemberRequest -from ....types.group_member_sync_request_detail_response import GroupMemberSyncRequestDetailResponse -from ....types.request_id import RequestId -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawSyncClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def submit( - self, - group_id: GroupId, - *, - members: typing.Sequence[GroupMemberSyncMemberRequest], - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[GroupMemberSyncDetailResponse]: - """ - Submits a member sync payload for the specified group. Members in the payload will be queued for processing asynchronously. Returns HTTP 202 with the batch ID and acceptance timestamp. - - Parameters - ---------- - group_id : GroupId - - members : typing.Sequence[GroupMemberSyncMemberRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[GroupMemberSyncDetailResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/groups/{encode_path_param(group_id)}/members/sync", - method="POST", - json={ - "members": convert_and_respect_annotation_metadata( - object_=members, annotation=typing.Sequence[GroupMemberSyncMemberRequest], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupMemberSyncDetailResponse, - parse_obj_as( - type_=GroupMemberSyncDetailResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get( - self, group_id: GroupId, request_id: RequestId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[GroupMemberSyncRequestDetailResponse]: - """ - Retrieves a previously-submitted group member sync request by its `grpmsr_` ID. Returns the acceptance timestamp, completion timestamp (if processing has finished), and the per-member `results` once available. While processing is in flight, `completed_at` and `results` are `null`. - - Parameters - ---------- - group_id : GroupId - - request_id : RequestId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[GroupMemberSyncRequestDetailResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/groups/{encode_path_param(group_id)}/members/sync/{encode_path_param(request_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupMemberSyncRequestDetailResponse, - parse_obj_as( - type_=GroupMemberSyncRequestDetailResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawSyncClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def submit( - self, - group_id: GroupId, - *, - members: typing.Sequence[GroupMemberSyncMemberRequest], - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[GroupMemberSyncDetailResponse]: - """ - Submits a member sync payload for the specified group. Members in the payload will be queued for processing asynchronously. Returns HTTP 202 with the batch ID and acceptance timestamp. - - Parameters - ---------- - group_id : GroupId - - members : typing.Sequence[GroupMemberSyncMemberRequest] - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[GroupMemberSyncDetailResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/groups/{encode_path_param(group_id)}/members/sync", - method="POST", - json={ - "members": convert_and_respect_annotation_metadata( - object_=members, annotation=typing.Sequence[GroupMemberSyncMemberRequest], direction="write" - ), - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupMemberSyncDetailResponse, - parse_obj_as( - type_=GroupMemberSyncDetailResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get( - self, group_id: GroupId, request_id: RequestId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[GroupMemberSyncRequestDetailResponse]: - """ - Retrieves a previously-submitted group member sync request by its `grpmsr_` ID. Returns the acceptance timestamp, completion timestamp (if processing has finished), and the per-member `results` once available. While processing is in flight, `completed_at` and `results` are `null`. - - Parameters - ---------- - group_id : GroupId - - request_id : RequestId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[GroupMemberSyncRequestDetailResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/groups/{encode_path_param(group_id)}/members/sync/{encode_path_param(request_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupMemberSyncRequestDetailResponse, - parse_obj_as( - type_=GroupMemberSyncRequestDetailResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/groups/raw_client.py b/src/vitable_connect/groups/raw_client.py deleted file mode 100644 index 68cb509..0000000 --- a/src/vitable_connect/groups/raw_client.py +++ /dev/null @@ -1,1238 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.pagination import AsyncPager, SyncPager -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..errors.unprocessable_entity_error import UnprocessableEntityError -from ..types.error_response import ErrorResponse -from ..types.group import Group -from ..types.group_id import GroupId -from ..types.group_list_response import GroupListResponse -from ..types.group_response import GroupResponse -from ..types.limit import Limit -from ..types.page import Page -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawGroupsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[Group, GroupListResponse]: - """ - Returns a paginated list of groups belonging to the authenticated organization. - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[Group, GroupListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - "v1/groups", - method="GET", - params={ - "limit": limit, - "page": page, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - GroupListResponse, - parse_obj_as( - type_=GroupListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list( - limit=limit, - page=page + 1, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def create( - self, *, name: str, external_reference_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[GroupResponse]: - """ - Creates a new group scoped to the authenticated organization. - - Parameters - ---------- - name : str - Display name for the group. - - external_reference_id : str - Your own identifier for this group. Use it to correlate the group with a record in your system; it must be unique within your organization. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[GroupResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - "v1/groups", - method="POST", - json={ - "name": name, - "external_reference_id": external_reference_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupResponse, - parse_obj_as( - type_=GroupResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get( - self, group_id: GroupId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[GroupResponse]: - """ - Retrieves a single group by its prefixed ID. Returns 404 if the group does not belong to the authenticated organization. - - Parameters - ---------- - group_id : GroupId - Unique group identifier (grp_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[GroupResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/groups/{encode_path_param(group_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupResponse, - parse_obj_as( - type_=GroupResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def update( - self, - group_id: GroupId, - *, - name: typing.Optional[str] = OMIT, - external_reference_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[GroupResponse]: - """ - Partially updates a group's name or external reference ID. Returns 404 if the group does not belong to the authenticated organization. - - Parameters - ---------- - group_id : GroupId - Unique group identifier (grp_*) - - name : typing.Optional[str] - New display name for the group. Omit to leave unchanged. - - external_reference_id : typing.Optional[str] - New external reference ID for the group. Omit to leave unchanged. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[GroupResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/groups/{encode_path_param(group_id)}", - method="PATCH", - json={ - "name": name, - "external_reference_id": external_reference_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupResponse, - parse_obj_as( - type_=GroupResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawGroupsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[Group, GroupListResponse]: - """ - Returns a paginated list of groups belonging to the authenticated organization. - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[Group, GroupListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - "v1/groups", - method="GET", - params={ - "limit": limit, - "page": page, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - GroupListResponse, - parse_obj_as( - type_=GroupListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list( - limit=limit, - page=page + 1, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def create( - self, *, name: str, external_reference_id: str, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[GroupResponse]: - """ - Creates a new group scoped to the authenticated organization. - - Parameters - ---------- - name : str - Display name for the group. - - external_reference_id : str - Your own identifier for this group. Use it to correlate the group with a record in your system; it must be unique within your organization. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[GroupResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/groups", - method="POST", - json={ - "name": name, - "external_reference_id": external_reference_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupResponse, - parse_obj_as( - type_=GroupResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get( - self, group_id: GroupId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[GroupResponse]: - """ - Retrieves a single group by its prefixed ID. Returns 404 if the group does not belong to the authenticated organization. - - Parameters - ---------- - group_id : GroupId - Unique group identifier (grp_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[GroupResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/groups/{encode_path_param(group_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupResponse, - parse_obj_as( - type_=GroupResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def update( - self, - group_id: GroupId, - *, - name: typing.Optional[str] = OMIT, - external_reference_id: typing.Optional[str] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[GroupResponse]: - """ - Partially updates a group's name or external reference ID. Returns 404 if the group does not belong to the authenticated organization. - - Parameters - ---------- - group_id : GroupId - Unique group identifier (grp_*) - - name : typing.Optional[str] - New display name for the group. Omit to leave unchanged. - - external_reference_id : typing.Optional[str] - New external reference ID for the group. Omit to leave unchanged. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[GroupResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/groups/{encode_path_param(group_id)}", - method="PATCH", - json={ - "name": name, - "external_reference_id": external_reference_id, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - GroupResponse, - parse_obj_as( - type_=GroupResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 422: - raise UnprocessableEntityError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/lib/.keep b/src/vitable_connect/lib/.keep new file mode 100644 index 0000000..5e2c99f --- /dev/null +++ b/src/vitable_connect/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/vitable_connect/members/__init__.py b/src/vitable_connect/members/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/members/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/members/client.py b/src/vitable_connect/members/client.py deleted file mode 100644 index df27f19..0000000 --- a/src/vitable_connect/members/client.py +++ /dev/null @@ -1,715 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.pagination import AsyncPager, SyncPager -from ..core.request_options import RequestOptions -from ..types.household_members_response import HouseholdMembersResponse -from ..types.limit import Limit -from ..types.member_dependents_response import MemberDependentsResponse -from ..types.member_digital_benefit_cards_response import MemberDigitalBenefitCardsResponse -from ..types.member_employments_response import MemberEmploymentsResponse -from ..types.member_enrollments_response import MemberEnrollmentsResponse -from ..types.member_id import MemberId -from ..types.member_list_item import MemberListItem -from ..types.member_list_response import MemberListResponse -from ..types.member_qualifying_life_event import MemberQualifyingLifeEvent -from ..types.member_qualifying_life_event_list_response import MemberQualifyingLifeEventListResponse -from ..types.member_response import MemberResponse -from ..types.member_search import MemberSearch -from ..types.page import Page -from ..types.status import Status -from .raw_client import AsyncRawMembersClient, RawMembersClient - - -class MembersClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawMembersClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawMembersClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawMembersClient - """ - return self._raw_client - - def get(self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None) -> MemberResponse: - """ - Retrieves a member's profile by ID — identity, demographics, address, contact details, tobacco status, and profile status. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - Unique member identifier (mbr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.members.get( - member_id="mbr_abc123def456", - ) - """ - _response = self._raw_client.get(member_id, request_options=request_options) - return _response.data - - def list_dependents( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberDependentsResponse: - """ - Lists a member's active legal dependents — name, relationship, date of birth, age, and sex at birth. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberDependentsResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.members.list_dependents( - member_id="mbr_abc123def456", - ) - """ - _response = self._raw_client.list_dependents(member_id, request_options=request_options) - return _response.data - - def list_employments( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberEmploymentsResponse: - """ - Lists a member's employment across every employer — the same employee record shape as the employer's employees list, plus the employer name. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all employments. A member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberEmploymentsResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.members.list_employments( - member_id="mbr_abc123def456", - ) - """ - _response = self._raw_client.list_employments(member_id, request_options=request_options) - return _response.data - - def list_enrollments( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberEnrollmentsResponse: - """ - Lists a member's benefit enrollments across every employer — benefit type and product, employer, carrier, plan, tier, employee deduction, employer contribution and total premium, the individual enrollment coverage boundary (`coverage_end`), the separate pre-effective cancellation boundary (`cancelled_date`), and the distinct benefit plan-year boundary (`plan_year_coverage_end`) used to determine whether the plan year itself has ended, the date the enrollment record was created (`issued_date`, the value Ops labels Issued on, reported for every row whatever the member answered), the window the member could answer in -- which never opens before the enrollment was issued, so a row issued mid-open-enrollment starts its window on its issue date -- whether a qualifying life event would currently be required for reissue under the product/open-enrollment rule, enrollment/open-enrollment window, and two statuses: `election_status` (what the member answered) and `policy_status` (what became of their coverage, null unless they enrolled). Every row includes a stable enrollment ID and the exact employer and benefit plan-year IDs used to fetch that row's plan-year detail. The full list is returned across all states so the client derives active plans (effective and upcoming) and the enrollment history from those per-row statuses. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all enrollments. A member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberEnrollmentsResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.members.list_enrollments( - member_id="mbr_abc123def456", - ) - """ - _response = self._raw_client.list_enrollments(member_id, request_options=request_options) - return _response.data - - def get_household( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HouseholdMembersResponse: - """ - Lists a member's household as a per-participant table — the account holder plus each active household member, with name, relationship, member type, date of birth, and household-admin flag. Access is scoped to the authenticated principal; a member not visible to the caller (or with no household) returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HouseholdMembersResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.members.get_household( - member_id="mbr_abc123def456", - ) - """ - _response = self._raw_client.get_household(member_id, request_options=request_options) - return _response.data - - def list_id_cards( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberDigitalBenefitCardsResponse: - """ - Lists a member's benefit ID cards — card type (medical, dental, vision, or rx), employer, plan, provider network, claims payer, carrier contact details, and the disclaimers printed on the card. Medical, dental and vision cards come from the member's active digital benefit cards; the rx card from the member's Ventegra pharmacy benefit (omitted when the member has no free-medication coverage), which carries no plan, network, or carrier details. Access is scoped to the authenticated principal, and an organization caller sees only cards from employers in its book; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberDigitalBenefitCardsResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.members.list_id_cards( - member_id="mbr_abc123def456", - ) - """ - _response = self._raw_client.list_id_cards(member_id, request_options=request_options) - return _response.data - - def list_qualifying_life_events( - self, - member_id: MemberId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - status: typing.Optional[Status] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[MemberQualifyingLifeEvent, MemberQualifyingLifeEventListResponse]: - """ - Lists a member's qualifying life events, including events already used for another enrollment. Returns all statuses by default; pass the status query param to filter to one (e.g. approved). Events are ordered newest submission first with stable paging. Custom text is present only when submitted and is otherwise null. A member not visible to the caller returns a 404. API keys and unbound access tokens have organization-wide access. Employer-bound tokens require employment at the bound employer, and employee-bound tokens require the exact employee-member relationship. Organization or scope mismatches return a 404 before pagination is validated. - - Parameters - ---------- - member_id : MemberId - Unique member identifier (mbr_*) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - status : typing.Optional[Status] - Optional. Filter to a single QLE status; omit to return all statuses. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[MemberQualifyingLifeEvent, MemberQualifyingLifeEventListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.members.list_qualifying_life_events( - member_id="mbr_abc123def456", - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list_qualifying_life_events( - member_id, limit=limit, page=page, status=status, request_options=request_options - ) - - def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[MemberSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[MemberListItem, MemberListResponse]: - """ - Retrieves a paginated list of the members in the authenticated organization's book — identity, contact details, and address. The book covers members reached through an employer in the organization's book as well as members of a group it owns. Supports free-text search (name, email, phone number, or exact member id). - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[MemberSearch] - Case-insensitive search across member name, email, and phone number; exact match on member id (prefixed or raw uuid) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[MemberListItem, MemberListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.members.list( - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list(limit=limit, page=page, search=search, request_options=request_options) - - -class AsyncMembersClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawMembersClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawMembersClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawMembersClient - """ - return self._raw_client - - async def get( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberResponse: - """ - Retrieves a member's profile by ID — identity, demographics, address, contact details, tobacco status, and profile status. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - Unique member identifier (mbr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.members.get( - member_id="mbr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get(member_id, request_options=request_options) - return _response.data - - async def list_dependents( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberDependentsResponse: - """ - Lists a member's active legal dependents — name, relationship, date of birth, age, and sex at birth. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberDependentsResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.members.list_dependents( - member_id="mbr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_dependents(member_id, request_options=request_options) - return _response.data - - async def list_employments( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberEmploymentsResponse: - """ - Lists a member's employment across every employer — the same employee record shape as the employer's employees list, plus the employer name. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all employments. A member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberEmploymentsResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.members.list_employments( - member_id="mbr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_employments(member_id, request_options=request_options) - return _response.data - - async def list_enrollments( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberEnrollmentsResponse: - """ - Lists a member's benefit enrollments across every employer — benefit type and product, employer, carrier, plan, tier, employee deduction, employer contribution and total premium, the individual enrollment coverage boundary (`coverage_end`), the separate pre-effective cancellation boundary (`cancelled_date`), and the distinct benefit plan-year boundary (`plan_year_coverage_end`) used to determine whether the plan year itself has ended, the date the enrollment record was created (`issued_date`, the value Ops labels Issued on, reported for every row whatever the member answered), the window the member could answer in -- which never opens before the enrollment was issued, so a row issued mid-open-enrollment starts its window on its issue date -- whether a qualifying life event would currently be required for reissue under the product/open-enrollment rule, enrollment/open-enrollment window, and two statuses: `election_status` (what the member answered) and `policy_status` (what became of their coverage, null unless they enrolled). Every row includes a stable enrollment ID and the exact employer and benefit plan-year IDs used to fetch that row's plan-year detail. The full list is returned across all states so the client derives active plans (effective and upcoming) and the enrollment history from those per-row statuses. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all enrollments. A member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberEnrollmentsResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.members.list_enrollments( - member_id="mbr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_enrollments(member_id, request_options=request_options) - return _response.data - - async def get_household( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HouseholdMembersResponse: - """ - Lists a member's household as a per-participant table — the account holder plus each active household member, with name, relationship, member type, date of birth, and household-admin flag. Access is scoped to the authenticated principal; a member not visible to the caller (or with no household) returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HouseholdMembersResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.members.get_household( - member_id="mbr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get_household(member_id, request_options=request_options) - return _response.data - - async def list_id_cards( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> MemberDigitalBenefitCardsResponse: - """ - Lists a member's benefit ID cards — card type (medical, dental, vision, or rx), employer, plan, provider network, claims payer, carrier contact details, and the disclaimers printed on the card. Medical, dental and vision cards come from the member's active digital benefit cards; the rx card from the member's Ventegra pharmacy benefit (omitted when the member has no free-medication coverage), which carries no plan, network, or carrier details. Access is scoped to the authenticated principal, and an organization caller sees only cards from employers in its book; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - MemberDigitalBenefitCardsResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.members.list_id_cards( - member_id="mbr_abc123def456", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_id_cards(member_id, request_options=request_options) - return _response.data - - async def list_qualifying_life_events( - self, - member_id: MemberId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - status: typing.Optional[Status] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[MemberQualifyingLifeEvent, MemberQualifyingLifeEventListResponse]: - """ - Lists a member's qualifying life events, including events already used for another enrollment. Returns all statuses by default; pass the status query param to filter to one (e.g. approved). Events are ordered newest submission first with stable paging. Custom text is present only when submitted and is otherwise null. A member not visible to the caller returns a 404. API keys and unbound access tokens have organization-wide access. Employer-bound tokens require employment at the bound employer, and employee-bound tokens require the exact employee-member relationship. Organization or scope mismatches return a 404 before pagination is validated. - - Parameters - ---------- - member_id : MemberId - Unique member identifier (mbr_*) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - status : typing.Optional[Status] - Optional. Filter to a single QLE status; omit to return all statuses. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[MemberQualifyingLifeEvent, MemberQualifyingLifeEventListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.members.list_qualifying_life_events( - member_id="mbr_abc123def456", - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list_qualifying_life_events( - member_id, limit=limit, page=page, status=status, request_options=request_options - ) - - async def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[MemberSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[MemberListItem, MemberListResponse]: - """ - Retrieves a paginated list of the members in the authenticated organization's book — identity, contact details, and address. The book covers members reached through an employer in the organization's book as well as members of a group it owns. Supports free-text search (name, email, phone number, or exact member id). - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[MemberSearch] - Case-insensitive search across member name, email, and phone number; exact match on member id (prefixed or raw uuid) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[MemberListItem, MemberListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.members.list( - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list(limit=limit, page=page, search=search, request_options=request_options) diff --git a/src/vitable_connect/members/raw_client.py b/src/vitable_connect/members/raw_client.py deleted file mode 100644 index 35d3ab8..0000000 --- a/src/vitable_connect/members/raw_client.py +++ /dev/null @@ -1,2254 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.pagination import AsyncPager, SyncPager -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..types.error_response import ErrorResponse -from ..types.household_members_response import HouseholdMembersResponse -from ..types.limit import Limit -from ..types.member_dependents_response import MemberDependentsResponse -from ..types.member_digital_benefit_cards_response import MemberDigitalBenefitCardsResponse -from ..types.member_employments_response import MemberEmploymentsResponse -from ..types.member_enrollments_response import MemberEnrollmentsResponse -from ..types.member_id import MemberId -from ..types.member_list_item import MemberListItem -from ..types.member_list_response import MemberListResponse -from ..types.member_qualifying_life_event import MemberQualifyingLifeEvent -from ..types.member_qualifying_life_event_list_response import MemberQualifyingLifeEventListResponse -from ..types.member_response import MemberResponse -from ..types.member_search import MemberSearch -from ..types.page import Page -from ..types.status import Status -from pydantic import ValidationError - - -class RawMembersClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def get( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[MemberResponse]: - """ - Retrieves a member's profile by ID — identity, demographics, address, contact details, tobacco status, and profile status. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - Unique member identifier (mbr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[MemberResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberResponse, - parse_obj_as( - type_=MemberResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_dependents( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[MemberDependentsResponse]: - """ - Lists a member's active legal dependents — name, relationship, date of birth, age, and sex at birth. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[MemberDependentsResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/dependents", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberDependentsResponse, - parse_obj_as( - type_=MemberDependentsResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_employments( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[MemberEmploymentsResponse]: - """ - Lists a member's employment across every employer — the same employee record shape as the employer's employees list, plus the employer name. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all employments. A member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[MemberEmploymentsResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/employments", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberEmploymentsResponse, - parse_obj_as( - type_=MemberEmploymentsResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_enrollments( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[MemberEnrollmentsResponse]: - """ - Lists a member's benefit enrollments across every employer — benefit type and product, employer, carrier, plan, tier, employee deduction, employer contribution and total premium, the individual enrollment coverage boundary (`coverage_end`), the separate pre-effective cancellation boundary (`cancelled_date`), and the distinct benefit plan-year boundary (`plan_year_coverage_end`) used to determine whether the plan year itself has ended, the date the enrollment record was created (`issued_date`, the value Ops labels Issued on, reported for every row whatever the member answered), the window the member could answer in -- which never opens before the enrollment was issued, so a row issued mid-open-enrollment starts its window on its issue date -- whether a qualifying life event would currently be required for reissue under the product/open-enrollment rule, enrollment/open-enrollment window, and two statuses: `election_status` (what the member answered) and `policy_status` (what became of their coverage, null unless they enrolled). Every row includes a stable enrollment ID and the exact employer and benefit plan-year IDs used to fetch that row's plan-year detail. The full list is returned across all states so the client derives active plans (effective and upcoming) and the enrollment history from those per-row statuses. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all enrollments. A member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[MemberEnrollmentsResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/enrollments", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberEnrollmentsResponse, - parse_obj_as( - type_=MemberEnrollmentsResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get_household( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[HouseholdMembersResponse]: - """ - Lists a member's household as a per-participant table — the account holder plus each active household member, with name, relationship, member type, date of birth, and household-admin flag. Access is scoped to the authenticated principal; a member not visible to the caller (or with no household) returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[HouseholdMembersResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/household", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - HouseholdMembersResponse, - parse_obj_as( - type_=HouseholdMembersResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_id_cards( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[MemberDigitalBenefitCardsResponse]: - """ - Lists a member's benefit ID cards — card type (medical, dental, vision, or rx), employer, plan, provider network, claims payer, carrier contact details, and the disclaimers printed on the card. Medical, dental and vision cards come from the member's active digital benefit cards; the rx card from the member's Ventegra pharmacy benefit (omitted when the member has no free-medication coverage), which carries no plan, network, or carrier details. Access is scoped to the authenticated principal, and an organization caller sees only cards from employers in its book; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[MemberDigitalBenefitCardsResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/id-cards", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberDigitalBenefitCardsResponse, - parse_obj_as( - type_=MemberDigitalBenefitCardsResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_qualifying_life_events( - self, - member_id: MemberId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - status: typing.Optional[Status] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[MemberQualifyingLifeEvent, MemberQualifyingLifeEventListResponse]: - """ - Lists a member's qualifying life events, including events already used for another enrollment. Returns all statuses by default; pass the status query param to filter to one (e.g. approved). Events are ordered newest submission first with stable paging. Custom text is present only when submitted and is otherwise null. A member not visible to the caller returns a 404. API keys and unbound access tokens have organization-wide access. Employer-bound tokens require employment at the bound employer, and employee-bound tokens require the exact employee-member relationship. Organization or scope mismatches return a 404 before pagination is validated. - - Parameters - ---------- - member_id : MemberId - Unique member identifier (mbr_*) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - status : typing.Optional[Status] - Optional. Filter to a single QLE status; omit to return all statuses. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[MemberQualifyingLifeEvent, MemberQualifyingLifeEventListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/qualifying-life-events", - method="GET", - params={ - "limit": limit, - "page": page, - "status": status, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - MemberQualifyingLifeEventListResponse, - parse_obj_as( - type_=MemberQualifyingLifeEventListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list_qualifying_life_events( - member_id, - limit=limit, - page=page + 1, - status=status, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[MemberSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[MemberListItem, MemberListResponse]: - """ - Retrieves a paginated list of the members in the authenticated organization's book — identity, contact details, and address. The book covers members reached through an employer in the organization's book as well as members of a group it owns. Supports free-text search (name, email, phone number, or exact member id). - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[MemberSearch] - Case-insensitive search across member name, email, and phone number; exact match on member id (prefixed or raw uuid) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[MemberListItem, MemberListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - "v2/members", - method="GET", - params={ - "limit": limit, - "page": page, - "search": search, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - MemberListResponse, - parse_obj_as( - type_=MemberListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list( - limit=limit, - page=page + 1, - search=search, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawMembersClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def get( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[MemberResponse]: - """ - Retrieves a member's profile by ID — identity, demographics, address, contact details, tobacco status, and profile status. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - Unique member identifier (mbr_*) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[MemberResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberResponse, - parse_obj_as( - type_=MemberResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_dependents( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[MemberDependentsResponse]: - """ - Lists a member's active legal dependents — name, relationship, date of birth, age, and sex at birth. Access is scoped to the authenticated principal; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[MemberDependentsResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/dependents", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberDependentsResponse, - parse_obj_as( - type_=MemberDependentsResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_employments( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[MemberEmploymentsResponse]: - """ - Lists a member's employment across every employer — the same employee record shape as the employer's employees list, plus the employer name. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all employments. A member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[MemberEmploymentsResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/employments", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberEmploymentsResponse, - parse_obj_as( - type_=MemberEmploymentsResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_enrollments( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[MemberEnrollmentsResponse]: - """ - Lists a member's benefit enrollments across every employer — benefit type and product, employer, carrier, plan, tier, employee deduction, employer contribution and total premium, the individual enrollment coverage boundary (`coverage_end`), the separate pre-effective cancellation boundary (`cancelled_date`), and the distinct benefit plan-year boundary (`plan_year_coverage_end`) used to determine whether the plan year itself has ended, the date the enrollment record was created (`issued_date`, the value Ops labels Issued on, reported for every row whatever the member answered), the window the member could answer in -- which never opens before the enrollment was issued, so a row issued mid-open-enrollment starts its window on its issue date -- whether a qualifying life event would currently be required for reissue under the product/open-enrollment rule, enrollment/open-enrollment window, and two statuses: `election_status` (what the member answered) and `policy_status` (what became of their coverage, null unless they enrolled). Every row includes a stable enrollment ID and the exact employer and benefit plan-year IDs used to fetch that row's plan-year detail. The full list is returned across all states so the client derives active plans (effective and upcoming) and the enrollment history from those per-row statuses. For an organization caller the rows are scoped to companies in that organization's book; a member (self/household) or Vitable Admin sees all enrollments. A member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[MemberEnrollmentsResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/enrollments", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberEnrollmentsResponse, - parse_obj_as( - type_=MemberEnrollmentsResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get_household( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[HouseholdMembersResponse]: - """ - Lists a member's household as a per-participant table — the account holder plus each active household member, with name, relationship, member type, date of birth, and household-admin flag. Access is scoped to the authenticated principal; a member not visible to the caller (or with no household) returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[HouseholdMembersResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/household", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - HouseholdMembersResponse, - parse_obj_as( - type_=HouseholdMembersResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_id_cards( - self, member_id: MemberId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[MemberDigitalBenefitCardsResponse]: - """ - Lists a member's benefit ID cards — card type (medical, dental, vision, or rx), employer, plan, provider network, claims payer, carrier contact details, and the disclaimers printed on the card. Medical, dental and vision cards come from the member's active digital benefit cards; the rx card from the member's Ventegra pharmacy benefit (omitted when the member has no free-medication coverage), which carries no plan, network, or carrier details. Access is scoped to the authenticated principal, and an organization caller sees only cards from employers in its book; a member not visible to the caller returns a 404. - - Parameters - ---------- - member_id : MemberId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[MemberDigitalBenefitCardsResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/id-cards", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - MemberDigitalBenefitCardsResponse, - parse_obj_as( - type_=MemberDigitalBenefitCardsResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_qualifying_life_events( - self, - member_id: MemberId, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - status: typing.Optional[Status] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[MemberQualifyingLifeEvent, MemberQualifyingLifeEventListResponse]: - """ - Lists a member's qualifying life events, including events already used for another enrollment. Returns all statuses by default; pass the status query param to filter to one (e.g. approved). Events are ordered newest submission first with stable paging. Custom text is present only when submitted and is otherwise null. A member not visible to the caller returns a 404. API keys and unbound access tokens have organization-wide access. Employer-bound tokens require employment at the bound employer, and employee-bound tokens require the exact employee-member relationship. Organization or scope mismatches return a 404 before pagination is validated. - - Parameters - ---------- - member_id : MemberId - Unique member identifier (mbr_*) - - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - status : typing.Optional[Status] - Optional. Filter to a single QLE status; omit to return all statuses. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[MemberQualifyingLifeEvent, MemberQualifyingLifeEventListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - f"v1/members/{encode_path_param(member_id)}/qualifying-life-events", - method="GET", - params={ - "limit": limit, - "page": page, - "status": status, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - MemberQualifyingLifeEventListResponse, - parse_obj_as( - type_=MemberQualifyingLifeEventListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list_qualifying_life_events( - member_id, - limit=limit, - page=page + 1, - status=status, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - search: typing.Optional[MemberSearch] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[MemberListItem, MemberListResponse]: - """ - Retrieves a paginated list of the members in the authenticated organization's book — identity, contact details, and address. The book covers members reached through an employer in the organization's book as well as members of a group it owns. Supports free-text search (name, email, phone number, or exact member id). - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - search : typing.Optional[MemberSearch] - Case-insensitive search across member name, email, and phone number; exact match on member id (prefixed or raw uuid) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[MemberListItem, MemberListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - "v2/members", - method="GET", - params={ - "limit": limit, - "page": page, - "search": search, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - MemberListResponse, - parse_obj_as( - type_=MemberListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list( - limit=limit, - page=page + 1, - search=search, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/organizations/__init__.py b/src/vitable_connect/organizations/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/organizations/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/organizations/client.py b/src/vitable_connect/organizations/client.py deleted file mode 100644 index 6524a32..0000000 --- a/src/vitable_connect/organizations/client.py +++ /dev/null @@ -1,195 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.request_options import RequestOptions -from ..types.create_organization_request_type import CreateOrganizationRequestType -from ..types.organization import Organization -from ..types.organizations_list_response import OrganizationsListResponse -from .raw_client import AsyncRawOrganizationsClient, RawOrganizationsClient - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class OrganizationsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawOrganizationsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawOrganizationsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawOrganizationsClient - """ - return self._raw_client - - def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> OrganizationsListResponse: - """ - Lists the organizations the authenticated caller is an active member of (paginated). Returns an empty list when the caller belongs to no organizations. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - OrganizationsListResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.organizations.list() - """ - _response = self._raw_client.list(request_options=request_options) - return _response.data - - def create( - self, - *, - name: str, - type: typing.Optional[CreateOrganizationRequestType] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Organization: - """ - Onboards the authenticated user's partner Organization: creates the local Organization + the creator's admin membership atomically, then mirrors it to WorkOS (creates the WorkOS org and binds the creator as admin). 409 `organization_already_exists` when the user already has an organization (v0: one organization per user). - - Parameters - ---------- - name : str - Legal or trading name of the organization. - - type : typing.Optional[CreateOrganizationRequestType] - Category of organization being onboarded. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Organization - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.organizations.create( - name="Acme Brokerage", - type="BROKERAGE", - ) - """ - _response = self._raw_client.create(name=name, type=type, request_options=request_options) - return _response.data - - -class AsyncOrganizationsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawOrganizationsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawOrganizationsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawOrganizationsClient - """ - return self._raw_client - - async def list(self, *, request_options: typing.Optional[RequestOptions] = None) -> OrganizationsListResponse: - """ - Lists the organizations the authenticated caller is an active member of (paginated). Returns an empty list when the caller belongs to no organizations. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - OrganizationsListResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.organizations.list() - - - asyncio.run(main()) - """ - _response = await self._raw_client.list(request_options=request_options) - return _response.data - - async def create( - self, - *, - name: str, - type: typing.Optional[CreateOrganizationRequestType] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> Organization: - """ - Onboards the authenticated user's partner Organization: creates the local Organization + the creator's admin membership atomically, then mirrors it to WorkOS (creates the WorkOS org and binds the creator as admin). 409 `organization_already_exists` when the user already has an organization (v0: one organization per user). - - Parameters - ---------- - name : str - Legal or trading name of the organization. - - type : typing.Optional[CreateOrganizationRequestType] - Category of organization being onboarded. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - Organization - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.organizations.create( - name="Acme Brokerage", - type="BROKERAGE", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.create(name=name, type=type, request_options=request_options) - return _response.data diff --git a/src/vitable_connect/organizations/raw_client.py b/src/vitable_connect/organizations/raw_client.py deleted file mode 100644 index 51ecb85..0000000 --- a/src/vitable_connect/organizations/raw_client.py +++ /dev/null @@ -1,585 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..types.create_organization_request_type import CreateOrganizationRequestType -from ..types.error_response import ErrorResponse -from ..types.organization import Organization -from ..types.organizations_list_response import OrganizationsListResponse -from pydantic import ValidationError - -# this is used as the default value for optional parameters -OMIT = typing.cast(typing.Any, ...) - - -class RawOrganizationsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[OrganizationsListResponse]: - """ - Lists the organizations the authenticated caller is an active member of (paginated). Returns an empty list when the caller belongs to no organizations. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[OrganizationsListResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - "v1/organizations", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - OrganizationsListResponse, - parse_obj_as( - type_=OrganizationsListResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def create( - self, - *, - name: str, - type: typing.Optional[CreateOrganizationRequestType] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> HttpResponse[Organization]: - """ - Onboards the authenticated user's partner Organization: creates the local Organization + the creator's admin membership atomically, then mirrors it to WorkOS (creates the WorkOS org and binds the creator as admin). 409 `organization_already_exists` when the user already has an organization (v0: one organization per user). - - Parameters - ---------- - name : str - Legal or trading name of the organization. - - type : typing.Optional[CreateOrganizationRequestType] - Category of organization being onboarded. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[Organization] - - """ - _response = self._client_wrapper.httpx_client.request( - "v1/organizations", - method="POST", - json={ - "name": name, - "type": type, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - Organization, - parse_obj_as( - type_=Organization, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawOrganizationsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list( - self, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[OrganizationsListResponse]: - """ - Lists the organizations the authenticated caller is an active member of (paginated). Returns an empty list when the caller belongs to no organizations. - - Parameters - ---------- - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[OrganizationsListResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/organizations", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - OrganizationsListResponse, - parse_obj_as( - type_=OrganizationsListResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def create( - self, - *, - name: str, - type: typing.Optional[CreateOrganizationRequestType] = OMIT, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncHttpResponse[Organization]: - """ - Onboards the authenticated user's partner Organization: creates the local Organization + the creator's admin membership atomically, then mirrors it to WorkOS (creates the WorkOS org and binds the creator as admin). 409 `organization_already_exists` when the user already has an organization (v0: one organization per user). - - Parameters - ---------- - name : str - Legal or trading name of the organization. - - type : typing.Optional[CreateOrganizationRequestType] - Category of organization being onboarded. - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[Organization] - - """ - _response = await self._client_wrapper.httpx_client.request( - "v1/organizations", - method="POST", - json={ - "name": name, - "type": type, - }, - headers={ - "content-type": "application/json", - }, - request_options=request_options, - omit=OMIT, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - Organization, - parse_obj_as( - type_=Organization, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/pagination.py b/src/vitable_connect/pagination.py new file mode 100644 index 0000000..8f5cbce --- /dev/null +++ b/src/vitable_connect/pagination.py @@ -0,0 +1,77 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Generic, TypeVar, Optional +from typing_extensions import override + +from ._models import BaseModel +from ._base_client import BasePage, PageInfo, BaseSyncPage, BaseAsyncPage + +__all__ = ["PageNumberPagePagination", "SyncPageNumberPage", "AsyncPageNumberPage"] + +_T = TypeVar("_T") + + +class PageNumberPagePagination(BaseModel): + page: Optional[int] = None + + total_pages: Optional[int] = None + + +class SyncPageNumberPage(BaseSyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + pagination: Optional[PageNumberPagePagination] = None + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> Optional[PageInfo]: + current_page = None + if self.pagination is not None: + if self.pagination.page is not None: + current_page = self.pagination.page + if current_page is None: + current_page = 1 + + total_pages = None + if self.pagination is not None: + if self.pagination.total_pages is not None: + total_pages = self.pagination.total_pages + if total_pages is not None and current_page >= total_pages: + return None + + return PageInfo(params={"page": current_page + 1}) + + +class AsyncPageNumberPage(BaseAsyncPage[_T], BasePage[_T], Generic[_T]): + data: List[_T] + pagination: Optional[PageNumberPagePagination] = None + + @override + def _get_page_items(self) -> List[_T]: + data = self.data + if not data: + return [] + return data + + @override + def next_page_info(self) -> Optional[PageInfo]: + current_page = None + if self.pagination is not None: + if self.pagination.page is not None: + current_page = self.pagination.page + if current_page is None: + current_page = 1 + + total_pages = None + if self.pagination is not None: + if self.pagination.total_pages is not None: + total_pages = self.pagination.total_pages + if total_pages is not None and current_page >= total_pages: + return None + + return PageInfo(params={"page": current_page + 1}) diff --git a/src/vitable_connect/plans/__init__.py b/src/vitable_connect/plans/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/plans/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/plans/client.py b/src/vitable_connect/plans/client.py deleted file mode 100644 index fbe8ebf..0000000 --- a/src/vitable_connect/plans/client.py +++ /dev/null @@ -1,143 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.pagination import AsyncPager, SyncPager -from ..core.request_options import RequestOptions -from ..types.limit import Limit -from ..types.page import Page -from ..types.plan import Plan -from ..types.plan_list_response import PlanListResponse -from .raw_client import AsyncRawPlansClient, RawPlansClient - - -class PlansClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawPlansClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawPlansClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawPlansClient - """ - return self._raw_client - - def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[Plan, PlanListResponse]: - """ - Returns a paginated list of benefit plans linked to the authenticated organization. - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[Plan, PlanListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.plans.list( - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list(limit=limit, page=page, request_options=request_options) - - -class AsyncPlansClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawPlansClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawPlansClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawPlansClient - """ - return self._raw_client - - async def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[Plan, PlanListResponse]: - """ - Returns a paginated list of benefit plans linked to the authenticated organization. - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[Plan, PlanListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.plans.list( - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list(limit=limit, page=page, request_options=request_options) diff --git a/src/vitable_connect/plans/raw_client.py b/src/vitable_connect/plans/raw_client.py deleted file mode 100644 index 115b347..0000000 --- a/src/vitable_connect/plans/raw_client.py +++ /dev/null @@ -1,340 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.pagination import AsyncPager, SyncPager -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..types.error_response import ErrorResponse -from ..types.limit import Limit -from ..types.page import Page -from ..types.plan import Plan -from ..types.plan_list_response import PlanListResponse -from pydantic import ValidationError - - -class RawPlansClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[Plan, PlanListResponse]: - """ - Returns a paginated list of benefit plans linked to the authenticated organization. - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[Plan, PlanListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - "v1/plans", - method="GET", - params={ - "limit": limit, - "page": page, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - PlanListResponse, - parse_obj_as( - type_=PlanListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list( - limit=limit, - page=page + 1, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawPlansClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list( - self, - *, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[Plan, PlanListResponse]: - """ - Returns a paginated list of benefit plans linked to the authenticated organization. - - Parameters - ---------- - limit : typing.Optional[Limit] - Items per page (default: 20, max: 100) - - page : typing.Optional[Page] - Page number (default: 1) - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[Plan, PlanListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - "v1/plans", - method="GET", - params={ - "limit": limit, - "page": page, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - PlanListResponse, - parse_obj_as( - type_=PlanListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list( - limit=limit, - page=page + 1, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect/resources/__init__.py b/src/vitable_connect/resources/__init__.py new file mode 100644 index 0000000..d022108 --- /dev/null +++ b/src/vitable_connect/resources/__init__.py @@ -0,0 +1,131 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .auth import ( + AuthResource, + AsyncAuthResource, + AuthResourceWithRawResponse, + AsyncAuthResourceWithRawResponse, + AuthResourceWithStreamingResponse, + AsyncAuthResourceWithStreamingResponse, +) +from .plans import ( + PlansResource, + AsyncPlansResource, + PlansResourceWithRawResponse, + AsyncPlansResourceWithRawResponse, + PlansResourceWithStreamingResponse, + AsyncPlansResourceWithStreamingResponse, +) +from .groups import ( + GroupsResource, + AsyncGroupsResource, + GroupsResourceWithRawResponse, + AsyncGroupsResourceWithRawResponse, + GroupsResourceWithStreamingResponse, + AsyncGroupsResourceWithStreamingResponse, +) +from .members import ( + MembersResource, + AsyncMembersResource, + MembersResourceWithRawResponse, + AsyncMembersResourceWithRawResponse, + MembersResourceWithStreamingResponse, + AsyncMembersResourceWithStreamingResponse, +) +from .employees import ( + EmployeesResource, + AsyncEmployeesResource, + EmployeesResourceWithRawResponse, + AsyncEmployeesResourceWithRawResponse, + EmployeesResourceWithStreamingResponse, + AsyncEmployeesResourceWithStreamingResponse, +) +from .employers import ( + EmployersResource, + AsyncEmployersResource, + EmployersResourceWithRawResponse, + AsyncEmployersResourceWithRawResponse, + EmployersResourceWithStreamingResponse, + AsyncEmployersResourceWithStreamingResponse, +) +from .enrollments import ( + EnrollmentsResource, + AsyncEnrollmentsResource, + EnrollmentsResourceWithRawResponse, + AsyncEnrollmentsResourceWithRawResponse, + EnrollmentsResourceWithStreamingResponse, + AsyncEnrollmentsResourceWithStreamingResponse, +) +from .organizations import ( + OrganizationsResource, + AsyncOrganizationsResource, + OrganizationsResourceWithRawResponse, + AsyncOrganizationsResourceWithRawResponse, + OrganizationsResourceWithStreamingResponse, + AsyncOrganizationsResourceWithStreamingResponse, +) +from .webhook_events import ( + WebhookEventsResource, + AsyncWebhookEventsResource, + WebhookEventsResourceWithRawResponse, + AsyncWebhookEventsResourceWithRawResponse, + WebhookEventsResourceWithStreamingResponse, + AsyncWebhookEventsResourceWithStreamingResponse, +) + +__all__ = [ + "AuthResource", + "AsyncAuthResource", + "AuthResourceWithRawResponse", + "AsyncAuthResourceWithRawResponse", + "AuthResourceWithStreamingResponse", + "AsyncAuthResourceWithStreamingResponse", + "EmployeesResource", + "AsyncEmployeesResource", + "EmployeesResourceWithRawResponse", + "AsyncEmployeesResourceWithRawResponse", + "EmployeesResourceWithStreamingResponse", + "AsyncEmployeesResourceWithStreamingResponse", + "EmployersResource", + "AsyncEmployersResource", + "EmployersResourceWithRawResponse", + "AsyncEmployersResourceWithRawResponse", + "EmployersResourceWithStreamingResponse", + "AsyncEmployersResourceWithStreamingResponse", + "EnrollmentsResource", + "AsyncEnrollmentsResource", + "EnrollmentsResourceWithRawResponse", + "AsyncEnrollmentsResourceWithRawResponse", + "EnrollmentsResourceWithStreamingResponse", + "AsyncEnrollmentsResourceWithStreamingResponse", + "WebhookEventsResource", + "AsyncWebhookEventsResource", + "WebhookEventsResourceWithRawResponse", + "AsyncWebhookEventsResourceWithRawResponse", + "WebhookEventsResourceWithStreamingResponse", + "AsyncWebhookEventsResourceWithStreamingResponse", + "GroupsResource", + "AsyncGroupsResource", + "GroupsResourceWithRawResponse", + "AsyncGroupsResourceWithRawResponse", + "GroupsResourceWithStreamingResponse", + "AsyncGroupsResourceWithStreamingResponse", + "MembersResource", + "AsyncMembersResource", + "MembersResourceWithRawResponse", + "AsyncMembersResourceWithRawResponse", + "MembersResourceWithStreamingResponse", + "AsyncMembersResourceWithStreamingResponse", + "OrganizationsResource", + "AsyncOrganizationsResource", + "OrganizationsResourceWithRawResponse", + "AsyncOrganizationsResourceWithRawResponse", + "OrganizationsResourceWithStreamingResponse", + "AsyncOrganizationsResourceWithStreamingResponse", + "PlansResource", + "AsyncPlansResource", + "PlansResourceWithRawResponse", + "AsyncPlansResourceWithRawResponse", + "PlansResourceWithStreamingResponse", + "AsyncPlansResourceWithStreamingResponse", +] diff --git a/src/vitable_connect/resources/auth.py b/src/vitable_connect/resources/auth.py new file mode 100644 index 0000000..77344a0 --- /dev/null +++ b/src/vitable_connect/resources/auth.py @@ -0,0 +1,198 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal + +import httpx + +from ..types import auth_issue_access_token_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.auth_issue_access_token_response import AuthIssueAccessTokenResponse + +__all__ = ["AuthResource", "AsyncAuthResource"] + + +class AuthResource(SyncAPIResource): + """Issue short-lived access tokens for scoped API access""" + + @cached_property + def with_raw_response(self) -> AuthResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AuthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AuthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AuthResourceWithStreamingResponse(self) + + def issue_access_token( + self, + *, + grant_type: Literal["client_credentials"], + bound_entity: Optional[auth_issue_access_token_params.BoundEntity] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AuthIssueAccessTokenResponse: + """Issues a short-lived access token from the authenticated API key. + + Access tokens + can optionally be bound to a specific employer or employee for scoped access. + Tokens expire after 15 minutes. + + Args: + grant_type: - `client_credentials` - client_credentials + + bound_entity: Optional entity to bind the token to for scoped access + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/auth/access-tokens", + body=maybe_transform( + { + "grant_type": grant_type, + "bound_entity": bound_entity, + }, + auth_issue_access_token_params.AuthIssueAccessTokenParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthIssueAccessTokenResponse, + ) + + +class AsyncAuthResource(AsyncAPIResource): + """Issue short-lived access tokens for scoped API access""" + + @cached_property + def with_raw_response(self) -> AsyncAuthResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncAuthResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAuthResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncAuthResourceWithStreamingResponse(self) + + async def issue_access_token( + self, + *, + grant_type: Literal["client_credentials"], + bound_entity: Optional[auth_issue_access_token_params.BoundEntity] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AuthIssueAccessTokenResponse: + """Issues a short-lived access token from the authenticated API key. + + Access tokens + can optionally be bound to a specific employer or employee for scoped access. + Tokens expire after 15 minutes. + + Args: + grant_type: - `client_credentials` - client_credentials + + bound_entity: Optional entity to bind the token to for scoped access + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/auth/access-tokens", + body=await async_maybe_transform( + { + "grant_type": grant_type, + "bound_entity": bound_entity, + }, + auth_issue_access_token_params.AuthIssueAccessTokenParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=AuthIssueAccessTokenResponse, + ) + + +class AuthResourceWithRawResponse: + def __init__(self, auth: AuthResource) -> None: + self._auth = auth + + self.issue_access_token = to_raw_response_wrapper( + auth.issue_access_token, + ) + + +class AsyncAuthResourceWithRawResponse: + def __init__(self, auth: AsyncAuthResource) -> None: + self._auth = auth + + self.issue_access_token = async_to_raw_response_wrapper( + auth.issue_access_token, + ) + + +class AuthResourceWithStreamingResponse: + def __init__(self, auth: AuthResource) -> None: + self._auth = auth + + self.issue_access_token = to_streamed_response_wrapper( + auth.issue_access_token, + ) + + +class AsyncAuthResourceWithStreamingResponse: + def __init__(self, auth: AsyncAuthResource) -> None: + self._auth = auth + + self.issue_access_token = async_to_streamed_response_wrapper( + auth.issue_access_token, + ) diff --git a/src/vitable_connect/resources/employees.py b/src/vitable_connect/resources/employees.py new file mode 100644 index 0000000..15c01a4 --- /dev/null +++ b/src/vitable_connect/resources/employees.py @@ -0,0 +1,491 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from datetime import date +from typing_extensions import Literal + +import httpx + +from ..types import EmployeeClass, employee_update_params, employee_list_enrollments_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncPageNumberPage, AsyncPageNumberPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.enrollment import Enrollment +from ..types.employee_class import EmployeeClass +from ..types.employee_update_response import EmployeeUpdateResponse +from ..types.employee_retrieve_response import EmployeeRetrieveResponse + +__all__ = ["EmployeesResource", "AsyncEmployeesResource"] + + +class EmployeesResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> EmployeesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return EmployeesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> EmployeesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return EmployeesResourceWithStreamingResponse(self) + + def retrieve( + self, + employee_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployeeRetrieveResponse: + """Retrieves detailed information for a specific employee by ID. + + Returns employee + details including personal information, employment status, classification and + compensation-type effective dates, compensation type, and payroll deductions + from the most recent statement period. Deductions reflect a snapshot of the + current period and are replaced when a new statement is generated. + + Args: + employee_id: Unique employee identifier (empl\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employee_id: + raise ValueError(f"Expected a non-empty value for `employee_id` but received {employee_id!r}") + return self._get( + path_template("/v1/employees/{employee_id}", employee_id=employee_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployeeRetrieveResponse, + ) + + def update( + self, + employee_id: str, + *, + effective_date: Union[str, date], + address: Optional[employee_update_params.Address] | Omit = omit, + compensation_type: Optional[Literal["Salary", "Hourly"]] | Omit = omit, + email: Optional[str] | Omit = omit, + employee_class: Optional[EmployeeClass] | Omit = omit, + gender: Optional[Literal["Male", "Female", "Transgender", "Non-binary", "Prefer not to respond"]] | Omit = omit, + phone: Optional[str] | Omit = omit, + start_date: Union[str, date, None] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployeeUpdateResponse: + """Updates employee personal, contact, address, and employment fields. + + This + endpoint currently supports email, phone, gender, address, employee_class, + start_date, and compensation_type. effective_date is required and applies to + employee_class and compensation_type when those fields are included in the + request. + + Args: + employee_id: Unique employee identifier (empl\\__\\**) + + effective_date: Past or present date applied to each tracked employment field included in this + request + + address: Employee's residential address + + compensation_type: - `Salary` - Salary + - `Hourly` - Hourly + + email: Email address + + employee_class: - `Full Time` - Full Time + - `Part Time` - Part Time + - `Temporary` - Temporary + - `Intern` - Intern + - `Seasonal` - Seasonal + - `Individual Contractor` - Individual Contractor + + gender: - `Male` - Male + - `Female` - Female + - `Transgender` - Transgender + - `Non-binary` - Non Binary + - `Prefer not to respond` - Prefer Not To Respond + + phone: Phone number + + start_date: Employment start date + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employee_id: + raise ValueError(f"Expected a non-empty value for `employee_id` but received {employee_id!r}") + return self._patch( + path_template("/v1/employees/{employee_id}", employee_id=employee_id), + body=maybe_transform( + { + "effective_date": effective_date, + "address": address, + "compensation_type": compensation_type, + "email": email, + "employee_class": employee_class, + "gender": gender, + "phone": phone, + "start_date": start_date, + }, + employee_update_params.EmployeeUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployeeUpdateResponse, + ) + + def list_enrollments( + self, + employee_id: str, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[Enrollment]: + """ + Retrieves a paginated list of benefit enrollments for an employee. + + Args: + employee_id: Unique employee identifier (empl\\__\\**) + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employee_id: + raise ValueError(f"Expected a non-empty value for `employee_id` but received {employee_id!r}") + return self._get_api_list( + path_template("/v1/employees/{employee_id}/enrollments", employee_id=employee_id), + page=SyncPageNumberPage[Enrollment], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + }, + employee_list_enrollments_params.EmployeeListEnrollmentsParams, + ), + ), + model=Enrollment, + ) + + +class AsyncEmployeesResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncEmployeesResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncEmployeesResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncEmployeesResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncEmployeesResourceWithStreamingResponse(self) + + async def retrieve( + self, + employee_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployeeRetrieveResponse: + """Retrieves detailed information for a specific employee by ID. + + Returns employee + details including personal information, employment status, classification and + compensation-type effective dates, compensation type, and payroll deductions + from the most recent statement period. Deductions reflect a snapshot of the + current period and are replaced when a new statement is generated. + + Args: + employee_id: Unique employee identifier (empl\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employee_id: + raise ValueError(f"Expected a non-empty value for `employee_id` but received {employee_id!r}") + return await self._get( + path_template("/v1/employees/{employee_id}", employee_id=employee_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployeeRetrieveResponse, + ) + + async def update( + self, + employee_id: str, + *, + effective_date: Union[str, date], + address: Optional[employee_update_params.Address] | Omit = omit, + compensation_type: Optional[Literal["Salary", "Hourly"]] | Omit = omit, + email: Optional[str] | Omit = omit, + employee_class: Optional[EmployeeClass] | Omit = omit, + gender: Optional[Literal["Male", "Female", "Transgender", "Non-binary", "Prefer not to respond"]] | Omit = omit, + phone: Optional[str] | Omit = omit, + start_date: Union[str, date, None] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployeeUpdateResponse: + """Updates employee personal, contact, address, and employment fields. + + This + endpoint currently supports email, phone, gender, address, employee_class, + start_date, and compensation_type. effective_date is required and applies to + employee_class and compensation_type when those fields are included in the + request. + + Args: + employee_id: Unique employee identifier (empl\\__\\**) + + effective_date: Past or present date applied to each tracked employment field included in this + request + + address: Employee's residential address + + compensation_type: - `Salary` - Salary + - `Hourly` - Hourly + + email: Email address + + employee_class: - `Full Time` - Full Time + - `Part Time` - Part Time + - `Temporary` - Temporary + - `Intern` - Intern + - `Seasonal` - Seasonal + - `Individual Contractor` - Individual Contractor + + gender: - `Male` - Male + - `Female` - Female + - `Transgender` - Transgender + - `Non-binary` - Non Binary + - `Prefer not to respond` - Prefer Not To Respond + + phone: Phone number + + start_date: Employment start date + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employee_id: + raise ValueError(f"Expected a non-empty value for `employee_id` but received {employee_id!r}") + return await self._patch( + path_template("/v1/employees/{employee_id}", employee_id=employee_id), + body=await async_maybe_transform( + { + "effective_date": effective_date, + "address": address, + "compensation_type": compensation_type, + "email": email, + "employee_class": employee_class, + "gender": gender, + "phone": phone, + "start_date": start_date, + }, + employee_update_params.EmployeeUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployeeUpdateResponse, + ) + + def list_enrollments( + self, + employee_id: str, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Enrollment, AsyncPageNumberPage[Enrollment]]: + """ + Retrieves a paginated list of benefit enrollments for an employee. + + Args: + employee_id: Unique employee identifier (empl\\__\\**) + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employee_id: + raise ValueError(f"Expected a non-empty value for `employee_id` but received {employee_id!r}") + return self._get_api_list( + path_template("/v1/employees/{employee_id}/enrollments", employee_id=employee_id), + page=AsyncPageNumberPage[Enrollment], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + }, + employee_list_enrollments_params.EmployeeListEnrollmentsParams, + ), + ), + model=Enrollment, + ) + + +class EmployeesResourceWithRawResponse: + def __init__(self, employees: EmployeesResource) -> None: + self._employees = employees + + self.retrieve = to_raw_response_wrapper( + employees.retrieve, + ) + self.update = to_raw_response_wrapper( + employees.update, + ) + self.list_enrollments = to_raw_response_wrapper( + employees.list_enrollments, + ) + + +class AsyncEmployeesResourceWithRawResponse: + def __init__(self, employees: AsyncEmployeesResource) -> None: + self._employees = employees + + self.retrieve = async_to_raw_response_wrapper( + employees.retrieve, + ) + self.update = async_to_raw_response_wrapper( + employees.update, + ) + self.list_enrollments = async_to_raw_response_wrapper( + employees.list_enrollments, + ) + + +class EmployeesResourceWithStreamingResponse: + def __init__(self, employees: EmployeesResource) -> None: + self._employees = employees + + self.retrieve = to_streamed_response_wrapper( + employees.retrieve, + ) + self.update = to_streamed_response_wrapper( + employees.update, + ) + self.list_enrollments = to_streamed_response_wrapper( + employees.list_enrollments, + ) + + +class AsyncEmployeesResourceWithStreamingResponse: + def __init__(self, employees: AsyncEmployeesResource) -> None: + self._employees = employees + + self.retrieve = async_to_streamed_response_wrapper( + employees.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + employees.update, + ) + self.list_enrollments = async_to_streamed_response_wrapper( + employees.list_enrollments, + ) diff --git a/src/vitable_connect/resources/employers.py b/src/vitable_connect/resources/employers.py new file mode 100644 index 0000000..03c6efb --- /dev/null +++ b/src/vitable_connect/resources/employers.py @@ -0,0 +1,2245 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Iterable, Optional +from typing_extensions import Literal + +import httpx + +from ..types import ( + employer_list_params, + employer_create_params, + employer_update_params, + employer_list_invoices_params, + employer_list_employees_params, + employer_update_settings_params, + employer_submit_census_sync_params, + employer_submit_payroll_access_setup_params, + employer_list_payroll_deduction_statements_params, + employer_list_benefit_plan_year_enrollments_params, +) +from .._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncPageNumberPage, AsyncPageNumberPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.employee import Employee +from ..types.employer_response import EmployerResponse +from ..types.employer_list_response import EmployerListResponse +from ..types.employer_list_invoices_response import EmployerListInvoicesResponse +from ..types.employer_retrieve_hris_response import EmployerRetrieveHRISResponse +from ..types.employer_update_settings_response import EmployerUpdateSettingsResponse +from ..types.employer_submit_census_sync_response import EmployerSubmitCensusSyncResponse +from ..types.employer_list_hris_providers_response import EmployerListHRISProvidersResponse +from ..types.employer_retrieve_invoice_pdf_response import EmployerRetrieveInvoicePdfResponse +from ..types.employer_list_benefit_plan_years_response import EmployerListBenefitPlanYearsResponse +from ..types.employer_retrieve_benefit_plan_year_response import EmployerRetrieveBenefitPlanYearResponse +from ..types.employer_submit_payroll_access_setup_response import EmployerSubmitPayrollAccessSetupResponse +from ..types.employer_retrieve_payroll_access_setup_response import EmployerRetrievePayrollAccessSetupResponse +from ..types.employer_ensure_payroll_integration_email_response import EmployerEnsurePayrollIntegrationEmailResponse +from ..types.employer_list_payroll_deduction_statements_response import EmployerListPayrollDeductionStatementsResponse +from ..types.employer_list_benefit_plan_year_enrollments_response import EmployerListBenefitPlanYearEnrollmentsResponse + +__all__ = ["EmployersResource", "AsyncEmployersResource"] + + +class EmployersResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> EmployersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return EmployersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> EmployersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return EmployersResourceWithStreamingResponse(self) + + def create( + self, + *, + address: employer_create_params.Address, + ein: str, + email: str, + legal_name: str, + name: str, + phone_number: Optional[str] | Omit = omit, + reference_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerResponse: + """Creates a new employer for the authenticated organization. + + Requires employer + name, legal name, EIN, email, and address information. Returns the created + employer with its assigned ID. + + Args: + address: Employer address + + ein: Employer Identification Number (format: XX-XXXXXXX) + + email: Email address for billing and communications + + legal_name: Legal business name + + name: Employer display name + + phone_number: Employer phone number (10-digit US format, e.g. 5551234567) + + reference_id: External reference ID for this employer + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/employers", + body=maybe_transform( + { + "address": address, + "ein": ein, + "email": email, + "legal_name": legal_name, + "name": name, + "phone_number": phone_number, + "reference_id": reference_id, + }, + employer_create_params.EmployerCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerResponse, + ) + + def retrieve( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerResponse: + """Retrieves detailed information for a specific employer by ID. + + The employer must + belong to the authenticated organization. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get( + path_template("/v1/employers/{employer_id}", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerResponse, + ) + + def update( + self, + employer_id: str, + *, + active: Optional[bool] | Omit = omit, + address: Optional[employer_update_params.Address] | Omit = omit, + legal_name: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerResponse: + """Updates an existing employer. + + All fields are optional — only provided fields are + updated. PO Box addresses are rejected. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + active: Whether the employer is active + + address: Employer address + + legal_name: Legal business name + + name: Employer display name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._put( + path_template("/v1/employers/{employer_id}", employer_id=employer_id), + body=maybe_transform( + { + "active": active, + "address": address, + "legal_name": legal_name, + "name": name, + }, + employer_update_params.EmployerUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerResponse, + ) + + def list( + self, + *, + benefit_family: List[Literal["mec", "mvp", "ichra", "vpc", "dental", "vision"]] | Omit = omit, + benefit_lifecycle_stage: List[Literal["open_enrollment", "renewal", "active", "onboarding", "cancelled"]] + | Omit = omit, + hris_provider: SequenceNotStr[str] | Omit = omit, + hris_status: List[Literal["Pending", "Active", "Inactive", "Paused", "Terminated"]] | Omit = omit, + include_cancelled: bool | Omit = omit, + limit: int | Omit = omit, + page: int | Omit = omit, + search: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[EmployerListResponse]: + """ + Returns the caller's employer book — every employer with its computed columns + (enrollment-rate summary, benefit-family tags, HRIS connection, + benefit-lifecycle stage) merged with the employer's flat CRM fields (legal name, + EIN, contact, address, timestamps). The book is derived from the authenticated + principal: one organization's employers, or every organization's for a caller + whose reach is not a single organization. Supports search by display name, legal + name, or exact EIN, employer id or contact email, benefit-family/lifecycle/HRIS + filters, and page/limit pagination. + + Args: + benefit_family: Filter to employers with at least one active benefit in these families. + + benefit_lifecycle_stage: Filter to employers in one of these computed benefit-lifecycle stages. + + hris_provider: Filter to employers whose HRIS connection is with one of these payroll providers + (e.g. `ADP RUN`). Matched case-insensitively; free text, so read the available + values from the HRIS-providers endpoint rather than assuming a fixed set. + + hris_status: Filter to employers whose HRIS connection is in one of these statuses. + + include_cancelled: Include cancelled employers (hidden by default unless their stage is explicitly + requested). + + limit: Items per page. + + page: Page number. + + search: Employer filter. Matches the display name or the legal name case-insensitively + as a substring, or one of these exactly: the EIN (with or without its dash), the + employer id, or the contact email of one of the employer's non-disabled admins. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/employers", + page=SyncPageNumberPage[EmployerListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "benefit_family": benefit_family, + "benefit_lifecycle_stage": benefit_lifecycle_stage, + "hris_provider": hris_provider, + "hris_status": hris_status, + "include_cancelled": include_cancelled, + "limit": limit, + "page": page, + "search": search, + }, + employer_list_params.EmployerListParams, + ), + ), + model=EmployerListResponse, + ) + + def ensure_payroll_integration_email( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerEnsurePayrollIntegrationEmailResponse: + """ + Provision and return the employer's payroll integration email. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._put( + path_template("/v1/employers/{employer_id}/payroll-integration-email", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerEnsurePayrollIntegrationEmailResponse, + ) + + def list_benefit_plan_year_enrollments( + self, + benefit_plan_year_id: str, + *, + employer_id: str, + election_status: List[Literal["Enrolled", "Expired", "Pending", "Waived"]] | Omit = omit, + limit: int | Omit = omit, + page: int | Omit = omit, + search: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse]: + """ + Returns a paginated list of every member with an enrollment in one of an + employer's plan years, any election status: what they elected, where their + coverage stands, dependent count, carrier, plan, tier, and the plan's total + monthly cost. The caller must be authorized for the employer `empr_<...>`; an + unknown or unauthorized employer, or an unknown plan year `plyr_<...>`, + returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + benefit_plan_year_id: Unique benefit-plan-year identifier (plyr\\__\\**). + + election_status: Filter by election status. Repeat the parameter to match several. + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + search: Case-insensitive search. Matches member name partially, and the `member_id` + exactly — either your own reference id or the prefixed `grpmbr_<...>` id. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + if not benefit_plan_year_id: + raise ValueError( + f"Expected a non-empty value for `benefit_plan_year_id` but received {benefit_plan_year_id!r}" + ) + return self._get_api_list( + path_template( + "/v1/employers/{employer_id}/benefit-plan-years/{benefit_plan_year_id}/enrollments", + employer_id=employer_id, + benefit_plan_year_id=benefit_plan_year_id, + ), + page=SyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "election_status": election_status, + "limit": limit, + "page": page, + "search": search, + }, + employer_list_benefit_plan_year_enrollments_params.EmployerListBenefitPlanYearEnrollmentsParams, + ), + ), + model=EmployerListBenefitPlanYearEnrollmentsResponse, + ) + + def list_benefit_plan_years( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerListBenefitPlanYearsResponse: + """ + Returns the employer's benefit plan years (all years, or one when `year` is + given), each with its benefits, offered states, benefit families, and the + year-level enrollment roll-up. The caller must be authorized for the employer; + an unknown or unauthorized employer returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get( + path_template("/v1/employers/{employer_id}/benefit-plan-years", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerListBenefitPlanYearsResponse, + ) + + def list_employees( + self, + employer_id: str, + *, + employment_status: Literal["active", "terminated"] | Omit = omit, + limit: int | Omit = omit, + page: int | Omit = omit, + search: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[Employee]: + """Retrieves a paginated list of employees for a specific employer. + + The caller must + be authorized for the employer; an unknown or unauthorized employer returns 404. + Results are paginated using page and limit parameters and can be narrowed with a + case-insensitive `search` (first name, last name, or email) and an + `employment_status` filter (active or terminated). Each employee includes + payroll deductions from the most recent statement period. When a new deduction + statement is generated, previous period deductions are replaced. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + employment_status: Filter by employment status (active or terminated) + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + search: Case-insensitive search across employee first name, last name, and email + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get_api_list( + path_template("/v1/employers/{employer_id}/employees", employer_id=employer_id), + page=SyncPageNumberPage[Employee], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "employment_status": employment_status, + "limit": limit, + "page": page, + "search": search, + }, + employer_list_employees_params.EmployerListEmployeesParams, + ), + ), + model=Employee, + ) + + def list_hris_providers( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerListHRISProvidersResponse: + """ + Returns the distinct HRIS/payroll providers across the same book + `GET /v1/employers` returns, sorted for display. Use these as the values for the + employers list's `hris_provider` filter — filter on `provider`, show + `provider_label`. The stored providers are free text, so they cannot be + enumerated in advance. + """ + return self._get( + "/v1/employers/hris-providers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerListHRISProvidersResponse, + ) + + def list_invoices( + self, + employer_id: str, + *, + limit: int | Omit = omit, + offset: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerListInvoicesResponse: + """Returns a cursor-paginated page of the employer's billing invoices, newest + first. + + Pass the `next_offset` from a previous page as `offset` to fetch the next + page. The caller must be authorized for the employer; an unknown or unauthorized + employer returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + limit: Maximum number of invoices per page + + offset: Opaque cursor from a previous page's next_offset + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get( + path_template("/v1/employers/{employer_id}/invoices", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "offset": offset, + }, + employer_list_invoices_params.EmployerListInvoicesParams, + ), + ), + cast_to=EmployerListInvoicesResponse, + ) + + def list_payroll_deduction_statements( + self, + employer_id: str, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse]: + """ + Returns a paginated list of the employer's payroll-deduction statements, newest + period first, each with its period, generation date, distinct employee count, + total deduction, change-file link, and deduction frequency. Statements + superseded by a later correction are excluded. The caller must be authorized for + the employer; an unknown or unauthorized employer returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + limit: Maximum number of statements per page + + page: Page number to retrieve (starts at 1) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get_api_list( + path_template("/v1/employers/{employer_id}/payroll-deduction-statements", employer_id=employer_id), + page=SyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + }, + employer_list_payroll_deduction_statements_params.EmployerListPayrollDeductionStatementsParams, + ), + ), + model=EmployerListPayrollDeductionStatementsResponse, + ) + + def retrieve_benefit_plan_year( + self, + benefit_plan_year_id: str, + *, + employer_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerRetrieveBenefitPlanYearResponse: + """ + Returns one benefit plan year in full — its benefit details plus the per-benefit + enrollment rate and SPD link — addressed by its `benefit_plan_year_id`. The + caller must be authorized for the employer; an unknown or unauthorized plan year + returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + benefit_plan_year_id: Unique benefit-plan-year identifier (plyr\\__\\**). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + if not benefit_plan_year_id: + raise ValueError( + f"Expected a non-empty value for `benefit_plan_year_id` but received {benefit_plan_year_id!r}" + ) + return self._get( + path_template( + "/v1/employers/{employer_id}/benefit-plan-years/{benefit_plan_year_id}", + employer_id=employer_id, + benefit_plan_year_id=benefit_plan_year_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerRetrieveBenefitPlanYearResponse, + ) + + def retrieve_hris( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerRetrieveHRISResponse: + """ + Returns the employer's HRIS connection — provider, status, last sync, and synced + row count — or null when the employer has no integration. The caller must be + authorized for the employer; an unknown or unauthorized employer returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get( + path_template("/v1/employers/{employer_id}/hris", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerRetrieveHRISResponse, + ) + + def retrieve_invoice_pdf( + self, + invoice_id: str, + *, + employer_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerRetrieveInvoicePdfResponse: + """ + Returns the time-limited PDF download link for a single invoice belonging to the + employer's billing customer. `invoice_id` is the external Chargebee id (not a + prefixed UUID). The caller must be authorized for the employer; an unknown or + unauthorized employer or invoice returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + invoice_id: External Chargebee invoice id (not a prefixed UUID). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + if not invoice_id: + raise ValueError(f"Expected a non-empty value for `invoice_id` but received {invoice_id!r}") + return self._get( + path_template( + "/v1/employers/{employer_id}/invoices/{invoice_id}/pdf", employer_id=employer_id, invoice_id=invoice_id + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerRetrieveInvoicePdfResponse, + ) + + def retrieve_payroll_access_setup( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerRetrievePayrollAccessSetupResponse: + """ + Return whether the employer has submitted payroll access setup. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get( + path_template("/v1/employers/{employer_id}/payroll-access-setup", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerRetrievePayrollAccessSetupResponse, + ) + + def submit_census_sync( + self, + employer_id: str, + *, + employees: Iterable[employer_submit_census_sync_params.Employee], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerSubmitCensusSyncResponse: + """Submits a census sync payload for the specified employer. + + The employees in the + payload will be queued for processing. Returns an accepted response with the + timestamp of acceptance. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._post( + path_template("/v1/employers/{employer_id}/census-sync", employer_id=employer_id), + body=maybe_transform( + {"employees": employees}, employer_submit_census_sync_params.EmployerSubmitCensusSyncParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerSubmitCensusSyncResponse, + ) + + def submit_payroll_access_setup( + self, + employer_id: str, + *, + access_method: Literal["SELF_SETUP", "NEEDS_HELP"], + all_benefit_eligible_employees_present: bool, + classifications_accurate: bool, + employees_in_payroll_acknowledged: bool, + has_additional_payroll_system: bool, + is_controlled_group: bool, + payroll_data_impacts_eligibility_acknowledged: bool, + additional_access_method: Optional[Literal["SELF_SETUP", "NEEDS_HELP"]] | Omit = omit, + additional_integration_confirmed: Optional[bool] | Omit = omit, + additional_login_url: Optional[str] | Omit = omit, + additional_password: Optional[str] | Omit = omit, + additional_phone: Optional[str] | Omit = omit, + additional_username: Optional[str] | Omit = omit, + classification_correction_source: Optional[Literal["ENTER_NAMES", "EMAIL_LIST"]] | Omit = omit, + integration_confirmed: Optional[bool] | Omit = omit, + login_url: Optional[str] | Omit = omit, + misclassified_employee_names: SequenceNotStr[str] | Omit = omit, + missing_employee_resolution: Optional[Literal["EMAIL_CENSUS", "SECOND_SYSTEM_ACCESS"]] | Omit = omit, + password: Optional[str] | Omit = omit, + phone: Optional[str] | Omit = omit, + remaining_employee_action: Optional[Literal["VITABLE_UPDATE", "EMPLOYER_UPDATE"]] | Omit = omit, + same_payroll_covers_other_eins: Optional[bool] | Omit = omit, + username: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerSubmitPayrollAccessSetupResponse: + """ + Submit the employer's payroll access setup answers. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + access_method: - `SELF_SETUP` - SELF_SETUP + - `NEEDS_HELP` - NEEDS_HELP + + additional_access_method: - `SELF_SETUP` - SELF_SETUP + - `NEEDS_HELP` - NEEDS_HELP + + classification_correction_source: - `ENTER_NAMES` - ENTER_NAMES + - `EMAIL_LIST` - EMAIL_LIST + + missing_employee_resolution: - `EMAIL_CENSUS` - EMAIL_CENSUS + - `SECOND_SYSTEM_ACCESS` - SECOND_SYSTEM_ACCESS + + remaining_employee_action: - `VITABLE_UPDATE` - VITABLE_UPDATE + - `EMPLOYER_UPDATE` - EMPLOYER_UPDATE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._put( + path_template("/v1/employers/{employer_id}/payroll-access-setup", employer_id=employer_id), + body=maybe_transform( + { + "access_method": access_method, + "all_benefit_eligible_employees_present": all_benefit_eligible_employees_present, + "classifications_accurate": classifications_accurate, + "employees_in_payroll_acknowledged": employees_in_payroll_acknowledged, + "has_additional_payroll_system": has_additional_payroll_system, + "is_controlled_group": is_controlled_group, + "payroll_data_impacts_eligibility_acknowledged": payroll_data_impacts_eligibility_acknowledged, + "additional_access_method": additional_access_method, + "additional_integration_confirmed": additional_integration_confirmed, + "additional_login_url": additional_login_url, + "additional_password": additional_password, + "additional_phone": additional_phone, + "additional_username": additional_username, + "classification_correction_source": classification_correction_source, + "integration_confirmed": integration_confirmed, + "login_url": login_url, + "misclassified_employee_names": misclassified_employee_names, + "missing_employee_resolution": missing_employee_resolution, + "password": password, + "phone": phone, + "remaining_employee_action": remaining_employee_action, + "same_payroll_covers_other_eins": same_payroll_covers_other_eins, + "username": username, + }, + employer_submit_payroll_access_setup_params.EmployerSubmitPayrollAccessSetupParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerSubmitPayrollAccessSetupResponse, + ) + + def update_settings( + self, + employer_id: str, + *, + pay_frequency: Literal["weekly", "bi_weekly", "semi_monthly", "monthly"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerUpdateSettingsResponse: + """Updates configuration settings for a specific employer. + + The employer must belong + to the authenticated organization. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + pay_frequency: - `weekly` - Weekly + - `bi_weekly` - Bi Weekly + - `semi_monthly` - Semi Monthly + - `monthly` - Monthly + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._put( + path_template("/v1/employers/{employer_id}/settings", employer_id=employer_id), + body=maybe_transform( + {"pay_frequency": pay_frequency}, employer_update_settings_params.EmployerUpdateSettingsParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerUpdateSettingsResponse, + ) + + +class AsyncEmployersResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncEmployersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncEmployersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncEmployersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncEmployersResourceWithStreamingResponse(self) + + async def create( + self, + *, + address: employer_create_params.Address, + ein: str, + email: str, + legal_name: str, + name: str, + phone_number: Optional[str] | Omit = omit, + reference_id: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerResponse: + """Creates a new employer for the authenticated organization. + + Requires employer + name, legal name, EIN, email, and address information. Returns the created + employer with its assigned ID. + + Args: + address: Employer address + + ein: Employer Identification Number (format: XX-XXXXXXX) + + email: Email address for billing and communications + + legal_name: Legal business name + + name: Employer display name + + phone_number: Employer phone number (10-digit US format, e.g. 5551234567) + + reference_id: External reference ID for this employer + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/employers", + body=await async_maybe_transform( + { + "address": address, + "ein": ein, + "email": email, + "legal_name": legal_name, + "name": name, + "phone_number": phone_number, + "reference_id": reference_id, + }, + employer_create_params.EmployerCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerResponse, + ) + + async def retrieve( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerResponse: + """Retrieves detailed information for a specific employer by ID. + + The employer must + belong to the authenticated organization. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._get( + path_template("/v1/employers/{employer_id}", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerResponse, + ) + + async def update( + self, + employer_id: str, + *, + active: Optional[bool] | Omit = omit, + address: Optional[employer_update_params.Address] | Omit = omit, + legal_name: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerResponse: + """Updates an existing employer. + + All fields are optional — only provided fields are + updated. PO Box addresses are rejected. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + active: Whether the employer is active + + address: Employer address + + legal_name: Legal business name + + name: Employer display name + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._put( + path_template("/v1/employers/{employer_id}", employer_id=employer_id), + body=await async_maybe_transform( + { + "active": active, + "address": address, + "legal_name": legal_name, + "name": name, + }, + employer_update_params.EmployerUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerResponse, + ) + + def list( + self, + *, + benefit_family: List[Literal["mec", "mvp", "ichra", "vpc", "dental", "vision"]] | Omit = omit, + benefit_lifecycle_stage: List[Literal["open_enrollment", "renewal", "active", "onboarding", "cancelled"]] + | Omit = omit, + hris_provider: SequenceNotStr[str] | Omit = omit, + hris_status: List[Literal["Pending", "Active", "Inactive", "Paused", "Terminated"]] | Omit = omit, + include_cancelled: bool | Omit = omit, + limit: int | Omit = omit, + page: int | Omit = omit, + search: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[EmployerListResponse, AsyncPageNumberPage[EmployerListResponse]]: + """ + Returns the caller's employer book — every employer with its computed columns + (enrollment-rate summary, benefit-family tags, HRIS connection, + benefit-lifecycle stage) merged with the employer's flat CRM fields (legal name, + EIN, contact, address, timestamps). The book is derived from the authenticated + principal: one organization's employers, or every organization's for a caller + whose reach is not a single organization. Supports search by display name, legal + name, or exact EIN, employer id or contact email, benefit-family/lifecycle/HRIS + filters, and page/limit pagination. + + Args: + benefit_family: Filter to employers with at least one active benefit in these families. + + benefit_lifecycle_stage: Filter to employers in one of these computed benefit-lifecycle stages. + + hris_provider: Filter to employers whose HRIS connection is with one of these payroll providers + (e.g. `ADP RUN`). Matched case-insensitively; free text, so read the available + values from the HRIS-providers endpoint rather than assuming a fixed set. + + hris_status: Filter to employers whose HRIS connection is in one of these statuses. + + include_cancelled: Include cancelled employers (hidden by default unless their stage is explicitly + requested). + + limit: Items per page. + + page: Page number. + + search: Employer filter. Matches the display name or the legal name case-insensitively + as a substring, or one of these exactly: the EIN (with or without its dash), the + employer id, or the contact email of one of the employer's non-disabled admins. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/employers", + page=AsyncPageNumberPage[EmployerListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "benefit_family": benefit_family, + "benefit_lifecycle_stage": benefit_lifecycle_stage, + "hris_provider": hris_provider, + "hris_status": hris_status, + "include_cancelled": include_cancelled, + "limit": limit, + "page": page, + "search": search, + }, + employer_list_params.EmployerListParams, + ), + ), + model=EmployerListResponse, + ) + + async def ensure_payroll_integration_email( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerEnsurePayrollIntegrationEmailResponse: + """ + Provision and return the employer's payroll integration email. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._put( + path_template("/v1/employers/{employer_id}/payroll-integration-email", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerEnsurePayrollIntegrationEmailResponse, + ) + + def list_benefit_plan_year_enrollments( + self, + benefit_plan_year_id: str, + *, + employer_id: str, + election_status: List[Literal["Enrolled", "Expired", "Pending", "Waived"]] | Omit = omit, + limit: int | Omit = omit, + page: int | Omit = omit, + search: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ + EmployerListBenefitPlanYearEnrollmentsResponse, + AsyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], + ]: + """ + Returns a paginated list of every member with an enrollment in one of an + employer's plan years, any election status: what they elected, where their + coverage stands, dependent count, carrier, plan, tier, and the plan's total + monthly cost. The caller must be authorized for the employer `empr_<...>`; an + unknown or unauthorized employer, or an unknown plan year `plyr_<...>`, + returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + benefit_plan_year_id: Unique benefit-plan-year identifier (plyr\\__\\**). + + election_status: Filter by election status. Repeat the parameter to match several. + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + search: Case-insensitive search. Matches member name partially, and the `member_id` + exactly — either your own reference id or the prefixed `grpmbr_<...>` id. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + if not benefit_plan_year_id: + raise ValueError( + f"Expected a non-empty value for `benefit_plan_year_id` but received {benefit_plan_year_id!r}" + ) + return self._get_api_list( + path_template( + "/v1/employers/{employer_id}/benefit-plan-years/{benefit_plan_year_id}/enrollments", + employer_id=employer_id, + benefit_plan_year_id=benefit_plan_year_id, + ), + page=AsyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "election_status": election_status, + "limit": limit, + "page": page, + "search": search, + }, + employer_list_benefit_plan_year_enrollments_params.EmployerListBenefitPlanYearEnrollmentsParams, + ), + ), + model=EmployerListBenefitPlanYearEnrollmentsResponse, + ) + + async def list_benefit_plan_years( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerListBenefitPlanYearsResponse: + """ + Returns the employer's benefit plan years (all years, or one when `year` is + given), each with its benefits, offered states, benefit families, and the + year-level enrollment roll-up. The caller must be authorized for the employer; + an unknown or unauthorized employer returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._get( + path_template("/v1/employers/{employer_id}/benefit-plan-years", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerListBenefitPlanYearsResponse, + ) + + def list_employees( + self, + employer_id: str, + *, + employment_status: Literal["active", "terminated"] | Omit = omit, + limit: int | Omit = omit, + page: int | Omit = omit, + search: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Employee, AsyncPageNumberPage[Employee]]: + """Retrieves a paginated list of employees for a specific employer. + + The caller must + be authorized for the employer; an unknown or unauthorized employer returns 404. + Results are paginated using page and limit parameters and can be narrowed with a + case-insensitive `search` (first name, last name, or email) and an + `employment_status` filter (active or terminated). Each employee includes + payroll deductions from the most recent statement period. When a new deduction + statement is generated, previous period deductions are replaced. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + employment_status: Filter by employment status (active or terminated) + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + search: Case-insensitive search across employee first name, last name, and email + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get_api_list( + path_template("/v1/employers/{employer_id}/employees", employer_id=employer_id), + page=AsyncPageNumberPage[Employee], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "employment_status": employment_status, + "limit": limit, + "page": page, + "search": search, + }, + employer_list_employees_params.EmployerListEmployeesParams, + ), + ), + model=Employee, + ) + + async def list_hris_providers( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerListHRISProvidersResponse: + """ + Returns the distinct HRIS/payroll providers across the same book + `GET /v1/employers` returns, sorted for display. Use these as the values for the + employers list's `hris_provider` filter — filter on `provider`, show + `provider_label`. The stored providers are free text, so they cannot be + enumerated in advance. + """ + return await self._get( + "/v1/employers/hris-providers", + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerListHRISProvidersResponse, + ) + + async def list_invoices( + self, + employer_id: str, + *, + limit: int | Omit = omit, + offset: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerListInvoicesResponse: + """Returns a cursor-paginated page of the employer's billing invoices, newest + first. + + Pass the `next_offset` from a previous page as `offset` to fetch the next + page. The caller must be authorized for the employer; an unknown or unauthorized + employer returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + limit: Maximum number of invoices per page + + offset: Opaque cursor from a previous page's next_offset + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._get( + path_template("/v1/employers/{employer_id}/invoices", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=await async_maybe_transform( + { + "limit": limit, + "offset": offset, + }, + employer_list_invoices_params.EmployerListInvoicesParams, + ), + ), + cast_to=EmployerListInvoicesResponse, + ) + + def list_payroll_deduction_statements( + self, + employer_id: str, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ + EmployerListPayrollDeductionStatementsResponse, + AsyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], + ]: + """ + Returns a paginated list of the employer's payroll-deduction statements, newest + period first, each with its period, generation date, distinct employee count, + total deduction, change-file link, and deduction frequency. Statements + superseded by a later correction are excluded. The caller must be authorized for + the employer; an unknown or unauthorized employer returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + limit: Maximum number of statements per page + + page: Page number to retrieve (starts at 1) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return self._get_api_list( + path_template("/v1/employers/{employer_id}/payroll-deduction-statements", employer_id=employer_id), + page=AsyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + }, + employer_list_payroll_deduction_statements_params.EmployerListPayrollDeductionStatementsParams, + ), + ), + model=EmployerListPayrollDeductionStatementsResponse, + ) + + async def retrieve_benefit_plan_year( + self, + benefit_plan_year_id: str, + *, + employer_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerRetrieveBenefitPlanYearResponse: + """ + Returns one benefit plan year in full — its benefit details plus the per-benefit + enrollment rate and SPD link — addressed by its `benefit_plan_year_id`. The + caller must be authorized for the employer; an unknown or unauthorized plan year + returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + benefit_plan_year_id: Unique benefit-plan-year identifier (plyr\\__\\**). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + if not benefit_plan_year_id: + raise ValueError( + f"Expected a non-empty value for `benefit_plan_year_id` but received {benefit_plan_year_id!r}" + ) + return await self._get( + path_template( + "/v1/employers/{employer_id}/benefit-plan-years/{benefit_plan_year_id}", + employer_id=employer_id, + benefit_plan_year_id=benefit_plan_year_id, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerRetrieveBenefitPlanYearResponse, + ) + + async def retrieve_hris( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerRetrieveHRISResponse: + """ + Returns the employer's HRIS connection — provider, status, last sync, and synced + row count — or null when the employer has no integration. The caller must be + authorized for the employer; an unknown or unauthorized employer returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._get( + path_template("/v1/employers/{employer_id}/hris", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerRetrieveHRISResponse, + ) + + async def retrieve_invoice_pdf( + self, + invoice_id: str, + *, + employer_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerRetrieveInvoicePdfResponse: + """ + Returns the time-limited PDF download link for a single invoice belonging to the + employer's billing customer. `invoice_id` is the external Chargebee id (not a + prefixed UUID). The caller must be authorized for the employer; an unknown or + unauthorized employer or invoice returns 404. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + invoice_id: External Chargebee invoice id (not a prefixed UUID). + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + if not invoice_id: + raise ValueError(f"Expected a non-empty value for `invoice_id` but received {invoice_id!r}") + return await self._get( + path_template( + "/v1/employers/{employer_id}/invoices/{invoice_id}/pdf", employer_id=employer_id, invoice_id=invoice_id + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerRetrieveInvoicePdfResponse, + ) + + async def retrieve_payroll_access_setup( + self, + employer_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerRetrievePayrollAccessSetupResponse: + """ + Return whether the employer has submitted payroll access setup. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._get( + path_template("/v1/employers/{employer_id}/payroll-access-setup", employer_id=employer_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerRetrievePayrollAccessSetupResponse, + ) + + async def submit_census_sync( + self, + employer_id: str, + *, + employees: Iterable[employer_submit_census_sync_params.Employee], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerSubmitCensusSyncResponse: + """Submits a census sync payload for the specified employer. + + The employees in the + payload will be queued for processing. Returns an accepted response with the + timestamp of acceptance. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._post( + path_template("/v1/employers/{employer_id}/census-sync", employer_id=employer_id), + body=await async_maybe_transform( + {"employees": employees}, employer_submit_census_sync_params.EmployerSubmitCensusSyncParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerSubmitCensusSyncResponse, + ) + + async def submit_payroll_access_setup( + self, + employer_id: str, + *, + access_method: Literal["SELF_SETUP", "NEEDS_HELP"], + all_benefit_eligible_employees_present: bool, + classifications_accurate: bool, + employees_in_payroll_acknowledged: bool, + has_additional_payroll_system: bool, + is_controlled_group: bool, + payroll_data_impacts_eligibility_acknowledged: bool, + additional_access_method: Optional[Literal["SELF_SETUP", "NEEDS_HELP"]] | Omit = omit, + additional_integration_confirmed: Optional[bool] | Omit = omit, + additional_login_url: Optional[str] | Omit = omit, + additional_password: Optional[str] | Omit = omit, + additional_phone: Optional[str] | Omit = omit, + additional_username: Optional[str] | Omit = omit, + classification_correction_source: Optional[Literal["ENTER_NAMES", "EMAIL_LIST"]] | Omit = omit, + integration_confirmed: Optional[bool] | Omit = omit, + login_url: Optional[str] | Omit = omit, + misclassified_employee_names: SequenceNotStr[str] | Omit = omit, + missing_employee_resolution: Optional[Literal["EMAIL_CENSUS", "SECOND_SYSTEM_ACCESS"]] | Omit = omit, + password: Optional[str] | Omit = omit, + phone: Optional[str] | Omit = omit, + remaining_employee_action: Optional[Literal["VITABLE_UPDATE", "EMPLOYER_UPDATE"]] | Omit = omit, + same_payroll_covers_other_eins: Optional[bool] | Omit = omit, + username: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerSubmitPayrollAccessSetupResponse: + """ + Submit the employer's payroll access setup answers. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + access_method: - `SELF_SETUP` - SELF_SETUP + - `NEEDS_HELP` - NEEDS_HELP + + additional_access_method: - `SELF_SETUP` - SELF_SETUP + - `NEEDS_HELP` - NEEDS_HELP + + classification_correction_source: - `ENTER_NAMES` - ENTER_NAMES + - `EMAIL_LIST` - EMAIL_LIST + + missing_employee_resolution: - `EMAIL_CENSUS` - EMAIL_CENSUS + - `SECOND_SYSTEM_ACCESS` - SECOND_SYSTEM_ACCESS + + remaining_employee_action: - `VITABLE_UPDATE` - VITABLE_UPDATE + - `EMPLOYER_UPDATE` - EMPLOYER_UPDATE + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._put( + path_template("/v1/employers/{employer_id}/payroll-access-setup", employer_id=employer_id), + body=await async_maybe_transform( + { + "access_method": access_method, + "all_benefit_eligible_employees_present": all_benefit_eligible_employees_present, + "classifications_accurate": classifications_accurate, + "employees_in_payroll_acknowledged": employees_in_payroll_acknowledged, + "has_additional_payroll_system": has_additional_payroll_system, + "is_controlled_group": is_controlled_group, + "payroll_data_impacts_eligibility_acknowledged": payroll_data_impacts_eligibility_acknowledged, + "additional_access_method": additional_access_method, + "additional_integration_confirmed": additional_integration_confirmed, + "additional_login_url": additional_login_url, + "additional_password": additional_password, + "additional_phone": additional_phone, + "additional_username": additional_username, + "classification_correction_source": classification_correction_source, + "integration_confirmed": integration_confirmed, + "login_url": login_url, + "misclassified_employee_names": misclassified_employee_names, + "missing_employee_resolution": missing_employee_resolution, + "password": password, + "phone": phone, + "remaining_employee_action": remaining_employee_action, + "same_payroll_covers_other_eins": same_payroll_covers_other_eins, + "username": username, + }, + employer_submit_payroll_access_setup_params.EmployerSubmitPayrollAccessSetupParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerSubmitPayrollAccessSetupResponse, + ) + + async def update_settings( + self, + employer_id: str, + *, + pay_frequency: Literal["weekly", "bi_weekly", "semi_monthly", "monthly"], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EmployerUpdateSettingsResponse: + """Updates configuration settings for a specific employer. + + The employer must belong + to the authenticated organization. + + Args: + employer_id: Unique employer identifier (empr\\__\\**) + + pay_frequency: - `weekly` - Weekly + - `bi_weekly` - Bi Weekly + - `semi_monthly` - Semi Monthly + - `monthly` - Monthly + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not employer_id: + raise ValueError(f"Expected a non-empty value for `employer_id` but received {employer_id!r}") + return await self._put( + path_template("/v1/employers/{employer_id}/settings", employer_id=employer_id), + body=await async_maybe_transform( + {"pay_frequency": pay_frequency}, employer_update_settings_params.EmployerUpdateSettingsParams + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EmployerUpdateSettingsResponse, + ) + + +class EmployersResourceWithRawResponse: + def __init__(self, employers: EmployersResource) -> None: + self._employers = employers + + self.create = to_raw_response_wrapper( + employers.create, + ) + self.retrieve = to_raw_response_wrapper( + employers.retrieve, + ) + self.update = to_raw_response_wrapper( + employers.update, + ) + self.list = to_raw_response_wrapper( + employers.list, + ) + self.ensure_payroll_integration_email = to_raw_response_wrapper( + employers.ensure_payroll_integration_email, + ) + self.list_benefit_plan_year_enrollments = to_raw_response_wrapper( + employers.list_benefit_plan_year_enrollments, + ) + self.list_benefit_plan_years = to_raw_response_wrapper( + employers.list_benefit_plan_years, + ) + self.list_employees = to_raw_response_wrapper( + employers.list_employees, + ) + self.list_hris_providers = to_raw_response_wrapper( + employers.list_hris_providers, + ) + self.list_invoices = to_raw_response_wrapper( + employers.list_invoices, + ) + self.list_payroll_deduction_statements = to_raw_response_wrapper( + employers.list_payroll_deduction_statements, + ) + self.retrieve_benefit_plan_year = to_raw_response_wrapper( + employers.retrieve_benefit_plan_year, + ) + self.retrieve_hris = to_raw_response_wrapper( + employers.retrieve_hris, + ) + self.retrieve_invoice_pdf = to_raw_response_wrapper( + employers.retrieve_invoice_pdf, + ) + self.retrieve_payroll_access_setup = to_raw_response_wrapper( + employers.retrieve_payroll_access_setup, + ) + self.submit_census_sync = to_raw_response_wrapper( + employers.submit_census_sync, + ) + self.submit_payroll_access_setup = to_raw_response_wrapper( + employers.submit_payroll_access_setup, + ) + self.update_settings = to_raw_response_wrapper( + employers.update_settings, + ) + + +class AsyncEmployersResourceWithRawResponse: + def __init__(self, employers: AsyncEmployersResource) -> None: + self._employers = employers + + self.create = async_to_raw_response_wrapper( + employers.create, + ) + self.retrieve = async_to_raw_response_wrapper( + employers.retrieve, + ) + self.update = async_to_raw_response_wrapper( + employers.update, + ) + self.list = async_to_raw_response_wrapper( + employers.list, + ) + self.ensure_payroll_integration_email = async_to_raw_response_wrapper( + employers.ensure_payroll_integration_email, + ) + self.list_benefit_plan_year_enrollments = async_to_raw_response_wrapper( + employers.list_benefit_plan_year_enrollments, + ) + self.list_benefit_plan_years = async_to_raw_response_wrapper( + employers.list_benefit_plan_years, + ) + self.list_employees = async_to_raw_response_wrapper( + employers.list_employees, + ) + self.list_hris_providers = async_to_raw_response_wrapper( + employers.list_hris_providers, + ) + self.list_invoices = async_to_raw_response_wrapper( + employers.list_invoices, + ) + self.list_payroll_deduction_statements = async_to_raw_response_wrapper( + employers.list_payroll_deduction_statements, + ) + self.retrieve_benefit_plan_year = async_to_raw_response_wrapper( + employers.retrieve_benefit_plan_year, + ) + self.retrieve_hris = async_to_raw_response_wrapper( + employers.retrieve_hris, + ) + self.retrieve_invoice_pdf = async_to_raw_response_wrapper( + employers.retrieve_invoice_pdf, + ) + self.retrieve_payroll_access_setup = async_to_raw_response_wrapper( + employers.retrieve_payroll_access_setup, + ) + self.submit_census_sync = async_to_raw_response_wrapper( + employers.submit_census_sync, + ) + self.submit_payroll_access_setup = async_to_raw_response_wrapper( + employers.submit_payroll_access_setup, + ) + self.update_settings = async_to_raw_response_wrapper( + employers.update_settings, + ) + + +class EmployersResourceWithStreamingResponse: + def __init__(self, employers: EmployersResource) -> None: + self._employers = employers + + self.create = to_streamed_response_wrapper( + employers.create, + ) + self.retrieve = to_streamed_response_wrapper( + employers.retrieve, + ) + self.update = to_streamed_response_wrapper( + employers.update, + ) + self.list = to_streamed_response_wrapper( + employers.list, + ) + self.ensure_payroll_integration_email = to_streamed_response_wrapper( + employers.ensure_payroll_integration_email, + ) + self.list_benefit_plan_year_enrollments = to_streamed_response_wrapper( + employers.list_benefit_plan_year_enrollments, + ) + self.list_benefit_plan_years = to_streamed_response_wrapper( + employers.list_benefit_plan_years, + ) + self.list_employees = to_streamed_response_wrapper( + employers.list_employees, + ) + self.list_hris_providers = to_streamed_response_wrapper( + employers.list_hris_providers, + ) + self.list_invoices = to_streamed_response_wrapper( + employers.list_invoices, + ) + self.list_payroll_deduction_statements = to_streamed_response_wrapper( + employers.list_payroll_deduction_statements, + ) + self.retrieve_benefit_plan_year = to_streamed_response_wrapper( + employers.retrieve_benefit_plan_year, + ) + self.retrieve_hris = to_streamed_response_wrapper( + employers.retrieve_hris, + ) + self.retrieve_invoice_pdf = to_streamed_response_wrapper( + employers.retrieve_invoice_pdf, + ) + self.retrieve_payroll_access_setup = to_streamed_response_wrapper( + employers.retrieve_payroll_access_setup, + ) + self.submit_census_sync = to_streamed_response_wrapper( + employers.submit_census_sync, + ) + self.submit_payroll_access_setup = to_streamed_response_wrapper( + employers.submit_payroll_access_setup, + ) + self.update_settings = to_streamed_response_wrapper( + employers.update_settings, + ) + + +class AsyncEmployersResourceWithStreamingResponse: + def __init__(self, employers: AsyncEmployersResource) -> None: + self._employers = employers + + self.create = async_to_streamed_response_wrapper( + employers.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + employers.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + employers.update, + ) + self.list = async_to_streamed_response_wrapper( + employers.list, + ) + self.ensure_payroll_integration_email = async_to_streamed_response_wrapper( + employers.ensure_payroll_integration_email, + ) + self.list_benefit_plan_year_enrollments = async_to_streamed_response_wrapper( + employers.list_benefit_plan_year_enrollments, + ) + self.list_benefit_plan_years = async_to_streamed_response_wrapper( + employers.list_benefit_plan_years, + ) + self.list_employees = async_to_streamed_response_wrapper( + employers.list_employees, + ) + self.list_hris_providers = async_to_streamed_response_wrapper( + employers.list_hris_providers, + ) + self.list_invoices = async_to_streamed_response_wrapper( + employers.list_invoices, + ) + self.list_payroll_deduction_statements = async_to_streamed_response_wrapper( + employers.list_payroll_deduction_statements, + ) + self.retrieve_benefit_plan_year = async_to_streamed_response_wrapper( + employers.retrieve_benefit_plan_year, + ) + self.retrieve_hris = async_to_streamed_response_wrapper( + employers.retrieve_hris, + ) + self.retrieve_invoice_pdf = async_to_streamed_response_wrapper( + employers.retrieve_invoice_pdf, + ) + self.retrieve_payroll_access_setup = async_to_streamed_response_wrapper( + employers.retrieve_payroll_access_setup, + ) + self.submit_census_sync = async_to_streamed_response_wrapper( + employers.submit_census_sync, + ) + self.submit_payroll_access_setup = async_to_streamed_response_wrapper( + employers.submit_payroll_access_setup, + ) + self.update_settings = async_to_streamed_response_wrapper( + employers.update_settings, + ) diff --git a/src/vitable_connect/resources/enrollments.py b/src/vitable_connect/resources/enrollments.py new file mode 100644 index 0000000..7cfd4fd --- /dev/null +++ b/src/vitable_connect/resources/enrollments.py @@ -0,0 +1,444 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ..types import enrollment_reissue_params, enrollment_terminate_params +from .._types import Body, Omit, Query, Headers, NoneType, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.enrollment_reissue_response import EnrollmentReissueResponse +from ..types.enrollment_retrieve_response import EnrollmentRetrieveResponse + +__all__ = ["EnrollmentsResource", "AsyncEnrollmentsResource"] + + +class EnrollmentsResource(SyncAPIResource): + """Manage benefit enrollments and elections for employees""" + + @cached_property + def with_raw_response(self) -> EnrollmentsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return EnrollmentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> EnrollmentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return EnrollmentsResourceWithStreamingResponse(self) + + def retrieve( + self, + enrollment_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EnrollmentRetrieveResponse: + """ + Retrieves a single enrollment: the employee and employer it belongs to, the + benefit product, its status, the coverage period, the employee payroll deduction + and employer contribution, and the enrolled plan's Summary of Benefits and + Coverage document when one is on file. An enrollment the caller cannot reach is + indistinguishable from one that does not exist. + + Args: + enrollment_id: Unique enrollment identifier (enrl\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not enrollment_id: + raise ValueError(f"Expected a non-empty value for `enrollment_id` but received {enrollment_id!r}") + return self._get( + path_template("/v1/enrollments/{enrollment_id}", enrollment_id=enrollment_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EnrollmentRetrieveResponse, + ) + + def reissue( + self, + enrollment_id: str, + *, + qualifying_life_event_id: Optional[str] | Omit = omit, + reason: Optional[str] | Omit = omit, + ticket_number: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EnrollmentReissueResponse: + """ + Closes the targeted enrollment and creates a new unanswered enrollment for the + same member and plan year. VPC never requires a qualifying life event; other + products require an accepted, member-owned event outside open enrollment. + User-backed callers must provide a reason; it is optional for organization + API-key callers. Tenant mismatches return a non-disclosing 404 before the + request body is validated. + + Args: + enrollment_id: Unique enrollment identifier (enrl\\__\\**) + + qualifying_life_event_id: Accepted member qualifying life event identifier (qle\\__\\**) + + reason: Audit reason for the reissue; required for user-backed callers and optional for + long-lived organization API-key callers + + ticket_number: Optional support or operational ticket number + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not enrollment_id: + raise ValueError(f"Expected a non-empty value for `enrollment_id` but received {enrollment_id!r}") + return self._post( + path_template("/v1/enrollments/{enrollment_id}/reissue", enrollment_id=enrollment_id), + body=maybe_transform( + { + "qualifying_life_event_id": qualifying_life_event_id, + "reason": reason, + "ticket_number": ticket_number, + }, + enrollment_reissue_params.EnrollmentReissueParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EnrollmentReissueResponse, + ) + + def terminate( + self, + enrollment_id: str, + *, + qualifying_life_event_id: Optional[str] | Omit = omit, + reason: Optional[str] | Omit = omit, + ticket_number: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Terminates enrolled coverage immediately. + + An accepted qualifying life event + owned by the enrollment member is required unless the plan is VPC or ICHRA. + User-backed callers must provide a reason; it is optional for organization + API-key callers. API keys may act across the caller organization's book. Tenant + mismatches return the same non-disclosing 404 before the request body is + validated. + + Args: + enrollment_id: Unique enrollment identifier (enrl\\__\\**) + + qualifying_life_event_id: Accepted member qualifying life event identifier (qle\\__\\**) + + reason: Audit reason for the termination; required for user-backed callers and optional + for long-lived organization API-key callers + + ticket_number: Optional support or operational ticket number + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not enrollment_id: + raise ValueError(f"Expected a non-empty value for `enrollment_id` but received {enrollment_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return self._post( + path_template("/v1/enrollments/{enrollment_id}/terminate", enrollment_id=enrollment_id), + body=maybe_transform( + { + "qualifying_life_event_id": qualifying_life_event_id, + "reason": reason, + "ticket_number": ticket_number, + }, + enrollment_terminate_params.EnrollmentTerminateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class AsyncEnrollmentsResource(AsyncAPIResource): + """Manage benefit enrollments and elections for employees""" + + @cached_property + def with_raw_response(self) -> AsyncEnrollmentsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncEnrollmentsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncEnrollmentsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncEnrollmentsResourceWithStreamingResponse(self) + + async def retrieve( + self, + enrollment_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EnrollmentRetrieveResponse: + """ + Retrieves a single enrollment: the employee and employer it belongs to, the + benefit product, its status, the coverage period, the employee payroll deduction + and employer contribution, and the enrolled plan's Summary of Benefits and + Coverage document when one is on file. An enrollment the caller cannot reach is + indistinguishable from one that does not exist. + + Args: + enrollment_id: Unique enrollment identifier (enrl\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not enrollment_id: + raise ValueError(f"Expected a non-empty value for `enrollment_id` but received {enrollment_id!r}") + return await self._get( + path_template("/v1/enrollments/{enrollment_id}", enrollment_id=enrollment_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EnrollmentRetrieveResponse, + ) + + async def reissue( + self, + enrollment_id: str, + *, + qualifying_life_event_id: Optional[str] | Omit = omit, + reason: Optional[str] | Omit = omit, + ticket_number: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> EnrollmentReissueResponse: + """ + Closes the targeted enrollment and creates a new unanswered enrollment for the + same member and plan year. VPC never requires a qualifying life event; other + products require an accepted, member-owned event outside open enrollment. + User-backed callers must provide a reason; it is optional for organization + API-key callers. Tenant mismatches return a non-disclosing 404 before the + request body is validated. + + Args: + enrollment_id: Unique enrollment identifier (enrl\\__\\**) + + qualifying_life_event_id: Accepted member qualifying life event identifier (qle\\__\\**) + + reason: Audit reason for the reissue; required for user-backed callers and optional for + long-lived organization API-key callers + + ticket_number: Optional support or operational ticket number + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not enrollment_id: + raise ValueError(f"Expected a non-empty value for `enrollment_id` but received {enrollment_id!r}") + return await self._post( + path_template("/v1/enrollments/{enrollment_id}/reissue", enrollment_id=enrollment_id), + body=await async_maybe_transform( + { + "qualifying_life_event_id": qualifying_life_event_id, + "reason": reason, + "ticket_number": ticket_number, + }, + enrollment_reissue_params.EnrollmentReissueParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=EnrollmentReissueResponse, + ) + + async def terminate( + self, + enrollment_id: str, + *, + qualifying_life_event_id: Optional[str] | Omit = omit, + reason: Optional[str] | Omit = omit, + ticket_number: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> None: + """Terminates enrolled coverage immediately. + + An accepted qualifying life event + owned by the enrollment member is required unless the plan is VPC or ICHRA. + User-backed callers must provide a reason; it is optional for organization + API-key callers. API keys may act across the caller organization's book. Tenant + mismatches return the same non-disclosing 404 before the request body is + validated. + + Args: + enrollment_id: Unique enrollment identifier (enrl\\__\\**) + + qualifying_life_event_id: Accepted member qualifying life event identifier (qle\\__\\**) + + reason: Audit reason for the termination; required for user-backed callers and optional + for long-lived organization API-key callers + + ticket_number: Optional support or operational ticket number + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not enrollment_id: + raise ValueError(f"Expected a non-empty value for `enrollment_id` but received {enrollment_id!r}") + extra_headers = {"Accept": "*/*", **(extra_headers or {})} + return await self._post( + path_template("/v1/enrollments/{enrollment_id}/terminate", enrollment_id=enrollment_id), + body=await async_maybe_transform( + { + "qualifying_life_event_id": qualifying_life_event_id, + "reason": reason, + "ticket_number": ticket_number, + }, + enrollment_terminate_params.EnrollmentTerminateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=NoneType, + ) + + +class EnrollmentsResourceWithRawResponse: + def __init__(self, enrollments: EnrollmentsResource) -> None: + self._enrollments = enrollments + + self.retrieve = to_raw_response_wrapper( + enrollments.retrieve, + ) + self.reissue = to_raw_response_wrapper( + enrollments.reissue, + ) + self.terminate = to_raw_response_wrapper( + enrollments.terminate, + ) + + +class AsyncEnrollmentsResourceWithRawResponse: + def __init__(self, enrollments: AsyncEnrollmentsResource) -> None: + self._enrollments = enrollments + + self.retrieve = async_to_raw_response_wrapper( + enrollments.retrieve, + ) + self.reissue = async_to_raw_response_wrapper( + enrollments.reissue, + ) + self.terminate = async_to_raw_response_wrapper( + enrollments.terminate, + ) + + +class EnrollmentsResourceWithStreamingResponse: + def __init__(self, enrollments: EnrollmentsResource) -> None: + self._enrollments = enrollments + + self.retrieve = to_streamed_response_wrapper( + enrollments.retrieve, + ) + self.reissue = to_streamed_response_wrapper( + enrollments.reissue, + ) + self.terminate = to_streamed_response_wrapper( + enrollments.terminate, + ) + + +class AsyncEnrollmentsResourceWithStreamingResponse: + def __init__(self, enrollments: AsyncEnrollmentsResource) -> None: + self._enrollments = enrollments + + self.retrieve = async_to_streamed_response_wrapper( + enrollments.retrieve, + ) + self.reissue = async_to_streamed_response_wrapper( + enrollments.reissue, + ) + self.terminate = async_to_streamed_response_wrapper( + enrollments.terminate, + ) diff --git a/src/vitable_connect/resources/groups/__init__.py b/src/vitable_connect/resources/groups/__init__.py new file mode 100644 index 0000000..f514819 --- /dev/null +++ b/src/vitable_connect/resources/groups/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .groups import ( + GroupsResource, + AsyncGroupsResource, + GroupsResourceWithRawResponse, + AsyncGroupsResourceWithRawResponse, + GroupsResourceWithStreamingResponse, + AsyncGroupsResourceWithStreamingResponse, +) +from .members import ( + MembersResource, + AsyncMembersResource, + MembersResourceWithRawResponse, + AsyncMembersResourceWithRawResponse, + MembersResourceWithStreamingResponse, + AsyncMembersResourceWithStreamingResponse, +) + +__all__ = [ + "MembersResource", + "AsyncMembersResource", + "MembersResourceWithRawResponse", + "AsyncMembersResourceWithRawResponse", + "MembersResourceWithStreamingResponse", + "AsyncMembersResourceWithStreamingResponse", + "GroupsResource", + "AsyncGroupsResource", + "GroupsResourceWithRawResponse", + "AsyncGroupsResourceWithRawResponse", + "GroupsResourceWithStreamingResponse", + "AsyncGroupsResourceWithStreamingResponse", +] diff --git a/src/vitable_connect/resources/groups/groups.py b/src/vitable_connect/resources/groups/groups.py new file mode 100644 index 0000000..2f79ec4 --- /dev/null +++ b/src/vitable_connect/resources/groups/groups.py @@ -0,0 +1,509 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional + +import httpx + +from ...types import group_list_params, group_create_params, group_update_params +from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from ..._utils import path_template, maybe_transform, async_maybe_transform +from ..._compat import cached_property +from ..._resource import SyncAPIResource, AsyncAPIResource +from ..._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...pagination import SyncPageNumberPage, AsyncPageNumberPage +from ...types.group import Group +from ..._base_client import AsyncPaginator, make_request_options +from .members.members import ( + MembersResource, + AsyncMembersResource, + MembersResourceWithRawResponse, + AsyncMembersResourceWithRawResponse, + MembersResourceWithStreamingResponse, + AsyncMembersResourceWithStreamingResponse, +) +from ...types.group_response import GroupResponse + +__all__ = ["GroupsResource", "AsyncGroupsResource"] + + +class GroupsResource(SyncAPIResource): + @cached_property + def members(self) -> MembersResource: + return MembersResource(self._client) + + @cached_property + def with_raw_response(self) -> GroupsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return GroupsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> GroupsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return GroupsResourceWithStreamingResponse(self) + + def create( + self, + *, + external_reference_id: str, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupResponse: + """ + Creates a new group scoped to the authenticated organization. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/groups", + body=maybe_transform( + { + "external_reference_id": external_reference_id, + "name": name, + }, + group_create_params.GroupCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupResponse, + ) + + def retrieve( + self, + group_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupResponse: + """Retrieves a single group by its prefixed ID. + + Returns 404 if the group does not + belong to the authenticated organization. + + Args: + group_id: Unique group identifier (grp\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._get( + path_template("/v1/groups/{group_id}", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupResponse, + ) + + def update( + self, + group_id: str, + *, + external_reference_id: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupResponse: + """Partially updates a group's name or external reference ID. + + Returns 404 if the + group does not belong to the authenticated organization. + + Args: + group_id: Unique group identifier (grp\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._patch( + path_template("/v1/groups/{group_id}", group_id=group_id), + body=maybe_transform( + { + "external_reference_id": external_reference_id, + "name": name, + }, + group_update_params.GroupUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[Group]: + """ + Returns a paginated list of groups belonging to the authenticated organization. + + Args: + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/groups", + page=SyncPageNumberPage[Group], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + }, + group_list_params.GroupListParams, + ), + ), + model=Group, + ) + + +class AsyncGroupsResource(AsyncAPIResource): + @cached_property + def members(self) -> AsyncMembersResource: + return AsyncMembersResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncGroupsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncGroupsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncGroupsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncGroupsResourceWithStreamingResponse(self) + + async def create( + self, + *, + external_reference_id: str, + name: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupResponse: + """ + Creates a new group scoped to the authenticated organization. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/groups", + body=await async_maybe_transform( + { + "external_reference_id": external_reference_id, + "name": name, + }, + group_create_params.GroupCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupResponse, + ) + + async def retrieve( + self, + group_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupResponse: + """Retrieves a single group by its prefixed ID. + + Returns 404 if the group does not + belong to the authenticated organization. + + Args: + group_id: Unique group identifier (grp\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._get( + path_template("/v1/groups/{group_id}", group_id=group_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupResponse, + ) + + async def update( + self, + group_id: str, + *, + external_reference_id: Optional[str] | Omit = omit, + name: Optional[str] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> GroupResponse: + """Partially updates a group's name or external reference ID. + + Returns 404 if the + group does not belong to the authenticated organization. + + Args: + group_id: Unique group identifier (grp\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._patch( + path_template("/v1/groups/{group_id}", group_id=group_id), + body=await async_maybe_transform( + { + "external_reference_id": external_reference_id, + "name": name, + }, + group_update_params.GroupUpdateParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=GroupResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[Group, AsyncPageNumberPage[Group]]: + """ + Returns a paginated list of groups belonging to the authenticated organization. + + Args: + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/groups", + page=AsyncPageNumberPage[Group], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + }, + group_list_params.GroupListParams, + ), + ), + model=Group, + ) + + +class GroupsResourceWithRawResponse: + def __init__(self, groups: GroupsResource) -> None: + self._groups = groups + + self.create = to_raw_response_wrapper( + groups.create, + ) + self.retrieve = to_raw_response_wrapper( + groups.retrieve, + ) + self.update = to_raw_response_wrapper( + groups.update, + ) + self.list = to_raw_response_wrapper( + groups.list, + ) + + @cached_property + def members(self) -> MembersResourceWithRawResponse: + return MembersResourceWithRawResponse(self._groups.members) + + +class AsyncGroupsResourceWithRawResponse: + def __init__(self, groups: AsyncGroupsResource) -> None: + self._groups = groups + + self.create = async_to_raw_response_wrapper( + groups.create, + ) + self.retrieve = async_to_raw_response_wrapper( + groups.retrieve, + ) + self.update = async_to_raw_response_wrapper( + groups.update, + ) + self.list = async_to_raw_response_wrapper( + groups.list, + ) + + @cached_property + def members(self) -> AsyncMembersResourceWithRawResponse: + return AsyncMembersResourceWithRawResponse(self._groups.members) + + +class GroupsResourceWithStreamingResponse: + def __init__(self, groups: GroupsResource) -> None: + self._groups = groups + + self.create = to_streamed_response_wrapper( + groups.create, + ) + self.retrieve = to_streamed_response_wrapper( + groups.retrieve, + ) + self.update = to_streamed_response_wrapper( + groups.update, + ) + self.list = to_streamed_response_wrapper( + groups.list, + ) + + @cached_property + def members(self) -> MembersResourceWithStreamingResponse: + return MembersResourceWithStreamingResponse(self._groups.members) + + +class AsyncGroupsResourceWithStreamingResponse: + def __init__(self, groups: AsyncGroupsResource) -> None: + self._groups = groups + + self.create = async_to_streamed_response_wrapper( + groups.create, + ) + self.retrieve = async_to_streamed_response_wrapper( + groups.retrieve, + ) + self.update = async_to_streamed_response_wrapper( + groups.update, + ) + self.list = async_to_streamed_response_wrapper( + groups.list, + ) + + @cached_property + def members(self) -> AsyncMembersResourceWithStreamingResponse: + return AsyncMembersResourceWithStreamingResponse(self._groups.members) diff --git a/src/vitable_connect/resources/groups/members/__init__.py b/src/vitable_connect/resources/groups/members/__init__.py new file mode 100644 index 0000000..db9c43c --- /dev/null +++ b/src/vitable_connect/resources/groups/members/__init__.py @@ -0,0 +1,33 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .sync import ( + SyncResource, + AsyncSyncResource, + SyncResourceWithRawResponse, + AsyncSyncResourceWithRawResponse, + SyncResourceWithStreamingResponse, + AsyncSyncResourceWithStreamingResponse, +) +from .members import ( + MembersResource, + AsyncMembersResource, + MembersResourceWithRawResponse, + AsyncMembersResourceWithRawResponse, + MembersResourceWithStreamingResponse, + AsyncMembersResourceWithStreamingResponse, +) + +__all__ = [ + "SyncResource", + "AsyncSyncResource", + "SyncResourceWithRawResponse", + "AsyncSyncResourceWithRawResponse", + "SyncResourceWithStreamingResponse", + "AsyncSyncResourceWithStreamingResponse", + "MembersResource", + "AsyncMembersResource", + "MembersResourceWithRawResponse", + "AsyncMembersResourceWithRawResponse", + "MembersResourceWithStreamingResponse", + "AsyncMembersResourceWithStreamingResponse", +] diff --git a/src/vitable_connect/resources/groups/members/members.py b/src/vitable_connect/resources/groups/members/members.py new file mode 100644 index 0000000..ea352c7 --- /dev/null +++ b/src/vitable_connect/resources/groups/members/members.py @@ -0,0 +1,102 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .sync import ( + SyncResource, + AsyncSyncResource, + SyncResourceWithRawResponse, + AsyncSyncResourceWithRawResponse, + SyncResourceWithStreamingResponse, + AsyncSyncResourceWithStreamingResponse, +) +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource + +__all__ = ["MembersResource", "AsyncMembersResource"] + + +class MembersResource(SyncAPIResource): + @cached_property + def sync(self) -> SyncResource: + return SyncResource(self._client) + + @cached_property + def with_raw_response(self) -> MembersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return MembersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MembersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return MembersResourceWithStreamingResponse(self) + + +class AsyncMembersResource(AsyncAPIResource): + @cached_property + def sync(self) -> AsyncSyncResource: + return AsyncSyncResource(self._client) + + @cached_property + def with_raw_response(self) -> AsyncMembersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncMembersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMembersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncMembersResourceWithStreamingResponse(self) + + +class MembersResourceWithRawResponse: + def __init__(self, members: MembersResource) -> None: + self._members = members + + @cached_property + def sync(self) -> SyncResourceWithRawResponse: + return SyncResourceWithRawResponse(self._members.sync) + + +class AsyncMembersResourceWithRawResponse: + def __init__(self, members: AsyncMembersResource) -> None: + self._members = members + + @cached_property + def sync(self) -> AsyncSyncResourceWithRawResponse: + return AsyncSyncResourceWithRawResponse(self._members.sync) + + +class MembersResourceWithStreamingResponse: + def __init__(self, members: MembersResource) -> None: + self._members = members + + @cached_property + def sync(self) -> SyncResourceWithStreamingResponse: + return SyncResourceWithStreamingResponse(self._members.sync) + + +class AsyncMembersResourceWithStreamingResponse: + def __init__(self, members: AsyncMembersResource) -> None: + self._members = members + + @cached_property + def sync(self) -> AsyncSyncResourceWithStreamingResponse: + return AsyncSyncResourceWithStreamingResponse(self._members.sync) diff --git a/src/vitable_connect/resources/groups/members/sync.py b/src/vitable_connect/resources/groups/members/sync.py new file mode 100644 index 0000000..71d1758 --- /dev/null +++ b/src/vitable_connect/resources/groups/members/sync.py @@ -0,0 +1,276 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Iterable + +import httpx + +from ...._types import Body, Query, Headers, NotGiven, not_given +from ...._utils import path_template, maybe_transform, async_maybe_transform +from ...._compat import cached_property +from ...._resource import SyncAPIResource, AsyncAPIResource +from ...._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ...._base_client import make_request_options +from ....types.groups.members import sync_submit_params +from ....types.groups.members.sync_submit_response import SyncSubmitResponse +from ....types.groups.members.sync_retrieve_response import SyncRetrieveResponse + +__all__ = ["SyncResource", "AsyncSyncResource"] + + +class SyncResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> SyncResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return SyncResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SyncResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return SyncResourceWithStreamingResponse(self) + + def retrieve( + self, + request_id: str, + *, + group_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncRetrieveResponse: + """ + Retrieves a previously-submitted group member sync request by its `grpmsr_` ID. + Returns the acceptance timestamp, completion timestamp (if processing has + finished), and the per-member `results` once available. While processing is in + flight, `completed_at` and `results` are `null`. + + Args: + group_id: Unique group identifier (grp\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + if not request_id: + raise ValueError(f"Expected a non-empty value for `request_id` but received {request_id!r}") + return self._get( + path_template("/v1/groups/{group_id}/members/sync/{request_id}", group_id=group_id, request_id=request_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SyncRetrieveResponse, + ) + + def submit( + self, + group_id: str, + *, + members: Iterable[sync_submit_params.Member], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncSubmitResponse: + """Submits a member sync payload for the specified group. + + Members in the payload + will be queued for processing asynchronously. Returns HTTP 202 with the batch ID + and acceptance timestamp. + + Args: + group_id: Unique group identifier (grp\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return self._post( + path_template("/v1/groups/{group_id}/members/sync", group_id=group_id), + body=maybe_transform({"members": members}, sync_submit_params.SyncSubmitParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SyncSubmitResponse, + ) + + +class AsyncSyncResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncSyncResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncSyncResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSyncResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncSyncResourceWithStreamingResponse(self) + + async def retrieve( + self, + request_id: str, + *, + group_id: str, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncRetrieveResponse: + """ + Retrieves a previously-submitted group member sync request by its `grpmsr_` ID. + Returns the acceptance timestamp, completion timestamp (if processing has + finished), and the per-member `results` once available. While processing is in + flight, `completed_at` and `results` are `null`. + + Args: + group_id: Unique group identifier (grp\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + if not request_id: + raise ValueError(f"Expected a non-empty value for `request_id` but received {request_id!r}") + return await self._get( + path_template("/v1/groups/{group_id}/members/sync/{request_id}", group_id=group_id, request_id=request_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SyncRetrieveResponse, + ) + + async def submit( + self, + group_id: str, + *, + members: Iterable[sync_submit_params.Member], + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncSubmitResponse: + """Submits a member sync payload for the specified group. + + Members in the payload + will be queued for processing asynchronously. Returns HTTP 202 with the batch ID + and acceptance timestamp. + + Args: + group_id: Unique group identifier (grp\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not group_id: + raise ValueError(f"Expected a non-empty value for `group_id` but received {group_id!r}") + return await self._post( + path_template("/v1/groups/{group_id}/members/sync", group_id=group_id), + body=await async_maybe_transform({"members": members}, sync_submit_params.SyncSubmitParams), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=SyncSubmitResponse, + ) + + +class SyncResourceWithRawResponse: + def __init__(self, sync: SyncResource) -> None: + self._sync = sync + + self.retrieve = to_raw_response_wrapper( + sync.retrieve, + ) + self.submit = to_raw_response_wrapper( + sync.submit, + ) + + +class AsyncSyncResourceWithRawResponse: + def __init__(self, sync: AsyncSyncResource) -> None: + self._sync = sync + + self.retrieve = async_to_raw_response_wrapper( + sync.retrieve, + ) + self.submit = async_to_raw_response_wrapper( + sync.submit, + ) + + +class SyncResourceWithStreamingResponse: + def __init__(self, sync: SyncResource) -> None: + self._sync = sync + + self.retrieve = to_streamed_response_wrapper( + sync.retrieve, + ) + self.submit = to_streamed_response_wrapper( + sync.submit, + ) + + +class AsyncSyncResourceWithStreamingResponse: + def __init__(self, sync: AsyncSyncResource) -> None: + self._sync = sync + + self.retrieve = async_to_streamed_response_wrapper( + sync.retrieve, + ) + self.submit = async_to_streamed_response_wrapper( + sync.submit, + ) diff --git a/src/vitable_connect/resources/members.py b/src/vitable_connect/resources/members.py new file mode 100644 index 0000000..4071581 --- /dev/null +++ b/src/vitable_connect/resources/members.py @@ -0,0 +1,935 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal + +import httpx + +from ..types import member_list_params, member_list_qualifying_life_events_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncPageNumberPage, AsyncPageNumberPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.member_list_response import MemberListResponse +from ..types.member_retrieve_response import MemberRetrieveResponse +from ..types.member_list_id_cards_response import MemberListIDCardsResponse +from ..types.member_list_dependents_response import MemberListDependentsResponse +from ..types.member_list_employments_response import MemberListEmploymentsResponse +from ..types.member_list_enrollments_response import MemberListEnrollmentsResponse +from ..types.member_retrieve_household_response import MemberRetrieveHouseholdResponse +from ..types.member_list_qualifying_life_events_response import MemberListQualifyingLifeEventsResponse + +__all__ = ["MembersResource", "AsyncMembersResource"] + + +class MembersResource(SyncAPIResource): + """Browse the members covered across your book and read a member's profile""" + + @cached_property + def with_raw_response(self) -> MembersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return MembersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> MembersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return MembersResourceWithStreamingResponse(self) + + def retrieve( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberRetrieveResponse: + """ + Retrieves a member's profile by ID — identity, demographics, address, contact + details, tobacco status, and profile status. Access is scoped to the + authenticated principal; a member not visible to the caller returns a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return self._get( + path_template("/v1/members/{member_id}", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberRetrieveResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + search: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[MemberListResponse]: + """ + Retrieves a paginated list of the members in the authenticated organization's + book — identity, contact details, and address. The book covers members reached + through an employer in the organization's book as well as members of a group it + owns. Supports free-text search (name, email, phone number, or exact member id). + + Args: + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + search: Case-insensitive search across member name, email, and phone number; exact match + on member id (prefixed or raw uuid) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v2/members", + page=SyncPageNumberPage[MemberListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + "search": search, + }, + member_list_params.MemberListParams, + ), + ), + model=MemberListResponse, + ) + + def list_dependents( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListDependentsResponse: + """ + Lists a member's active legal dependents — name, relationship, date of birth, + age, and sex at birth. Access is scoped to the authenticated principal; a member + not visible to the caller returns a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return self._get( + path_template("/v1/members/{member_id}/dependents", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberListDependentsResponse, + ) + + def list_employments( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListEmploymentsResponse: + """ + Lists a member's employment across every employer — the same employee record + shape as the employer's employees list, plus the employer name. For an + organization caller the rows are scoped to companies in that organization's + book; a member (self/household) or Vitable Admin sees all employments. A member + not visible to the caller returns a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return self._get( + path_template("/v1/members/{member_id}/employments", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberListEmploymentsResponse, + ) + + def list_enrollments( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListEnrollmentsResponse: + """ + Lists a member's benefit enrollments across every employer — benefit type and + product, employer, carrier, plan, tier, employee deduction, employer + contribution and total premium, the individual enrollment coverage boundary + (`coverage_end`), the separate pre-effective cancellation boundary + (`cancelled_date`), and the distinct benefit plan-year boundary + (`plan_year_coverage_end`) used to determine whether the plan year itself has + ended, the date the enrollment record was created (`issued_date`, the value Ops + labels Issued on, reported for every row whatever the member answered), the + window the member could answer in -- which never opens before the enrollment was + issued, so a row issued mid-open-enrollment starts its window on its issue date + -- whether a qualifying life event would currently be required for reissue under + the product/open-enrollment rule, enrollment/open-enrollment window, and two + statuses: `election_status` (what the member answered) and `policy_status` (what + became of their coverage, null unless they enrolled). Every row includes a + stable enrollment ID and the exact employer and benefit plan-year IDs used to + fetch that row's plan-year detail. The full list is returned across all states + so the client derives active plans (effective and upcoming) and the enrollment + history from those per-row statuses. For an organization caller the rows are + scoped to companies in that organization's book; a member (self/household) or + Vitable Admin sees all enrollments. A member not visible to the caller returns + a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return self._get( + path_template("/v1/members/{member_id}/enrollments", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberListEnrollmentsResponse, + ) + + def list_id_cards( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListIDCardsResponse: + """ + Lists a member's benefit ID cards — card type (medical, dental, vision, or rx), + employer, plan, provider network, claims payer, carrier contact details, and the + disclaimers printed on the card. Medical, dental and vision cards come from the + member's active digital benefit cards; the rx card from the member's Ventegra + pharmacy benefit (omitted when the member has no free-medication coverage), + which carries no plan, network, or carrier details. Access is scoped to the + authenticated principal, and an organization caller sees only cards from + employers in its book; a member not visible to the caller returns a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return self._get( + path_template("/v1/members/{member_id}/id-cards", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberListIDCardsResponse, + ) + + def list_qualifying_life_events( + self, + member_id: str, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + status: Literal["approved", "denied", "pending"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[MemberListQualifyingLifeEventsResponse]: + """ + Lists a member's qualifying life events, including events already used for + another enrollment. Returns all statuses by default; pass the status query param + to filter to one (e.g. approved). Events are ordered newest submission first + with stable paging. Custom text is present only when submitted and is otherwise + null. A member not visible to the caller returns a 404. API keys and unbound + access tokens have organization-wide access. Employer-bound tokens require + employment at the bound employer, and employee-bound tokens require the exact + employee-member relationship. Organization or scope mismatches return a 404 + before pagination is validated. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + status: Optional. Filter to a single QLE status; omit to return all statuses. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return self._get_api_list( + path_template("/v1/members/{member_id}/qualifying-life-events", member_id=member_id), + page=SyncPageNumberPage[MemberListQualifyingLifeEventsResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + "status": status, + }, + member_list_qualifying_life_events_params.MemberListQualifyingLifeEventsParams, + ), + ), + model=MemberListQualifyingLifeEventsResponse, + ) + + def retrieve_household( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberRetrieveHouseholdResponse: + """ + Lists a member's household as a per-participant table — the account holder plus + each active household member, with name, relationship, member type, date of + birth, and household-admin flag. Access is scoped to the authenticated + principal; a member not visible to the caller (or with no household) returns + a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return self._get( + path_template("/v1/members/{member_id}/household", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberRetrieveHouseholdResponse, + ) + + +class AsyncMembersResource(AsyncAPIResource): + """Browse the members covered across your book and read a member's profile""" + + @cached_property + def with_raw_response(self) -> AsyncMembersResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncMembersResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncMembersResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncMembersResourceWithStreamingResponse(self) + + async def retrieve( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberRetrieveResponse: + """ + Retrieves a member's profile by ID — identity, demographics, address, contact + details, tobacco status, and profile status. Access is scoped to the + authenticated principal; a member not visible to the caller returns a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return await self._get( + path_template("/v1/members/{member_id}", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberRetrieveResponse, + ) + + def list( + self, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + search: str | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[MemberListResponse, AsyncPageNumberPage[MemberListResponse]]: + """ + Retrieves a paginated list of the members in the authenticated organization's + book — identity, contact details, and address. The book covers members reached + through an employer in the organization's book as well as members of a group it + owns. Supports free-text search (name, email, phone number, or exact member id). + + Args: + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + search: Case-insensitive search across member name, email, and phone number; exact match + on member id (prefixed or raw uuid) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v2/members", + page=AsyncPageNumberPage[MemberListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + "search": search, + }, + member_list_params.MemberListParams, + ), + ), + model=MemberListResponse, + ) + + async def list_dependents( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListDependentsResponse: + """ + Lists a member's active legal dependents — name, relationship, date of birth, + age, and sex at birth. Access is scoped to the authenticated principal; a member + not visible to the caller returns a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return await self._get( + path_template("/v1/members/{member_id}/dependents", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberListDependentsResponse, + ) + + async def list_employments( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListEmploymentsResponse: + """ + Lists a member's employment across every employer — the same employee record + shape as the employer's employees list, plus the employer name. For an + organization caller the rows are scoped to companies in that organization's + book; a member (self/household) or Vitable Admin sees all employments. A member + not visible to the caller returns a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return await self._get( + path_template("/v1/members/{member_id}/employments", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberListEmploymentsResponse, + ) + + async def list_enrollments( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListEnrollmentsResponse: + """ + Lists a member's benefit enrollments across every employer — benefit type and + product, employer, carrier, plan, tier, employee deduction, employer + contribution and total premium, the individual enrollment coverage boundary + (`coverage_end`), the separate pre-effective cancellation boundary + (`cancelled_date`), and the distinct benefit plan-year boundary + (`plan_year_coverage_end`) used to determine whether the plan year itself has + ended, the date the enrollment record was created (`issued_date`, the value Ops + labels Issued on, reported for every row whatever the member answered), the + window the member could answer in -- which never opens before the enrollment was + issued, so a row issued mid-open-enrollment starts its window on its issue date + -- whether a qualifying life event would currently be required for reissue under + the product/open-enrollment rule, enrollment/open-enrollment window, and two + statuses: `election_status` (what the member answered) and `policy_status` (what + became of their coverage, null unless they enrolled). Every row includes a + stable enrollment ID and the exact employer and benefit plan-year IDs used to + fetch that row's plan-year detail. The full list is returned across all states + so the client derives active plans (effective and upcoming) and the enrollment + history from those per-row statuses. For an organization caller the rows are + scoped to companies in that organization's book; a member (self/household) or + Vitable Admin sees all enrollments. A member not visible to the caller returns + a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return await self._get( + path_template("/v1/members/{member_id}/enrollments", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberListEnrollmentsResponse, + ) + + async def list_id_cards( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberListIDCardsResponse: + """ + Lists a member's benefit ID cards — card type (medical, dental, vision, or rx), + employer, plan, provider network, claims payer, carrier contact details, and the + disclaimers printed on the card. Medical, dental and vision cards come from the + member's active digital benefit cards; the rx card from the member's Ventegra + pharmacy benefit (omitted when the member has no free-medication coverage), + which carries no plan, network, or carrier details. Access is scoped to the + authenticated principal, and an organization caller sees only cards from + employers in its book; a member not visible to the caller returns a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return await self._get( + path_template("/v1/members/{member_id}/id-cards", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberListIDCardsResponse, + ) + + def list_qualifying_life_events( + self, + member_id: str, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + status: Literal["approved", "denied", "pending"] | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[ + MemberListQualifyingLifeEventsResponse, AsyncPageNumberPage[MemberListQualifyingLifeEventsResponse] + ]: + """ + Lists a member's qualifying life events, including events already used for + another enrollment. Returns all statuses by default; pass the status query param + to filter to one (e.g. approved). Events are ordered newest submission first + with stable paging. Custom text is present only when submitted and is otherwise + null. A member not visible to the caller returns a 404. API keys and unbound + access tokens have organization-wide access. Employer-bound tokens require + employment at the bound employer, and employee-bound tokens require the exact + employee-member relationship. Organization or scope mismatches return a 404 + before pagination is validated. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + status: Optional. Filter to a single QLE status; omit to return all statuses. + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return self._get_api_list( + path_template("/v1/members/{member_id}/qualifying-life-events", member_id=member_id), + page=AsyncPageNumberPage[MemberListQualifyingLifeEventsResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + "status": status, + }, + member_list_qualifying_life_events_params.MemberListQualifyingLifeEventsParams, + ), + ), + model=MemberListQualifyingLifeEventsResponse, + ) + + async def retrieve_household( + self, + member_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> MemberRetrieveHouseholdResponse: + """ + Lists a member's household as a per-participant table — the account holder plus + each active household member, with name, relationship, member type, date of + birth, and household-admin flag. Access is scoped to the authenticated + principal; a member not visible to the caller (or with no household) returns + a 404. + + Args: + member_id: Unique member identifier (mbr\\__\\**) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not member_id: + raise ValueError(f"Expected a non-empty value for `member_id` but received {member_id!r}") + return await self._get( + path_template("/v1/members/{member_id}/household", member_id=member_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=MemberRetrieveHouseholdResponse, + ) + + +class MembersResourceWithRawResponse: + def __init__(self, members: MembersResource) -> None: + self._members = members + + self.retrieve = to_raw_response_wrapper( + members.retrieve, + ) + self.list = to_raw_response_wrapper( + members.list, + ) + self.list_dependents = to_raw_response_wrapper( + members.list_dependents, + ) + self.list_employments = to_raw_response_wrapper( + members.list_employments, + ) + self.list_enrollments = to_raw_response_wrapper( + members.list_enrollments, + ) + self.list_id_cards = to_raw_response_wrapper( + members.list_id_cards, + ) + self.list_qualifying_life_events = to_raw_response_wrapper( + members.list_qualifying_life_events, + ) + self.retrieve_household = to_raw_response_wrapper( + members.retrieve_household, + ) + + +class AsyncMembersResourceWithRawResponse: + def __init__(self, members: AsyncMembersResource) -> None: + self._members = members + + self.retrieve = async_to_raw_response_wrapper( + members.retrieve, + ) + self.list = async_to_raw_response_wrapper( + members.list, + ) + self.list_dependents = async_to_raw_response_wrapper( + members.list_dependents, + ) + self.list_employments = async_to_raw_response_wrapper( + members.list_employments, + ) + self.list_enrollments = async_to_raw_response_wrapper( + members.list_enrollments, + ) + self.list_id_cards = async_to_raw_response_wrapper( + members.list_id_cards, + ) + self.list_qualifying_life_events = async_to_raw_response_wrapper( + members.list_qualifying_life_events, + ) + self.retrieve_household = async_to_raw_response_wrapper( + members.retrieve_household, + ) + + +class MembersResourceWithStreamingResponse: + def __init__(self, members: MembersResource) -> None: + self._members = members + + self.retrieve = to_streamed_response_wrapper( + members.retrieve, + ) + self.list = to_streamed_response_wrapper( + members.list, + ) + self.list_dependents = to_streamed_response_wrapper( + members.list_dependents, + ) + self.list_employments = to_streamed_response_wrapper( + members.list_employments, + ) + self.list_enrollments = to_streamed_response_wrapper( + members.list_enrollments, + ) + self.list_id_cards = to_streamed_response_wrapper( + members.list_id_cards, + ) + self.list_qualifying_life_events = to_streamed_response_wrapper( + members.list_qualifying_life_events, + ) + self.retrieve_household = to_streamed_response_wrapper( + members.retrieve_household, + ) + + +class AsyncMembersResourceWithStreamingResponse: + def __init__(self, members: AsyncMembersResource) -> None: + self._members = members + + self.retrieve = async_to_streamed_response_wrapper( + members.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + members.list, + ) + self.list_dependents = async_to_streamed_response_wrapper( + members.list_dependents, + ) + self.list_employments = async_to_streamed_response_wrapper( + members.list_employments, + ) + self.list_enrollments = async_to_streamed_response_wrapper( + members.list_enrollments, + ) + self.list_id_cards = async_to_streamed_response_wrapper( + members.list_id_cards, + ) + self.list_qualifying_life_events = async_to_streamed_response_wrapper( + members.list_qualifying_life_events, + ) + self.retrieve_household = async_to_streamed_response_wrapper( + members.retrieve_household, + ) diff --git a/src/vitable_connect/resources/organizations.py b/src/vitable_connect/resources/organizations.py new file mode 100644 index 0000000..cf9e92b --- /dev/null +++ b/src/vitable_connect/resources/organizations.py @@ -0,0 +1,281 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal + +import httpx + +from ..types import organization_create_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform, async_maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from .._base_client import make_request_options +from ..types.organization_list_response import OrganizationListResponse +from ..types.organization_create_response import OrganizationCreateResponse + +__all__ = ["OrganizationsResource", "AsyncOrganizationsResource"] + + +class OrganizationsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> OrganizationsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return OrganizationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> OrganizationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return OrganizationsResourceWithStreamingResponse(self) + + def create( + self, + *, + name: str, + type: Optional[ + Literal["BROKERAGE", "TPA", "GENERAL_AGENT", "CHANNEL_PARTNER", "CONSULTING_FIRM", "API_PLATFORM"] + ] + | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> OrganizationCreateResponse: + """ + Onboards the authenticated user's partner Organization: creates the local + Organization + the creator's admin membership atomically, then mirrors it to + WorkOS (creates the WorkOS org and binds the creator as admin). 409 + `organization_already_exists` when the user already has an organization (v0: one + organization per user). + + Args: + type: - `BROKERAGE` - BROKERAGE + - `TPA` - TPA + - `GENERAL_AGENT` - GENERAL_AGENT + - `CHANNEL_PARTNER` - CHANNEL_PARTNER + - `CONSULTING_FIRM` - CONSULTING_FIRM + - `API_PLATFORM` - API_PLATFORM + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._post( + "/v1/organizations", + body=maybe_transform( + { + "name": name, + "type": type, + }, + organization_create_params.OrganizationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={}, + ), + cast_to=OrganizationCreateResponse, + ) + + def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> OrganizationListResponse: + """ + Lists the organizations the authenticated caller is an active member of + (paginated). Returns an empty list when the caller belongs to no organizations. + """ + return self._get( + "/v1/organizations", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={}, + ), + cast_to=OrganizationListResponse, + ) + + +class AsyncOrganizationsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncOrganizationsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncOrganizationsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncOrganizationsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncOrganizationsResourceWithStreamingResponse(self) + + async def create( + self, + *, + name: str, + type: Optional[ + Literal["BROKERAGE", "TPA", "GENERAL_AGENT", "CHANNEL_PARTNER", "CONSULTING_FIRM", "API_PLATFORM"] + ] + | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> OrganizationCreateResponse: + """ + Onboards the authenticated user's partner Organization: creates the local + Organization + the creator's admin membership atomically, then mirrors it to + WorkOS (creates the WorkOS org and binds the creator as admin). 409 + `organization_already_exists` when the user already has an organization (v0: one + organization per user). + + Args: + type: - `BROKERAGE` - BROKERAGE + - `TPA` - TPA + - `GENERAL_AGENT` - GENERAL_AGENT + - `CHANNEL_PARTNER` - CHANNEL_PARTNER + - `CONSULTING_FIRM` - CONSULTING_FIRM + - `API_PLATFORM` - API_PLATFORM + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return await self._post( + "/v1/organizations", + body=await async_maybe_transform( + { + "name": name, + "type": type, + }, + organization_create_params.OrganizationCreateParams, + ), + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={}, + ), + cast_to=OrganizationCreateResponse, + ) + + async def list( + self, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> OrganizationListResponse: + """ + Lists the organizations the authenticated caller is an active member of + (paginated). Returns an empty list when the caller belongs to no organizations. + """ + return await self._get( + "/v1/organizations", + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + security={}, + ), + cast_to=OrganizationListResponse, + ) + + +class OrganizationsResourceWithRawResponse: + def __init__(self, organizations: OrganizationsResource) -> None: + self._organizations = organizations + + self.create = to_raw_response_wrapper( + organizations.create, + ) + self.list = to_raw_response_wrapper( + organizations.list, + ) + + +class AsyncOrganizationsResourceWithRawResponse: + def __init__(self, organizations: AsyncOrganizationsResource) -> None: + self._organizations = organizations + + self.create = async_to_raw_response_wrapper( + organizations.create, + ) + self.list = async_to_raw_response_wrapper( + organizations.list, + ) + + +class OrganizationsResourceWithStreamingResponse: + def __init__(self, organizations: OrganizationsResource) -> None: + self._organizations = organizations + + self.create = to_streamed_response_wrapper( + organizations.create, + ) + self.list = to_streamed_response_wrapper( + organizations.list, + ) + + +class AsyncOrganizationsResourceWithStreamingResponse: + def __init__(self, organizations: AsyncOrganizationsResource) -> None: + self._organizations = organizations + + self.create = async_to_streamed_response_wrapper( + organizations.create, + ) + self.list = async_to_streamed_response_wrapper( + organizations.list, + ) diff --git a/src/vitable_connect/resources/plans.py b/src/vitable_connect/resources/plans.py new file mode 100644 index 0000000..c78b0c3 --- /dev/null +++ b/src/vitable_connect/resources/plans.py @@ -0,0 +1,196 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import httpx + +from ..types import plan_list_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncPageNumberPage, AsyncPageNumberPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.plan_list_response import PlanListResponse + +__all__ = ["PlansResource", "AsyncPlansResource"] + + +class PlansResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> PlansResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return PlansResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PlansResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return PlansResourceWithStreamingResponse(self) + + def list( + self, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[PlanListResponse]: + """ + Returns a paginated list of benefit plans linked to the authenticated + organization. + + Args: + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/plans", + page=SyncPageNumberPage[PlanListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + }, + plan_list_params.PlanListParams, + ), + ), + model=PlanListResponse, + ) + + +class AsyncPlansResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncPlansResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncPlansResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPlansResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncPlansResourceWithStreamingResponse(self) + + def list( + self, + *, + limit: int | Omit = omit, + page: int | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[PlanListResponse, AsyncPageNumberPage[PlanListResponse]]: + """ + Returns a paginated list of benefit plans linked to the authenticated + organization. + + Args: + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/plans", + page=AsyncPageNumberPage[PlanListResponse], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "limit": limit, + "page": page, + }, + plan_list_params.PlanListParams, + ), + ), + model=PlanListResponse, + ) + + +class PlansResourceWithRawResponse: + def __init__(self, plans: PlansResource) -> None: + self._plans = plans + + self.list = to_raw_response_wrapper( + plans.list, + ) + + +class AsyncPlansResourceWithRawResponse: + def __init__(self, plans: AsyncPlansResource) -> None: + self._plans = plans + + self.list = async_to_raw_response_wrapper( + plans.list, + ) + + +class PlansResourceWithStreamingResponse: + def __init__(self, plans: PlansResource) -> None: + self._plans = plans + + self.list = to_streamed_response_wrapper( + plans.list, + ) + + +class AsyncPlansResourceWithStreamingResponse: + def __init__(self, plans: AsyncPlansResource) -> None: + self._plans = plans + + self.list = async_to_streamed_response_wrapper( + plans.list, + ) diff --git a/src/vitable_connect/resources/webhook_events.py b/src/vitable_connect/resources/webhook_events.py new file mode 100644 index 0000000..7ddcd0e --- /dev/null +++ b/src/vitable_connect/resources/webhook_events.py @@ -0,0 +1,454 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Literal + +import httpx + +from ..types import webhook_event_list_params +from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given +from .._utils import path_template, maybe_transform +from .._compat import cached_property +from .._resource import SyncAPIResource, AsyncAPIResource +from .._response import ( + to_raw_response_wrapper, + to_streamed_response_wrapper, + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, +) +from ..pagination import SyncPageNumberPage, AsyncPageNumberPage +from .._base_client import AsyncPaginator, make_request_options +from ..types.webhook_event import WebhookEvent +from ..types.webhook_event_retrieve_response import WebhookEventRetrieveResponse +from ..types.webhook_event_list_deliveries_response import WebhookEventListDeliveriesResponse + +__all__ = ["WebhookEventsResource", "AsyncWebhookEventsResource"] + + +class WebhookEventsResource(SyncAPIResource): + @cached_property + def with_raw_response(self) -> WebhookEventsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return WebhookEventsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WebhookEventsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return WebhookEventsResourceWithStreamingResponse(self) + + def retrieve( + self, + event_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookEventRetrieveResponse: + """Retrieves a single webhook event by its prefixed ID. + + Returns 404 if the event + does not exist or belongs to a different organization. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not event_id: + raise ValueError(f"Expected a non-empty value for `event_id` but received {event_id!r}") + return self._get( + path_template("/v1/webhook-events/{event_id}", event_id=event_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookEventRetrieveResponse, + ) + + def list( + self, + *, + created_after: Union[str, datetime] | Omit = omit, + created_before: Union[str, datetime] | Omit = omit, + event_name: Literal[ + "enrollment.accepted", + "enrollment.terminated", + "enrollment.termination_rescheduled", + "enrollment.elected", + "enrollment.granted", + "enrollment.waived", + "enrollment.started", + "employee.eligibility_granted", + "employee.eligibility_terminated", + "employee.deactivated", + "employee.deduction_created", + ] + | Omit = omit, + limit: int | Omit = omit, + page: int | Omit = omit, + resource_id: str | Omit = omit, + resource_type: Literal["enrollment", "employee", "employer", "dependent", "plan_year", "payroll_deduction"] + | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> SyncPageNumberPage[WebhookEvent]: + """ + Retrieves a paginated list of webhook events for the authenticated organization. + Supports filtering by event name, resource type, resource ID, and date range. + + Args: + event_name: - `enrollment.accepted` - Enrollment Accepted + - `enrollment.terminated` - Enrollment Terminated + - `enrollment.termination_rescheduled` - Enrollment Termination Rescheduled + - `enrollment.elected` - Enrollment Elected + - `enrollment.granted` - Enrollment Granted + - `enrollment.waived` - Enrollment Waived + - `enrollment.started` - Enrollment Started + - `employee.eligibility_granted` - Employee Eligibility Granted + - `employee.eligibility_terminated` - Employee Eligibility Terminated + - `employee.deactivated` - Employee Deactivated + - `employee.deduction_created` - Employee Deduction Created + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + resource_type: - `enrollment` - Enrollment + - `employee` - Employee + - `employer` - Employer + - `dependent` - Dependent + - `plan_year` - Plan Year + - `payroll_deduction` - Payroll Deduction + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/webhook-events", + page=SyncPageNumberPage[WebhookEvent], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "created_after": created_after, + "created_before": created_before, + "event_name": event_name, + "limit": limit, + "page": page, + "resource_id": resource_id, + "resource_type": resource_type, + }, + webhook_event_list_params.WebhookEventListParams, + ), + ), + model=WebhookEvent, + ) + + def list_deliveries( + self, + event_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookEventListDeliveriesResponse: + """Retrieves all delivery attempts for a webhook event. + + Returns up to 100 + deliveries. Each delivery includes a computed status field (Pending, In + Progress, Delivered, or Failed). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not event_id: + raise ValueError(f"Expected a non-empty value for `event_id` but received {event_id!r}") + return self._get( + path_template("/v1/webhook-events/{event_id}/deliveries", event_id=event_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookEventListDeliveriesResponse, + ) + + +class AsyncWebhookEventsResource(AsyncAPIResource): + @cached_property + def with_raw_response(self) -> AsyncWebhookEventsResourceWithRawResponse: + """ + This property can be used as a prefix for any HTTP method call to return + the raw response object instead of the parsed content. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#accessing-raw-response-data-eg-headers + """ + return AsyncWebhookEventsResourceWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWebhookEventsResourceWithStreamingResponse: + """ + An alternative to `.with_raw_response` that doesn't eagerly read the response body. + + For more information, see https://www.github.com/Vitable-Inc/vitable-connect-python#with_streaming_response + """ + return AsyncWebhookEventsResourceWithStreamingResponse(self) + + async def retrieve( + self, + event_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookEventRetrieveResponse: + """Retrieves a single webhook event by its prefixed ID. + + Returns 404 if the event + does not exist or belongs to a different organization. + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not event_id: + raise ValueError(f"Expected a non-empty value for `event_id` but received {event_id!r}") + return await self._get( + path_template("/v1/webhook-events/{event_id}", event_id=event_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookEventRetrieveResponse, + ) + + def list( + self, + *, + created_after: Union[str, datetime] | Omit = omit, + created_before: Union[str, datetime] | Omit = omit, + event_name: Literal[ + "enrollment.accepted", + "enrollment.terminated", + "enrollment.termination_rescheduled", + "enrollment.elected", + "enrollment.granted", + "enrollment.waived", + "enrollment.started", + "employee.eligibility_granted", + "employee.eligibility_terminated", + "employee.deactivated", + "employee.deduction_created", + ] + | Omit = omit, + limit: int | Omit = omit, + page: int | Omit = omit, + resource_id: str | Omit = omit, + resource_type: Literal["enrollment", "employee", "employer", "dependent", "plan_year", "payroll_deduction"] + | Omit = omit, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncPaginator[WebhookEvent, AsyncPageNumberPage[WebhookEvent]]: + """ + Retrieves a paginated list of webhook events for the authenticated organization. + Supports filtering by event name, resource type, resource ID, and date range. + + Args: + event_name: - `enrollment.accepted` - Enrollment Accepted + - `enrollment.terminated` - Enrollment Terminated + - `enrollment.termination_rescheduled` - Enrollment Termination Rescheduled + - `enrollment.elected` - Enrollment Elected + - `enrollment.granted` - Enrollment Granted + - `enrollment.waived` - Enrollment Waived + - `enrollment.started` - Enrollment Started + - `employee.eligibility_granted` - Employee Eligibility Granted + - `employee.eligibility_terminated` - Employee Eligibility Terminated + - `employee.deactivated` - Employee Deactivated + - `employee.deduction_created` - Employee Deduction Created + + limit: Items per page (default: 20, max: 100) + + page: Page number (default: 1) + + resource_type: - `enrollment` - Enrollment + - `employee` - Employee + - `employer` - Employer + - `dependent` - Dependent + - `plan_year` - Plan Year + - `payroll_deduction` - Payroll Deduction + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + return self._get_api_list( + "/v1/webhook-events", + page=AsyncPageNumberPage[WebhookEvent], + options=make_request_options( + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + query=maybe_transform( + { + "created_after": created_after, + "created_before": created_before, + "event_name": event_name, + "limit": limit, + "page": page, + "resource_id": resource_id, + "resource_type": resource_type, + }, + webhook_event_list_params.WebhookEventListParams, + ), + ), + model=WebhookEvent, + ) + + async def list_deliveries( + self, + event_id: str, + *, + # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. + # The extra values given here take precedence over values defined on the client or passed to this method. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> WebhookEventListDeliveriesResponse: + """Retrieves all delivery attempts for a webhook event. + + Returns up to 100 + deliveries. Each delivery includes a computed status field (Pending, In + Progress, Delivered, or Failed). + + Args: + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + if not event_id: + raise ValueError(f"Expected a non-empty value for `event_id` but received {event_id!r}") + return await self._get( + path_template("/v1/webhook-events/{event_id}/deliveries", event_id=event_id), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=WebhookEventListDeliveriesResponse, + ) + + +class WebhookEventsResourceWithRawResponse: + def __init__(self, webhook_events: WebhookEventsResource) -> None: + self._webhook_events = webhook_events + + self.retrieve = to_raw_response_wrapper( + webhook_events.retrieve, + ) + self.list = to_raw_response_wrapper( + webhook_events.list, + ) + self.list_deliveries = to_raw_response_wrapper( + webhook_events.list_deliveries, + ) + + +class AsyncWebhookEventsResourceWithRawResponse: + def __init__(self, webhook_events: AsyncWebhookEventsResource) -> None: + self._webhook_events = webhook_events + + self.retrieve = async_to_raw_response_wrapper( + webhook_events.retrieve, + ) + self.list = async_to_raw_response_wrapper( + webhook_events.list, + ) + self.list_deliveries = async_to_raw_response_wrapper( + webhook_events.list_deliveries, + ) + + +class WebhookEventsResourceWithStreamingResponse: + def __init__(self, webhook_events: WebhookEventsResource) -> None: + self._webhook_events = webhook_events + + self.retrieve = to_streamed_response_wrapper( + webhook_events.retrieve, + ) + self.list = to_streamed_response_wrapper( + webhook_events.list, + ) + self.list_deliveries = to_streamed_response_wrapper( + webhook_events.list_deliveries, + ) + + +class AsyncWebhookEventsResourceWithStreamingResponse: + def __init__(self, webhook_events: AsyncWebhookEventsResource) -> None: + self._webhook_events = webhook_events + + self.retrieve = async_to_streamed_response_wrapper( + webhook_events.retrieve, + ) + self.list = async_to_streamed_response_wrapper( + webhook_events.list, + ) + self.list_deliveries = async_to_streamed_response_wrapper( + webhook_events.list_deliveries, + ) diff --git a/src/vitable_connect/types/__init__.py b/src/vitable_connect/types/__init__.py index 880af5f..29975b2 100644 --- a/src/vitable_connect/types/__init__.py +++ b/src/vitable_connect/types/__init__.py @@ -1,560 +1,99 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -# isort: skip_file +from __future__ import annotations -import typing -from importlib import import_module - -if typing.TYPE_CHECKING: - from .access_method import AccessMethod - from .access_token_response import AccessTokenResponse - from .additional_access_method import AdditionalAccessMethod - from .address import Address - from .address_request import AddressRequest - from .benefit_family import BenefitFamily - from .benefit_family_param_item import BenefitFamilyParamItem - from .benefit_lifecycle_stage_item import BenefitLifecycleStageItem - from .benefit_plan_network import BenefitPlanNetwork - from .benefit_plan_year_detail import BenefitPlanYearDetail - from .benefit_plan_year_id import BenefitPlanYearId - from .benefit_plan_year_list import BenefitPlanYearList - from .benefit_status import BenefitStatus - from .benefit_summary import BenefitSummary - from .bound_entity import BoundEntity - from .bound_entity_response import BoundEntityResponse - from .bound_entity_type import BoundEntityType - from .card_type import CardType - from .category import Category - from .census_sync_detail_response import CensusSyncDetailResponse - from .census_sync_employee_address_request import CensusSyncEmployeeAddressRequest - from .census_sync_employee_request import CensusSyncEmployeeRequest - from .census_sync_response import CensusSyncResponse - from .classification_correction_source import ClassificationCorrectionSource - from .compensation_type import CompensationType - from .contribution_class_compensation import ContributionClassCompensation - from .contribution_class_employment import ContributionClassEmployment - from .contribution_class_location import ContributionClassLocation - from .contribution_range import ContributionRange - from .contribution_strategy import ContributionStrategy - from .contribution_tier import ContributionTier - from .coverage_tier import CoverageTier - from .create_organization_request_type import CreateOrganizationRequestType - from .created_after import CreatedAfter - from .created_before import CreatedBefore - from .deduction_detail import DeductionDetail - from .deduction_frequency import DeductionFrequency - from .digital_benefit_card import DigitalBenefitCard - from .election_status_item import ElectionStatusItem - from .eligibility_policy import EligibilityPolicy - from .eligibility_policy_rule import EligibilityPolicyRule - from .eligibility_rule_compensation_type import EligibilityRuleCompensationType - from .eligibility_rule_employee_class import EligibilityRuleEmployeeClass - from .eligible_geographical_location import EligibleGeographicalLocation - from .eligible_geographical_location_type import EligibleGeographicalLocationType - from .employee import Employee - from .employee_address import EmployeeAddress - from .employee_address_input import EmployeeAddressInput - from .employee_class import EmployeeClass - from .employee_id import EmployeeId - from .employee_list_response import EmployeeListResponse - from .employee_response import EmployeeResponse - from .employee_search import EmployeeSearch - from .employee_status import EmployeeStatus - from .employer import Employer - from .employer_address import EmployerAddress - from .employer_address_input import EmployerAddressInput - from .employer_benefit_lifecycle_stage import EmployerBenefitLifecycleStage - from .employer_benefit_plan_year_response import EmployerBenefitPlanYearResponse - from .employer_benefit_plan_years_list_response import EmployerBenefitPlanYearsListResponse - from .employer_contact import EmployerContact - from .employer_hris import EmployerHris - from .employer_hris_response import EmployerHrisResponse - from .employer_hris_status import EmployerHrisStatus - from .employer_id import EmployerId - from .employer_invoice_pdf import EmployerInvoicePdf - from .employer_invoice_pdf_response import EmployerInvoicePdfResponse - from .employer_invoices_list_response import EmployerInvoicesListResponse - from .employer_payroll_deduction_statement_list_response import EmployerPayrollDeductionStatementListResponse - from .employer_response import EmployerResponse - from .employer_search import EmployerSearch - from .employer_settings import EmployerSettings - from .employer_settings_response import EmployerSettingsResponse - from .enrollment import Enrollment - from .enrollment_detail import EnrollmentDetail - from .enrollment_id import EnrollmentId - from .enrollment_list_response import EnrollmentListResponse - from .enrollment_rate import EnrollmentRate - from .enrollment_rate_summary import EnrollmentRateSummary - from .enrollment_response import EnrollmentResponse - from .enrollment_status import EnrollmentStatus - from .error_response import ErrorResponse - from .event_id import EventId - from .event_name import EventName - from .gender import Gender - from .grant_type import GrantType - from .group import Group - from .group_id import GroupId - from .group_list_response import GroupListResponse - from .group_member_sync_detail_response import GroupMemberSyncDetailResponse - from .group_member_sync_failure import GroupMemberSyncFailure - from .group_member_sync_member_request import GroupMemberSyncMemberRequest - from .group_member_sync_request_detail import GroupMemberSyncRequestDetail - from .group_member_sync_request_detail_response import GroupMemberSyncRequestDetailResponse - from .group_member_sync_response import GroupMemberSyncResponse - from .group_member_sync_results import GroupMemberSyncResults - from .group_response import GroupResponse - from .household_member import HouseholdMember - from .household_member_relationship import HouseholdMemberRelationship - from .household_member_type import HouseholdMemberType - from .household_members_response import HouseholdMembersResponse - from .hris_connection import HrisConnection - from .hris_status_item import HrisStatusItem - from .ichra_contribution_class import IchraContributionClass - from .idp_provider import IdpProvider - from .include_cancelled import IncludeCancelled - from .invoice import Invoice - from .invoice_id import InvoiceId - from .invoices_cursor_pagination import InvoicesCursorPagination - from .limit import Limit - from .list_webhook_event_deliveries_response import ListWebhookEventDeliveriesResponse - from .marital_status import MaritalStatus - from .member import Member - from .member_address import MemberAddress - from .member_dependents_response import MemberDependentsResponse - from .member_digital_benefit_cards_response import MemberDigitalBenefitCardsResponse - from .member_employments_response import MemberEmploymentsResponse - from .member_enrollment import MemberEnrollment - from .member_enrollments_response import MemberEnrollmentsResponse - from .member_id import MemberId - from .member_legal_dependent import MemberLegalDependent - from .member_list_item import MemberListItem - from .member_list_response import MemberListResponse - from .member_qualifying_life_event import MemberQualifyingLifeEvent - from .member_qualifying_life_event_list_response import MemberQualifyingLifeEventListResponse - from .member_response import MemberResponse - from .member_search import MemberSearch - from .member_status import MemberStatus - from .missing_employee_resolution import MissingEmployeeResolution - from .name_suffix import NameSuffix - from .offset import Offset - from .operation import Operation - from .organization import Organization - from .organization_employer import OrganizationEmployer - from .organization_employer_list_response import OrganizationEmployerListResponse - from .organization_hris_provider import OrganizationHrisProvider - from .organization_hris_providers_response import OrganizationHrisProvidersResponse - from .organization_type import OrganizationType - from .organizations_list_response import OrganizationsListResponse - from .page import Page - from .pagination import Pagination - from .payroll_access_setup_status import PayrollAccessSetupStatus - from .payroll_access_setup_status_response import PayrollAccessSetupStatusResponse - from .payroll_deduction_statement import PayrollDeductionStatement - from .payroll_integration_email import PayrollIntegrationEmail - from .payroll_integration_email_response import PayrollIntegrationEmailResponse - from .plan import Plan - from .plan_list_response import PlanListResponse - from .plan_year_enrollment import PlanYearEnrollment - from .plan_year_enrollment_election_status import PlanYearEnrollmentElectionStatus - from .plan_year_enrollment_list_response import PlanYearEnrollmentListResponse - from .plan_year_enrollment_policy_status import PlanYearEnrollmentPolicyStatus - from .preferred_language import PreferredLanguage - from .product_code import ProductCode - from .qualifying_life_event_status import QualifyingLifeEventStatus - from .qualifying_life_event_type import QualifyingLifeEventType - from .reissue_enrollment import ReissueEnrollment - from .reissue_enrollment_response import ReissueEnrollmentResponse - from .relationship import Relationship - from .remaining_employee_action import RemainingEmployeeAction - from .request_id import RequestId - from .resource_id import ResourceId - from .resource_type import ResourceType - from .search import Search - from .sex_at_birth import SexAtBirth - from .state import State - from .status import Status - from .tax_classification import TaxClassification - from .termination_rule import TerminationRule - from .update_employer_address_input import UpdateEmployerAddressInput - from .waiting_period import WaitingPeriod - from .webhook_delivery import WebhookDelivery - from .webhook_event import WebhookEvent - from .webhook_event_list_response import WebhookEventListResponse - from .webhook_event_response import WebhookEventResponse -_dynamic_imports: typing.Dict[str, str] = { - "AccessMethod": ".access_method", - "AccessTokenResponse": ".access_token_response", - "AdditionalAccessMethod": ".additional_access_method", - "Address": ".address", - "AddressRequest": ".address_request", - "BenefitFamily": ".benefit_family", - "BenefitFamilyParamItem": ".benefit_family_param_item", - "BenefitLifecycleStageItem": ".benefit_lifecycle_stage_item", - "BenefitPlanNetwork": ".benefit_plan_network", - "BenefitPlanYearDetail": ".benefit_plan_year_detail", - "BenefitPlanYearId": ".benefit_plan_year_id", - "BenefitPlanYearList": ".benefit_plan_year_list", - "BenefitStatus": ".benefit_status", - "BenefitSummary": ".benefit_summary", - "BoundEntity": ".bound_entity", - "BoundEntityResponse": ".bound_entity_response", - "BoundEntityType": ".bound_entity_type", - "CardType": ".card_type", - "Category": ".category", - "CensusSyncDetailResponse": ".census_sync_detail_response", - "CensusSyncEmployeeAddressRequest": ".census_sync_employee_address_request", - "CensusSyncEmployeeRequest": ".census_sync_employee_request", - "CensusSyncResponse": ".census_sync_response", - "ClassificationCorrectionSource": ".classification_correction_source", - "CompensationType": ".compensation_type", - "ContributionClassCompensation": ".contribution_class_compensation", - "ContributionClassEmployment": ".contribution_class_employment", - "ContributionClassLocation": ".contribution_class_location", - "ContributionRange": ".contribution_range", - "ContributionStrategy": ".contribution_strategy", - "ContributionTier": ".contribution_tier", - "CoverageTier": ".coverage_tier", - "CreateOrganizationRequestType": ".create_organization_request_type", - "CreatedAfter": ".created_after", - "CreatedBefore": ".created_before", - "DeductionDetail": ".deduction_detail", - "DeductionFrequency": ".deduction_frequency", - "DigitalBenefitCard": ".digital_benefit_card", - "ElectionStatusItem": ".election_status_item", - "EligibilityPolicy": ".eligibility_policy", - "EligibilityPolicyRule": ".eligibility_policy_rule", - "EligibilityRuleCompensationType": ".eligibility_rule_compensation_type", - "EligibilityRuleEmployeeClass": ".eligibility_rule_employee_class", - "EligibleGeographicalLocation": ".eligible_geographical_location", - "EligibleGeographicalLocationType": ".eligible_geographical_location_type", - "Employee": ".employee", - "EmployeeAddress": ".employee_address", - "EmployeeAddressInput": ".employee_address_input", - "EmployeeClass": ".employee_class", - "EmployeeId": ".employee_id", - "EmployeeListResponse": ".employee_list_response", - "EmployeeResponse": ".employee_response", - "EmployeeSearch": ".employee_search", - "EmployeeStatus": ".employee_status", - "Employer": ".employer", - "EmployerAddress": ".employer_address", - "EmployerAddressInput": ".employer_address_input", - "EmployerBenefitLifecycleStage": ".employer_benefit_lifecycle_stage", - "EmployerBenefitPlanYearResponse": ".employer_benefit_plan_year_response", - "EmployerBenefitPlanYearsListResponse": ".employer_benefit_plan_years_list_response", - "EmployerContact": ".employer_contact", - "EmployerHris": ".employer_hris", - "EmployerHrisResponse": ".employer_hris_response", - "EmployerHrisStatus": ".employer_hris_status", - "EmployerId": ".employer_id", - "EmployerInvoicePdf": ".employer_invoice_pdf", - "EmployerInvoicePdfResponse": ".employer_invoice_pdf_response", - "EmployerInvoicesListResponse": ".employer_invoices_list_response", - "EmployerPayrollDeductionStatementListResponse": ".employer_payroll_deduction_statement_list_response", - "EmployerResponse": ".employer_response", - "EmployerSearch": ".employer_search", - "EmployerSettings": ".employer_settings", - "EmployerSettingsResponse": ".employer_settings_response", - "Enrollment": ".enrollment", - "EnrollmentDetail": ".enrollment_detail", - "EnrollmentId": ".enrollment_id", - "EnrollmentListResponse": ".enrollment_list_response", - "EnrollmentRate": ".enrollment_rate", - "EnrollmentRateSummary": ".enrollment_rate_summary", - "EnrollmentResponse": ".enrollment_response", - "EnrollmentStatus": ".enrollment_status", - "ErrorResponse": ".error_response", - "EventId": ".event_id", - "EventName": ".event_name", - "Gender": ".gender", - "GrantType": ".grant_type", - "Group": ".group", - "GroupId": ".group_id", - "GroupListResponse": ".group_list_response", - "GroupMemberSyncDetailResponse": ".group_member_sync_detail_response", - "GroupMemberSyncFailure": ".group_member_sync_failure", - "GroupMemberSyncMemberRequest": ".group_member_sync_member_request", - "GroupMemberSyncRequestDetail": ".group_member_sync_request_detail", - "GroupMemberSyncRequestDetailResponse": ".group_member_sync_request_detail_response", - "GroupMemberSyncResponse": ".group_member_sync_response", - "GroupMemberSyncResults": ".group_member_sync_results", - "GroupResponse": ".group_response", - "HouseholdMember": ".household_member", - "HouseholdMemberRelationship": ".household_member_relationship", - "HouseholdMemberType": ".household_member_type", - "HouseholdMembersResponse": ".household_members_response", - "HrisConnection": ".hris_connection", - "HrisStatusItem": ".hris_status_item", - "IchraContributionClass": ".ichra_contribution_class", - "IdpProvider": ".idp_provider", - "IncludeCancelled": ".include_cancelled", - "Invoice": ".invoice", - "InvoiceId": ".invoice_id", - "InvoicesCursorPagination": ".invoices_cursor_pagination", - "Limit": ".limit", - "ListWebhookEventDeliveriesResponse": ".list_webhook_event_deliveries_response", - "MaritalStatus": ".marital_status", - "Member": ".member", - "MemberAddress": ".member_address", - "MemberDependentsResponse": ".member_dependents_response", - "MemberDigitalBenefitCardsResponse": ".member_digital_benefit_cards_response", - "MemberEmploymentsResponse": ".member_employments_response", - "MemberEnrollment": ".member_enrollment", - "MemberEnrollmentsResponse": ".member_enrollments_response", - "MemberId": ".member_id", - "MemberLegalDependent": ".member_legal_dependent", - "MemberListItem": ".member_list_item", - "MemberListResponse": ".member_list_response", - "MemberQualifyingLifeEvent": ".member_qualifying_life_event", - "MemberQualifyingLifeEventListResponse": ".member_qualifying_life_event_list_response", - "MemberResponse": ".member_response", - "MemberSearch": ".member_search", - "MemberStatus": ".member_status", - "MissingEmployeeResolution": ".missing_employee_resolution", - "NameSuffix": ".name_suffix", - "Offset": ".offset", - "Operation": ".operation", - "Organization": ".organization", - "OrganizationEmployer": ".organization_employer", - "OrganizationEmployerListResponse": ".organization_employer_list_response", - "OrganizationHrisProvider": ".organization_hris_provider", - "OrganizationHrisProvidersResponse": ".organization_hris_providers_response", - "OrganizationType": ".organization_type", - "OrganizationsListResponse": ".organizations_list_response", - "Page": ".page", - "Pagination": ".pagination", - "PayrollAccessSetupStatus": ".payroll_access_setup_status", - "PayrollAccessSetupStatusResponse": ".payroll_access_setup_status_response", - "PayrollDeductionStatement": ".payroll_deduction_statement", - "PayrollIntegrationEmail": ".payroll_integration_email", - "PayrollIntegrationEmailResponse": ".payroll_integration_email_response", - "Plan": ".plan", - "PlanListResponse": ".plan_list_response", - "PlanYearEnrollment": ".plan_year_enrollment", - "PlanYearEnrollmentElectionStatus": ".plan_year_enrollment_election_status", - "PlanYearEnrollmentListResponse": ".plan_year_enrollment_list_response", - "PlanYearEnrollmentPolicyStatus": ".plan_year_enrollment_policy_status", - "PreferredLanguage": ".preferred_language", - "ProductCode": ".product_code", - "QualifyingLifeEventStatus": ".qualifying_life_event_status", - "QualifyingLifeEventType": ".qualifying_life_event_type", - "ReissueEnrollment": ".reissue_enrollment", - "ReissueEnrollmentResponse": ".reissue_enrollment_response", - "Relationship": ".relationship", - "RemainingEmployeeAction": ".remaining_employee_action", - "RequestId": ".request_id", - "ResourceId": ".resource_id", - "ResourceType": ".resource_type", - "Search": ".search", - "SexAtBirth": ".sex_at_birth", - "State": ".state", - "Status": ".status", - "TaxClassification": ".tax_classification", - "TerminationRule": ".termination_rule", - "UpdateEmployerAddressInput": ".update_employer_address_input", - "WaitingPeriod": ".waiting_period", - "WebhookDelivery": ".webhook_delivery", - "WebhookEvent": ".webhook_event", - "WebhookEventListResponse": ".webhook_event_list_response", - "WebhookEventResponse": ".webhook_event_response", -} - - -def __getattr__(attr_name: str) -> typing.Any: - module_name = _dynamic_imports.get(attr_name) - if module_name is None: - raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") - try: - module = import_module(module_name, __package__) - if module_name == f".{attr_name}": - return module - else: - return getattr(module, attr_name) - except ImportError as e: - raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e - except AttributeError as e: - raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e - - -def __dir__(): - lazy_attrs = list(_dynamic_imports.keys()) - return sorted(lazy_attrs) - - -__all__ = [ - "AccessMethod", - "AccessTokenResponse", - "AdditionalAccessMethod", - "Address", - "AddressRequest", - "BenefitFamily", - "BenefitFamilyParamItem", - "BenefitLifecycleStageItem", - "BenefitPlanNetwork", - "BenefitPlanYearDetail", - "BenefitPlanYearId", - "BenefitPlanYearList", - "BenefitStatus", - "BenefitSummary", - "BoundEntity", - "BoundEntityResponse", - "BoundEntityType", - "CardType", - "Category", - "CensusSyncDetailResponse", - "CensusSyncEmployeeAddressRequest", - "CensusSyncEmployeeRequest", - "CensusSyncResponse", - "ClassificationCorrectionSource", - "CompensationType", - "ContributionClassCompensation", - "ContributionClassEmployment", - "ContributionClassLocation", - "ContributionRange", - "ContributionStrategy", - "ContributionTier", - "CoverageTier", - "CreateOrganizationRequestType", - "CreatedAfter", - "CreatedBefore", - "DeductionDetail", - "DeductionFrequency", - "DigitalBenefitCard", - "ElectionStatusItem", - "EligibilityPolicy", - "EligibilityPolicyRule", - "EligibilityRuleCompensationType", - "EligibilityRuleEmployeeClass", - "EligibleGeographicalLocation", - "EligibleGeographicalLocationType", - "Employee", - "EmployeeAddress", - "EmployeeAddressInput", - "EmployeeClass", - "EmployeeId", - "EmployeeListResponse", - "EmployeeResponse", - "EmployeeSearch", - "EmployeeStatus", - "Employer", - "EmployerAddress", - "EmployerAddressInput", - "EmployerBenefitLifecycleStage", - "EmployerBenefitPlanYearResponse", - "EmployerBenefitPlanYearsListResponse", - "EmployerContact", - "EmployerHris", - "EmployerHrisResponse", - "EmployerHrisStatus", - "EmployerId", - "EmployerInvoicePdf", - "EmployerInvoicePdfResponse", - "EmployerInvoicesListResponse", - "EmployerPayrollDeductionStatementListResponse", - "EmployerResponse", - "EmployerSearch", - "EmployerSettings", - "EmployerSettingsResponse", - "Enrollment", - "EnrollmentDetail", - "EnrollmentId", - "EnrollmentListResponse", - "EnrollmentRate", - "EnrollmentRateSummary", - "EnrollmentResponse", - "EnrollmentStatus", - "ErrorResponse", - "EventId", - "EventName", - "Gender", - "GrantType", - "Group", - "GroupId", - "GroupListResponse", - "GroupMemberSyncDetailResponse", - "GroupMemberSyncFailure", - "GroupMemberSyncMemberRequest", - "GroupMemberSyncRequestDetail", - "GroupMemberSyncRequestDetailResponse", - "GroupMemberSyncResponse", - "GroupMemberSyncResults", - "GroupResponse", - "HouseholdMember", - "HouseholdMemberRelationship", - "HouseholdMemberType", - "HouseholdMembersResponse", - "HrisConnection", - "HrisStatusItem", - "IchraContributionClass", - "IdpProvider", - "IncludeCancelled", - "Invoice", - "InvoiceId", - "InvoicesCursorPagination", - "Limit", - "ListWebhookEventDeliveriesResponse", - "MaritalStatus", - "Member", - "MemberAddress", - "MemberDependentsResponse", - "MemberDigitalBenefitCardsResponse", - "MemberEmploymentsResponse", - "MemberEnrollment", - "MemberEnrollmentsResponse", - "MemberId", - "MemberLegalDependent", - "MemberListItem", - "MemberListResponse", - "MemberQualifyingLifeEvent", - "MemberQualifyingLifeEventListResponse", - "MemberResponse", - "MemberSearch", - "MemberStatus", - "MissingEmployeeResolution", - "NameSuffix", - "Offset", - "Operation", - "Organization", - "OrganizationEmployer", - "OrganizationEmployerListResponse", - "OrganizationHrisProvider", - "OrganizationHrisProvidersResponse", - "OrganizationType", - "OrganizationsListResponse", - "Page", - "Pagination", - "PayrollAccessSetupStatus", - "PayrollAccessSetupStatusResponse", - "PayrollDeductionStatement", - "PayrollIntegrationEmail", - "PayrollIntegrationEmailResponse", - "Plan", - "PlanListResponse", - "PlanYearEnrollment", - "PlanYearEnrollmentElectionStatus", - "PlanYearEnrollmentListResponse", - "PlanYearEnrollmentPolicyStatus", - "PreferredLanguage", - "ProductCode", - "QualifyingLifeEventStatus", - "QualifyingLifeEventType", - "ReissueEnrollment", - "ReissueEnrollmentResponse", - "Relationship", - "RemainingEmployeeAction", - "RequestId", - "ResourceId", - "ResourceType", - "Search", - "SexAtBirth", - "State", - "Status", - "TaxClassification", - "TerminationRule", - "UpdateEmployerAddressInput", - "WaitingPeriod", - "WebhookDelivery", - "WebhookEvent", - "WebhookEventListResponse", - "WebhookEventResponse", -] +from .group import Group as Group +from .employee import Employee as Employee +from .employer import Employer as Employer +from .enrollment import Enrollment as Enrollment +from .pagination import Pagination as Pagination +from .webhook_event import WebhookEvent as WebhookEvent +from .employee_class import EmployeeClass as EmployeeClass +from .group_response import GroupResponse as GroupResponse +from .plan_list_params import PlanListParams as PlanListParams +from .employer_response import EmployerResponse as EmployerResponse +from .enrollment_status import EnrollmentStatus as EnrollmentStatus +from .group_list_params import GroupListParams as GroupListParams +from .member_list_params import MemberListParams as MemberListParams +from .plan_list_response import PlanListResponse as PlanListResponse +from .group_create_params import GroupCreateParams as GroupCreateParams +from .group_update_params import GroupUpdateParams as GroupUpdateParams +from .employer_list_params import EmployerListParams as EmployerListParams +from .member_list_response import MemberListResponse as MemberListResponse +from .employee_update_params import EmployeeUpdateParams as EmployeeUpdateParams +from .employer_create_params import EmployerCreateParams as EmployerCreateParams +from .employer_list_response import EmployerListResponse as EmployerListResponse +from .employer_update_params import EmployerUpdateParams as EmployerUpdateParams +from .employee_update_response import EmployeeUpdateResponse as EmployeeUpdateResponse +from .member_retrieve_response import MemberRetrieveResponse as MemberRetrieveResponse +from .enrollment_reissue_params import EnrollmentReissueParams as EnrollmentReissueParams +from .webhook_event_list_params import WebhookEventListParams as WebhookEventListParams +from .employee_retrieve_response import EmployeeRetrieveResponse as EmployeeRetrieveResponse +from .organization_create_params import OrganizationCreateParams as OrganizationCreateParams +from .organization_list_response import OrganizationListResponse as OrganizationListResponse +from .enrollment_reissue_response import EnrollmentReissueResponse as EnrollmentReissueResponse +from .enrollment_terminate_params import EnrollmentTerminateParams as EnrollmentTerminateParams +from .enrollment_retrieve_response import EnrollmentRetrieveResponse as EnrollmentRetrieveResponse +from .organization_create_response import OrganizationCreateResponse as OrganizationCreateResponse +from .employer_list_invoices_params import EmployerListInvoicesParams as EmployerListInvoicesParams +from .member_list_id_cards_response import MemberListIDCardsResponse as MemberListIDCardsResponse +from .auth_issue_access_token_params import AuthIssueAccessTokenParams as AuthIssueAccessTokenParams +from .employer_list_employees_params import EmployerListEmployeesParams as EmployerListEmployeesParams +from .employer_list_invoices_response import EmployerListInvoicesResponse as EmployerListInvoicesResponse +from .employer_retrieve_hris_response import EmployerRetrieveHRISResponse as EmployerRetrieveHRISResponse +from .employer_update_settings_params import EmployerUpdateSettingsParams as EmployerUpdateSettingsParams +from .member_list_dependents_response import MemberListDependentsResponse as MemberListDependentsResponse +from .webhook_event_retrieve_response import WebhookEventRetrieveResponse as WebhookEventRetrieveResponse +from .auth_issue_access_token_response import AuthIssueAccessTokenResponse as AuthIssueAccessTokenResponse +from .employee_list_enrollments_params import EmployeeListEnrollmentsParams as EmployeeListEnrollmentsParams +from .member_list_employments_response import MemberListEmploymentsResponse as MemberListEmploymentsResponse +from .member_list_enrollments_response import MemberListEnrollmentsResponse as MemberListEnrollmentsResponse +from .employer_update_settings_response import EmployerUpdateSettingsResponse as EmployerUpdateSettingsResponse +from .employer_submit_census_sync_params import EmployerSubmitCensusSyncParams as EmployerSubmitCensusSyncParams +from .member_retrieve_household_response import MemberRetrieveHouseholdResponse as MemberRetrieveHouseholdResponse +from .employer_submit_census_sync_response import EmployerSubmitCensusSyncResponse as EmployerSubmitCensusSyncResponse +from .employer_list_hris_providers_response import ( + EmployerListHRISProvidersResponse as EmployerListHRISProvidersResponse, +) +from .employer_retrieve_invoice_pdf_response import ( + EmployerRetrieveInvoicePdfResponse as EmployerRetrieveInvoicePdfResponse, +) +from .webhook_event_list_deliveries_response import ( + WebhookEventListDeliveriesResponse as WebhookEventListDeliveriesResponse, +) +from .employer_list_benefit_plan_years_response import ( + EmployerListBenefitPlanYearsResponse as EmployerListBenefitPlanYearsResponse, +) +from .member_list_qualifying_life_events_params import ( + MemberListQualifyingLifeEventsParams as MemberListQualifyingLifeEventsParams, +) +from .employer_submit_payroll_access_setup_params import ( + EmployerSubmitPayrollAccessSetupParams as EmployerSubmitPayrollAccessSetupParams, +) +from .member_list_qualifying_life_events_response import ( + MemberListQualifyingLifeEventsResponse as MemberListQualifyingLifeEventsResponse, +) +from .employer_retrieve_benefit_plan_year_response import ( + EmployerRetrieveBenefitPlanYearResponse as EmployerRetrieveBenefitPlanYearResponse, +) +from .employer_submit_payroll_access_setup_response import ( + EmployerSubmitPayrollAccessSetupResponse as EmployerSubmitPayrollAccessSetupResponse, +) +from .employer_retrieve_payroll_access_setup_response import ( + EmployerRetrievePayrollAccessSetupResponse as EmployerRetrievePayrollAccessSetupResponse, +) +from .employer_list_payroll_deduction_statements_params import ( + EmployerListPayrollDeductionStatementsParams as EmployerListPayrollDeductionStatementsParams, +) +from .employer_ensure_payroll_integration_email_response import ( + EmployerEnsurePayrollIntegrationEmailResponse as EmployerEnsurePayrollIntegrationEmailResponse, +) +from .employer_list_benefit_plan_year_enrollments_params import ( + EmployerListBenefitPlanYearEnrollmentsParams as EmployerListBenefitPlanYearEnrollmentsParams, +) +from .employer_list_payroll_deduction_statements_response import ( + EmployerListPayrollDeductionStatementsResponse as EmployerListPayrollDeductionStatementsResponse, +) +from .employer_list_benefit_plan_year_enrollments_response import ( + EmployerListBenefitPlanYearEnrollmentsResponse as EmployerListBenefitPlanYearEnrollmentsResponse, +) diff --git a/src/vitable_connect/types/access_method.py b/src/vitable_connect/types/access_method.py deleted file mode 100644 index ebf9a97..0000000 --- a/src/vitable_connect/types/access_method.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -AccessMethod = typing.Union[typing.Literal["SELF_SETUP", "NEEDS_HELP"], typing.Any] diff --git a/src/vitable_connect/types/access_token_response.py b/src/vitable_connect/types/access_token_response.py deleted file mode 100644 index e6d81a5..0000000 --- a/src/vitable_connect/types/access_token_response.py +++ /dev/null @@ -1,38 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .bound_entity_response import BoundEntityResponse - - -class AccessTokenResponse(UniversalBaseModel): - access_token: str = pydantic.Field() - """ - The issued access token (vit_at_*) - """ - - token_type: str = pydantic.Field() - """ - Token type, always 'Bearer' - """ - - expires_in: int = pydantic.Field() - """ - Token lifetime in seconds - """ - - bound_entity: typing.Optional[BoundEntityResponse] = pydantic.Field(default=None) - """ - Entity the token is bound to, if any - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/additional_access_method.py b/src/vitable_connect/types/additional_access_method.py deleted file mode 100644 index 301d466..0000000 --- a/src/vitable_connect/types/additional_access_method.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -AdditionalAccessMethod = typing.Union[typing.Literal["SELF_SETUP", "NEEDS_HELP"], typing.Any] diff --git a/src/vitable_connect/types/address.py b/src/vitable_connect/types/address.py deleted file mode 100644 index d18fdc0..0000000 --- a/src/vitable_connect/types/address.py +++ /dev/null @@ -1,60 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata - - -class Address(UniversalBaseModel): - """ - Shared read serializer for a postal address on public API responses. - - One definition for the address block every public resource emits (employer, employee, …), so the - 5-field shape isn't hand-rolled per endpoint. Read-only: it renders an already-built address - value object (e.g. ``AddressDVO``) whose attributes map 1:1 to these fields. - """ - - address_line1: typing_extensions.Annotated[ - str, - FieldMetadata(alias="address_line_1"), - pydantic.Field(alias="address_line_1", description="Primary street address."), - ] - """ - Primary street address. - """ - - address_line2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="address_line_2"), - pydantic.Field(alias="address_line_2", description="Secondary street address (apt, suite, etc.)."), - ] = None - """ - Secondary street address (apt, suite, etc.). - """ - - city: str = pydantic.Field() - """ - City name. - """ - - state: str = pydantic.Field() - """ - Two-letter state code (e.g. `CA`, `NY`). - """ - - zipcode: str = pydantic.Field() - """ - ZIP code (5 or 9 digit). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/address_request.py b/src/vitable_connect/types/address_request.py deleted file mode 100644 index 9bcb817..0000000 --- a/src/vitable_connect/types/address_request.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata - - -class AddressRequest(UniversalBaseModel): - address_line1: typing_extensions.Annotated[ - str, - FieldMetadata(alias="address_line_1"), - pydantic.Field(alias="address_line_1", description="Street address, including house or building number."), - ] - """ - Street address, including house or building number. - """ - - address_line2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="address_line_2"), - pydantic.Field(alias="address_line_2", description="Apartment, suite, unit, or floor. Omit if not applicable."), - ] = None - """ - Apartment, suite, unit, or floor. Omit if not applicable. - """ - - city: str = pydantic.Field() - """ - City or town name. - """ - - state: str = pydantic.Field() - """ - Two-letter USPS state or territory code, e.g. `PA`. - """ - - zipcode: str = pydantic.Field() - """ - US postal code, either five digits or ZIP+4. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/auth_issue_access_token_params.py b/src/vitable_connect/types/auth_issue_access_token_params.py new file mode 100644 index 0000000..1b50f6d --- /dev/null +++ b/src/vitable_connect/types/auth_issue_access_token_params.py @@ -0,0 +1,32 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["AuthIssueAccessTokenParams", "BoundEntity"] + + +class AuthIssueAccessTokenParams(TypedDict, total=False): + grant_type: Required[Literal["client_credentials"]] + """- `client_credentials` - client_credentials""" + + bound_entity: Optional[BoundEntity] + """Optional entity to bind the token to for scoped access""" + + +class BoundEntity(TypedDict, total=False): + """Optional entity to bind the token to for scoped access""" + + id: Required[str] + """ + Prefixed entity ID to bind the token to (empr*\\** for employer, empl*\\** for + employee) + """ + + type: Required[Literal["employer", "employee"]] + """ + - `employer` - employer + - `employee` - employee + """ diff --git a/src/vitable_connect/types/auth_issue_access_token_response.py b/src/vitable_connect/types/auth_issue_access_token_response.py new file mode 100644 index 0000000..f15594d --- /dev/null +++ b/src/vitable_connect/types/auth_issue_access_token_response.py @@ -0,0 +1,35 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["AuthIssueAccessTokenResponse", "BoundEntity"] + + +class BoundEntity(BaseModel): + """Entity the token is bound to, if any""" + + id: str + """Prefixed entity ID the token is bound to (empr*\\** or empl*\\**)""" + + type: Literal["employer", "employee"] + """ + - `employer` - employer + - `employee` - employee + """ + + +class AuthIssueAccessTokenResponse(BaseModel): + access_token: str + """The issued access token (vit*at*\\**)""" + + expires_in: int + """Token lifetime in seconds""" + + token_type: str + """Token type, always 'Bearer'""" + + bound_entity: Optional[BoundEntity] = None + """Entity the token is bound to, if any""" diff --git a/src/vitable_connect/types/benefit_family.py b/src/vitable_connect/types/benefit_family.py deleted file mode 100644 index f5a2fd1..0000000 --- a/src/vitable_connect/types/benefit_family.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -BenefitFamily = typing.Union[typing.Literal["mec", "mvp", "ichra", "vpc", "dental", "vision"], typing.Any] diff --git a/src/vitable_connect/types/benefit_family_param_item.py b/src/vitable_connect/types/benefit_family_param_item.py deleted file mode 100644 index 46bffb4..0000000 --- a/src/vitable_connect/types/benefit_family_param_item.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -BenefitFamilyParamItem = typing.Union[typing.Literal["mec", "mvp", "ichra", "vpc", "dental", "vision"], typing.Any] diff --git a/src/vitable_connect/types/benefit_lifecycle_stage_item.py b/src/vitable_connect/types/benefit_lifecycle_stage_item.py deleted file mode 100644 index 3d82b93..0000000 --- a/src/vitable_connect/types/benefit_lifecycle_stage_item.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -BenefitLifecycleStageItem = typing.Union[ - typing.Literal["open_enrollment", "renewal", "active", "onboarding", "cancelled"], typing.Any -] diff --git a/src/vitable_connect/types/benefit_plan_network.py b/src/vitable_connect/types/benefit_plan_network.py deleted file mode 100644 index 7859f4c..0000000 --- a/src/vitable_connect/types/benefit_plan_network.py +++ /dev/null @@ -1,73 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .address import Address - - -class BenefitPlanNetwork(UniversalBaseModel): - id: typing.Optional[str] = pydantic.Field(default=None) - """ - Unique identifier for the network. - """ - - name: str = pydantic.Field() - """ - Name of the network - """ - - logo: typing.Optional[str] = pydantic.Field(default=None) - """ - URL of the network's logo, or `null` if none is on file. - """ - - address: Address = pydantic.Field() - """ - Postal address of the provider network. - """ - - edi: typing.Optional[str] = pydantic.Field(default=None) - """ - Network's EDI - """ - - website: str = pydantic.Field() - """ - Website of the network - """ - - phone: str = pydantic.Field() - """ - General contact phone number for the network. - """ - - member_website: typing.Optional[str] = pydantic.Field(default=None) - """ - Website for members - """ - - member_phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Phone number members should call for support. - """ - - provider_website: typing.Optional[str] = pydantic.Field(default=None) - """ - Website for providers - """ - - provider_phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Phone number providers should call for claims and eligibility. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/benefit_plan_year_detail.py b/src/vitable_connect/types/benefit_plan_year_detail.py deleted file mode 100644 index d7a7e08..0000000 --- a/src/vitable_connect/types/benefit_plan_year_detail.py +++ /dev/null @@ -1,144 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .benefit_family import BenefitFamily -from .benefit_status import BenefitStatus -from .contribution_range import ContributionRange -from .contribution_strategy import ContributionStrategy -from .eligibility_policy import EligibilityPolicy -from .enrollment_rate import EnrollmentRate - - -class BenefitPlanYearDetail(UniversalBaseModel): - """ - One plan year, detail view. - - Standalone (no shared base) so the exact detail payload is readable in one place; the list - serializer is a separate class even where fields overlap. Detail carries the SPD link and omits - the list-only ``is_current`` flag. - """ - - benefit_plan_year_id: str = pydantic.Field() - """ - Prefixed plan-year identifier (`plyr_*`). - """ - - benefit_id: str = pydantic.Field() - """ - Prefixed benefit identifier (`bprd_*`). - """ - - year: int = pydantic.Field() - """ - Calendar coverage year. - """ - - family: BenefitFamily = pydantic.Field() - """ - Benefit family. - - * `mec` - Mec - * `mvp` - Mvp - * `ichra` - Ichra - * `vpc` - Vpc - * `dental` - Dental - * `vision` - Vision - """ - - product_name: str = pydantic.Field() - """ - Benefit/product display name. - """ - - carrier: typing.Optional[str] = pydantic.Field(default=None) - """ - Carrier name, or null (e.g. ICHRA). - """ - - network_names: typing.List[str] = pydantic.Field() - """ - Displayed networks: ["multi"] for ICHRA, otherwise the plan year's distinct network names. - """ - - premium_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Monthly premium in cents; only for an ICHRA benefit with effective coverage. - """ - - employee_contribution: typing.Optional[ContributionRange] = pydantic.Field(default=None) - """ - Employee contribution range. - """ - - employer_contribution: typing.Optional[ContributionRange] = pydantic.Field(default=None) - """ - Employer contribution range. - """ - - status: BenefitStatus = pydantic.Field() - """ - `active`/`upcoming`/`inactive`. - - * `active` - Active - * `upcoming` - Upcoming - * `open_enrollment` - Open Enrollment - * `inactive` - Inactive - """ - - coverage_start: dt.date = pydantic.Field() - """ - Coverage start. - """ - - coverage_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Coverage end. - """ - - open_enrollment_start: dt.date = pydantic.Field() - """ - Open-enrollment start. - """ - - open_enrollment_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Open-enrollment end. - """ - - offered_states: typing.List[str] = pydantic.Field() - """ - Distinct offered state codes. - """ - - enrollment_rate: EnrollmentRate = pydantic.Field() - """ - Enrolled/eligible rate for this plan year. - """ - - spd_file_url: typing.Optional[str] = pydantic.Field(default=None) - """ - Summary Plan Description (SPD) link, or null. - """ - - contribution_strategy: ContributionStrategy = pydantic.Field() - """ - How the plan year prices contributions (ICHRA classes or coverage tiers). - """ - - eligibility_policy: typing.Optional[EligibilityPolicy] = pydantic.Field(default=None) - """ - The plan year's active eligibility policy, or null. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/benefit_plan_year_id.py b/src/vitable_connect/types/benefit_plan_year_id.py deleted file mode 100644 index 163bcfa..0000000 --- a/src/vitable_connect/types/benefit_plan_year_id.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -BenefitPlanYearId = str -""" -Unique benefit-plan-year identifier (plyr_*). -""" diff --git a/src/vitable_connect/types/benefit_plan_year_list.py b/src/vitable_connect/types/benefit_plan_year_list.py deleted file mode 100644 index d7ab87e..0000000 --- a/src/vitable_connect/types/benefit_plan_year_list.py +++ /dev/null @@ -1,131 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .benefit_family import BenefitFamily -from .benefit_status import BenefitStatus -from .contribution_range import ContributionRange -from .enrollment_rate import EnrollmentRate - - -class BenefitPlanYearList(UniversalBaseModel): - """ - One plan year, list view. - - Standalone (no shared base) so the exact list payload is readable in one place; the detail - serializer is a separate class even where fields overlap. - """ - - benefit_plan_year_id: str = pydantic.Field() - """ - Prefixed plan-year identifier (`plyr_*`). - """ - - benefit_id: str = pydantic.Field() - """ - Prefixed benefit identifier (`bprd_*`). - """ - - year: int = pydantic.Field() - """ - Calendar coverage year. - """ - - family: BenefitFamily = pydantic.Field() - """ - Benefit family. - - * `mec` - Mec - * `mvp` - Mvp - * `ichra` - Ichra - * `vpc` - Vpc - * `dental` - Dental - * `vision` - Vision - """ - - product_name: str = pydantic.Field() - """ - Benefit/product display name. - """ - - carrier: typing.Optional[str] = pydantic.Field(default=None) - """ - Carrier name, or null (e.g. ICHRA). - """ - - network_names: typing.List[str] = pydantic.Field() - """ - Displayed networks: ["multi"] for ICHRA, otherwise the plan year's distinct network names. - """ - - premium_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Monthly premium in cents; only for an ICHRA benefit with effective coverage. - """ - - employee_contribution: typing.Optional[ContributionRange] = pydantic.Field(default=None) - """ - Employee contribution range. - """ - - employer_contribution: typing.Optional[ContributionRange] = pydantic.Field(default=None) - """ - Employer contribution range. - """ - - status: BenefitStatus = pydantic.Field() - """ - `active`/`upcoming`/`inactive`. - - * `active` - Active - * `upcoming` - Upcoming - * `open_enrollment` - Open Enrollment - * `inactive` - Inactive - """ - - coverage_start: dt.date = pydantic.Field() - """ - Coverage start. - """ - - coverage_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Coverage end. - """ - - open_enrollment_start: dt.date = pydantic.Field() - """ - Open-enrollment start. - """ - - open_enrollment_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Open-enrollment end. - """ - - offered_states: typing.List[str] = pydantic.Field() - """ - Distinct offered state codes. - """ - - is_current: bool = pydantic.Field() - """ - Whether this is the current plan year. - """ - - enrollment_rate: EnrollmentRate = pydantic.Field() - """ - Enrolled/eligible rate for this plan year. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/benefit_status.py b/src/vitable_connect/types/benefit_status.py deleted file mode 100644 index b8a5f9c..0000000 --- a/src/vitable_connect/types/benefit_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -BenefitStatus = typing.Union[typing.Literal["active", "upcoming", "open_enrollment", "inactive"], typing.Any] diff --git a/src/vitable_connect/types/benefit_summary.py b/src/vitable_connect/types/benefit_summary.py deleted file mode 100644 index 25b39b8..0000000 --- a/src/vitable_connect/types/benefit_summary.py +++ /dev/null @@ -1,60 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .category import Category -from .product_code import ProductCode - - -class BenefitSummary(UniversalBaseModel): - id: str = pydantic.Field() - """ - Benefit product ID (bprd_*) - """ - - name: str = pydantic.Field() - """ - Display name of the benefit product - """ - - product_code: ProductCode = pydantic.Field() - """ - Programmatic product identifier - - * `EBA` - Eba Mec - * `VPC` - Vpc Enhanced - * `VPC_CORE` - Vpc Core - * `MEC` - Vpc Mec - * `MEC2` - Mec2 - * `MEC_PLUS` - Mec Plus - * `MVP` - Mvp - * `MVP2` - Mvp2 - * `MVPSL` - Mvpsl - * `MVPSL2` - Mvpsl2 - * `VD` - Dental - * `VV` - Vision - * `ICHRA` - Ichra - * `ICHRA_PREMIUM_PLUS` - Ichra Premium Plus - * `ICHRA_REIMBURSEMENT_ONLY` - Ichra Reimbursement Only - """ - - category: Category = pydantic.Field() - """ - High-level benefit grouping - - * `Medical` - Medical - * `Dental` - Dental - * `Vision` - Vision - * `Hospital` - Hospital - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/bound_entity.py b/src/vitable_connect/types/bound_entity.py deleted file mode 100644 index 8758e94..0000000 --- a/src/vitable_connect/types/bound_entity.py +++ /dev/null @@ -1,31 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .bound_entity_type import BoundEntityType - - -class BoundEntity(UniversalBaseModel): - id: str = pydantic.Field() - """ - Prefixed entity ID to bind the token to (empr_* for employer, empl_* for employee) - """ - - type: BoundEntityType = pydantic.Field() - """ - Type of entity: 'employer' or 'employee' - - * `employer` - employer - * `employee` - employee - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/bound_entity_response.py b/src/vitable_connect/types/bound_entity_response.py deleted file mode 100644 index faae4f8..0000000 --- a/src/vitable_connect/types/bound_entity_response.py +++ /dev/null @@ -1,31 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .bound_entity_type import BoundEntityType - - -class BoundEntityResponse(UniversalBaseModel): - id: str = pydantic.Field() - """ - Prefixed entity ID the token is bound to (empr_* or empl_*) - """ - - type: BoundEntityType = pydantic.Field() - """ - Type of bound entity - - * `employer` - employer - * `employee` - employee - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/bound_entity_type.py b/src/vitable_connect/types/bound_entity_type.py deleted file mode 100644 index 0677fd0..0000000 --- a/src/vitable_connect/types/bound_entity_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -BoundEntityType = typing.Union[typing.Literal["employer", "employee"], typing.Any] diff --git a/src/vitable_connect/types/card_type.py b/src/vitable_connect/types/card_type.py deleted file mode 100644 index 88961f1..0000000 --- a/src/vitable_connect/types/card_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CardType = typing.Union[typing.Literal["medical", "dental", "vision", "rx"], typing.Any] diff --git a/src/vitable_connect/types/category.py b/src/vitable_connect/types/category.py deleted file mode 100644 index 9b03f6a..0000000 --- a/src/vitable_connect/types/category.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -Category = typing.Union[typing.Literal["Medical", "Dental", "Vision", "Hospital"], typing.Any] diff --git a/src/vitable_connect/types/census_sync_detail_response.py b/src/vitable_connect/types/census_sync_detail_response.py deleted file mode 100644 index 9f9c449..0000000 --- a/src/vitable_connect/types/census_sync_detail_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .census_sync_response import CensusSyncResponse - - -class CensusSyncDetailResponse(UniversalBaseModel): - """ - Response containing a single census sync detail resource. - """ - - data: CensusSyncResponse = pydantic.Field() - """ - The census sync request, including its current processing status. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/census_sync_employee_address_request.py b/src/vitable_connect/types/census_sync_employee_address_request.py deleted file mode 100644 index 501c59e..0000000 --- a/src/vitable_connect/types/census_sync_employee_address_request.py +++ /dev/null @@ -1,53 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata -from .state import State - - -class CensusSyncEmployeeAddressRequest(UniversalBaseModel): - address_line1: typing_extensions.Annotated[ - str, - FieldMetadata(alias="address_line_1"), - pydantic.Field(alias="address_line_1", description="Street address, including house or building number."), - ] - """ - Street address, including house or building number. - """ - - address_line2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="address_line_2"), - pydantic.Field(alias="address_line_2", description="Apartment, suite, unit, or floor. Omit if not applicable."), - ] = None - """ - Apartment, suite, unit, or floor. Omit if not applicable. - """ - - city: str = pydantic.Field() - """ - City or town name. - """ - - state: State = pydantic.Field() - """ - US state or territory for the address. - """ - - zipcode: str = pydantic.Field() - """ - US postal code, either five digits or ZIP+4. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/census_sync_employee_request.py b/src/vitable_connect/types/census_sync_employee_request.py deleted file mode 100644 index 16028c4..0000000 --- a/src/vitable_connect/types/census_sync_employee_request.py +++ /dev/null @@ -1,71 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .census_sync_employee_address_request import CensusSyncEmployeeAddressRequest -from .compensation_type import CompensationType -from .employee_class import EmployeeClass - - -class CensusSyncEmployeeRequest(UniversalBaseModel): - reference_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Your own identifier for this employee. Vitable echoes it back on sync results so you can match failures to your records. - """ - - first_name: str = pydantic.Field() - """ - Employee's legal first name. - """ - - last_name: str = pydantic.Field() - """ - Employee's legal last name. - """ - - date_of_birth: dt.date = pydantic.Field() - """ - Employee's date of birth as `YYYY-MM-DD`. Used to match existing members. - """ - - email: str = pydantic.Field() - """ - Employee's work email address. Vitable sends enrollment invitations here. - """ - - phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Phone number - """ - - address: typing.Optional[CensusSyncEmployeeAddressRequest] = pydantic.Field(default=None) - """ - Employee's home address, used to determine plan availability. - """ - - start_date: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Employment start date as `YYYY-MM-DD`. Drives benefit eligibility dates. - """ - - employee_class: typing.Optional[EmployeeClass] = pydantic.Field(default=None) - """ - Employee class used to match the employer's eligibility policy rules. - """ - - compensation_type: typing.Optional[CompensationType] = pydantic.Field(default=None) - """ - Whether the employee is salaried or hourly. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/census_sync_response.py b/src/vitable_connect/types/census_sync_response.py deleted file mode 100644 index c40cd99..0000000 --- a/src/vitable_connect/types/census_sync_response.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class CensusSyncResponse(UniversalBaseModel): - employer_id: str = pydantic.Field() - """ - Employer the census sync was submitted for (`empr_*`). - """ - - accepted_at: dt.datetime = pydantic.Field() - """ - When Vitable accepted the census sync for processing (RFC 3339). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/classification_correction_source.py b/src/vitable_connect/types/classification_correction_source.py deleted file mode 100644 index 59fb1ef..0000000 --- a/src/vitable_connect/types/classification_correction_source.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ClassificationCorrectionSource = typing.Union[typing.Literal["ENTER_NAMES", "EMAIL_LIST"], typing.Any] diff --git a/src/vitable_connect/types/compensation_type.py b/src/vitable_connect/types/compensation_type.py deleted file mode 100644 index 6ed6036..0000000 --- a/src/vitable_connect/types/compensation_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CompensationType = typing.Union[typing.Literal["Salary", "Hourly"], typing.Any] diff --git a/src/vitable_connect/types/contribution_class_compensation.py b/src/vitable_connect/types/contribution_class_compensation.py deleted file mode 100644 index 42e44f0..0000000 --- a/src/vitable_connect/types/contribution_class_compensation.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ContributionClassCompensation = typing.Union[typing.Literal["Unspecified", "Salary", "Hourly"], typing.Any] diff --git a/src/vitable_connect/types/contribution_class_employment.py b/src/vitable_connect/types/contribution_class_employment.py deleted file mode 100644 index 2e87c48..0000000 --- a/src/vitable_connect/types/contribution_class_employment.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ContributionClassEmployment = typing.Union[ - typing.Literal["Unspecified", "Full Time", "Part Time", "Temporary", "Seasonal"], typing.Any -] diff --git a/src/vitable_connect/types/contribution_class_location.py b/src/vitable_connect/types/contribution_class_location.py deleted file mode 100644 index 828d43f..0000000 --- a/src/vitable_connect/types/contribution_class_location.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ContributionClassLocation = typing.Union[typing.Literal["Unspecified", "State"], typing.Any] diff --git a/src/vitable_connect/types/contribution_range.py b/src/vitable_connect/types/contribution_range.py deleted file mode 100644 index c6b52f7..0000000 --- a/src/vitable_connect/types/contribution_range.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class ContributionRange(UniversalBaseModel): - min_cents: int = pydantic.Field() - """ - Lowest per-tier contribution in cents. - """ - - max_cents: int = pydantic.Field() - """ - Highest per-tier contribution in cents. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/contribution_strategy.py b/src/vitable_connect/types/contribution_strategy.py deleted file mode 100644 index eb215c2..0000000 --- a/src/vitable_connect/types/contribution_strategy.py +++ /dev/null @@ -1,34 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .contribution_tier import ContributionTier -from .ichra_contribution_class import IchraContributionClass - - -class ContributionStrategy(UniversalBaseModel): - """ - How the plan year prices contributions: exactly one collection is populated, determined by the - plan year's ``family`` (ICHRA vs tier-priced). - """ - - ichra_contribution_classes: typing.List[IchraContributionClass] = pydantic.Field() - """ - ICHRA contribution classes; empty for tier-priced benefits. - """ - - contribution_tiers: typing.List[ContributionTier] = pydantic.Field() - """ - Coverage tiers and their costs; empty for ICHRA benefits. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/contribution_tier.py b/src/vitable_connect/types/contribution_tier.py deleted file mode 100644 index bd1eaf4..0000000 --- a/src/vitable_connect/types/contribution_tier.py +++ /dev/null @@ -1,70 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .coverage_tier import CoverageTier - - -class ContributionTier(UniversalBaseModel): - """ - One non-ICHRA coverage tier, mirroring the internal configuration ``CompanyBenefitPlanTierCostDTO`` - minus the tier-cost id, ``pepm`` and ``pepm_per_dependent``; ``benefit_plan_id`` is the prefixed - ``bpln_*`` form rather than a raw UUID. - """ - - benefit_plan_id: str = pydantic.Field() - """ - Prefixed benefit-plan identifier (`bpln_*`). - """ - - benefit_plan_name: str = pydantic.Field() - """ - Benefit plan name. - """ - - benefit_plan_tier_name: str = pydantic.Field() - """ - Coverage-tier name. - """ - - coverage_tier: CoverageTier = pydantic.Field() - """ - Coverage-tier code for this tier. - - * `Unspecified` - Unspecified - * `EE` - Ee - * `ES` - Es - * `EC` - Ec - * `EF` - Ef - """ - - cost: int = pydantic.Field() - """ - Monthly employee deduction in cents. - """ - - cost_per_dependent: int = pydantic.Field() - """ - Monthly employee deduction per dependent, in cents. - """ - - dependents_required_in: bool = pydantic.Field() - """ - Whether dependents are required for this tier. - """ - - spouse_required_in: bool = pydantic.Field() - """ - Whether a spouse is required for this tier. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/coverage_tier.py b/src/vitable_connect/types/coverage_tier.py deleted file mode 100644 index 79c2a27..0000000 --- a/src/vitable_connect/types/coverage_tier.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CoverageTier = typing.Union[typing.Literal["Unspecified", "EE", "ES", "EC", "EF"], typing.Any] diff --git a/src/vitable_connect/types/create_organization_request_type.py b/src/vitable_connect/types/create_organization_request_type.py deleted file mode 100644 index 3e1ba02..0000000 --- a/src/vitable_connect/types/create_organization_request_type.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -CreateOrganizationRequestType = typing.Union[ - typing.Literal["BROKERAGE", "TPA", "GENERAL_AGENT", "CHANNEL_PARTNER", "CONSULTING_FIRM", "API_PLATFORM"], - typing.Any, -] diff --git a/src/vitable_connect/types/created_after.py b/src/vitable_connect/types/created_after.py deleted file mode 100644 index 028f5a4..0000000 --- a/src/vitable_connect/types/created_after.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt - -CreatedAfter = dt.datetime diff --git a/src/vitable_connect/types/created_before.py b/src/vitable_connect/types/created_before.py deleted file mode 100644 index f3722df..0000000 --- a/src/vitable_connect/types/created_before.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt - -CreatedBefore = dt.datetime diff --git a/src/vitable_connect/types/deduction_detail.py b/src/vitable_connect/types/deduction_detail.py deleted file mode 100644 index b9276a0..0000000 --- a/src/vitable_connect/types/deduction_detail.py +++ /dev/null @@ -1,64 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .deduction_frequency import DeductionFrequency -from .tax_classification import TaxClassification - - -class DeductionDetail(UniversalBaseModel): - deduction_category: typing.Optional[str] = pydantic.Field(default=None) - """ - Deduction category (reserved for future use) - """ - - deduction_amount_in_cents: int = pydantic.Field() - """ - Employee deduction amount in cents - """ - - tax_classification: TaxClassification = pydantic.Field() - """ - Tax classification: Pre-tax, Post-tax, or Unknown - - * `Unknown` - Unknown - * `Pre-tax` - Pre Tax - * `Post-tax` - Post Tax - """ - - frequency: DeductionFrequency = pydantic.Field() - """ - Deduction frequency - - * `weekly` - Weekly - * `bi_weekly` - Bi Weekly - * `semi_monthly` - Semi Monthly - * `monthly` - Monthly - """ - - benefit_name: str = pydantic.Field() - """ - Name of the benefit plan - """ - - period_start_date: dt.date = pydantic.Field() - """ - Period start date (YYYY-MM-DD) - """ - - period_end_date: dt.date = pydantic.Field() - """ - Period end date (YYYY-MM-DD) - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/deduction_frequency.py b/src/vitable_connect/types/deduction_frequency.py deleted file mode 100644 index b8fe501..0000000 --- a/src/vitable_connect/types/deduction_frequency.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -DeductionFrequency = typing.Union[typing.Literal["weekly", "bi_weekly", "semi_monthly", "monthly"], typing.Any] diff --git a/src/vitable_connect/types/digital_benefit_card.py b/src/vitable_connect/types/digital_benefit_card.py deleted file mode 100644 index f17a1bb..0000000 --- a/src/vitable_connect/types/digital_benefit_card.py +++ /dev/null @@ -1,115 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .benefit_plan_network import BenefitPlanNetwork -from .card_type import CardType -from .product_code import ProductCode - - -class DigitalBenefitCard(UniversalBaseModel): - """ - Wire serializer for :class:`DigitalBenefitCardDTO` (one benefit ID card). - """ - - card_type: CardType = pydantic.Field() - """ - Kind of card: medical, dental, vision, or rx - - * `medical` - medical - * `dental` - dental - * `vision` - vision - * `rx` - rx - """ - - group_id: str = pydantic.Field() - """ - Group number printed on the card (the rx group id for an rx card) - """ - - group_member_id: str = pydantic.Field() - """ - Member id printed on the card (the Ventegra cardholder id for an rx card) - """ - - member_name: str = pydantic.Field() - """ - Name of the member the card is issued to - """ - - employer_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Employer the card's coverage is through; null for an rx card without group info - """ - - plan_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Benefit plan name on the card; null for a consumer-membership rx card - """ - - benefit_code: typing.Optional[ProductCode] = pydantic.Field(default=None) - """ - Benefit code backing the card; null for a consumer-membership rx card, which has no enrollment - - * `EBA` - Eba Mec - * `VPC` - Vpc Enhanced - * `VPC_CORE` - Vpc Core - * `MEC` - Vpc Mec - * `MEC2` - Mec2 - * `MEC_PLUS` - Mec Plus - * `MVP` - Mvp - * `MVP2` - Mvp2 - * `MVPSL` - Mvpsl - * `MVPSL2` - Mvpsl2 - * `VD` - Dental - * `VV` - Vision - * `ICHRA` - Ichra - * `ICHRA_PREMIUM_PLUS` - Ichra Premium Plus - * `ICHRA_REIMBURSEMENT_ONLY` - Ichra Reimbursement Only - """ - - network: typing.Optional[BenefitPlanNetwork] = pydantic.Field(default=None) - """ - Provider network shown on the card; null for an rx card - """ - - claims_payer_display_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Claims payer shown on the card - """ - - carrier_phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Carrier phone number on the card; null for an rx card - """ - - carrier_website: typing.Optional[str] = pydantic.Field(default=None) - """ - Carrier website on the card; null for an rx card - """ - - plan_disclaimer: typing.Optional[str] = pydantic.Field(default=None) - """ - Plan-specific disclaimer text; null for an rx card - """ - - general_disclaimer: typing.Optional[str] = pydantic.Field(default=None) - """ - General disclaimer text; null for an rx card - """ - - nsa_table: typing.List[typing.List[str]] = pydantic.Field() - """ - No Surprises Act cost-sharing table rendered on the card; empty for an rx card - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/election_status_item.py b/src/vitable_connect/types/election_status_item.py deleted file mode 100644 index 2779bfa..0000000 --- a/src/vitable_connect/types/election_status_item.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ElectionStatusItem = typing.Union[typing.Literal["Enrolled", "Expired", "Pending", "Waived"], typing.Any] diff --git a/src/vitable_connect/types/eligibility_policy.py b/src/vitable_connect/types/eligibility_policy.py deleted file mode 100644 index 79df4a5..0000000 --- a/src/vitable_connect/types/eligibility_policy.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .eligibility_policy_rule import EligibilityPolicyRule -from .termination_rule import TerminationRule -from .waiting_period import WaitingPeriod - - -class EligibilityPolicy(UniversalBaseModel): - """ - The plan year's active eligibility policy. Mirrors the internal ``PlanYearEligibilityPolicyDTO`` - but exposes only the public subset (no raw ids, ``active_in``, or timestamps). - """ - - waiting_period: typing.Optional[WaitingPeriod] = pydantic.Field(default=None) - """ - When coverage starts for a newly-eligible member, or null. - - * `FIRST_OF_FOLLOWING_MONTH` - First Of Following Month - * `THIRTY_DAYS` - Thirty Days - * `SIXTY_DAYS` - Sixty Days - """ - - termination_rule: TerminationRule = pydantic.Field() - """ - When coverage ends after a member becomes ineligible. - - * `END_OF_N_MONTHS` - End Of N Months - * `END_OF_PLAN_YEAR` - End Of Plan Year - """ - - termination_n_months: typing.Optional[int] = pydantic.Field(default=None) - """ - Months of continued coverage; set only when `termination_rule` is `END_OF_N_MONTHS`. - """ - - rules: typing.List[EligibilityPolicyRule] = pydantic.Field() - """ - Eligibility rules; never empty for a valid policy. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/eligibility_policy_rule.py b/src/vitable_connect/types/eligibility_policy_rule.py deleted file mode 100644 index eaf99fe..0000000 --- a/src/vitable_connect/types/eligibility_policy_rule.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .eligibility_rule_compensation_type import EligibilityRuleCompensationType -from .eligibility_rule_employee_class import EligibilityRuleEmployeeClass -from .eligible_geographical_location import EligibleGeographicalLocation - - -class EligibilityPolicyRule(UniversalBaseModel): - """ - One eligibility rule — the workforce slice it makes eligible. Mirrors the internal - ``PlanYearEligibilityPolicyRuleDTO`` minus the raw ids and timestamps. - """ - - employee_class: EligibilityRuleEmployeeClass = pydantic.Field() - """ - Employee-class matcher (`All` for any). - - * `Full Time` - Full Time - * `Part Time` - Part Time - * `Temporary` - Temporary - * `Intern` - Intern - * `Seasonal` - Seasonal - * `Individual Contractor` - Individual Contractor - * `All` - All - """ - - compensation_type: EligibilityRuleCompensationType = pydantic.Field() - """ - Compensation matcher (`All` for any). - - * `Salary` - Salary - * `Hourly` - Hourly - * `All` - All - """ - - eligible_geographical_location: EligibleGeographicalLocation = pydantic.Field() - """ - Geographic matcher. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/eligibility_rule_compensation_type.py b/src/vitable_connect/types/eligibility_rule_compensation_type.py deleted file mode 100644 index 39943a0..0000000 --- a/src/vitable_connect/types/eligibility_rule_compensation_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -EligibilityRuleCompensationType = typing.Union[typing.Literal["Salary", "Hourly", "All"], typing.Any] diff --git a/src/vitable_connect/types/eligibility_rule_employee_class.py b/src/vitable_connect/types/eligibility_rule_employee_class.py deleted file mode 100644 index 2ca09cf..0000000 --- a/src/vitable_connect/types/eligibility_rule_employee_class.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -EligibilityRuleEmployeeClass = typing.Union[ - typing.Literal["Full Time", "Part Time", "Temporary", "Intern", "Seasonal", "Individual Contractor", "All"], - typing.Any, -] diff --git a/src/vitable_connect/types/eligible_geographical_location.py b/src/vitable_connect/types/eligible_geographical_location.py deleted file mode 100644 index c9d0d4f..0000000 --- a/src/vitable_connect/types/eligible_geographical_location.py +++ /dev/null @@ -1,31 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .eligible_geographical_location_type import EligibleGeographicalLocationType - - -class EligibleGeographicalLocation(UniversalBaseModel): - type: EligibleGeographicalLocationType = pydantic.Field() - """ - `All` (unrestricted) or `StateCodes` (restricted to `state_codes`). - - * `All` - All - * `StateCodes` - State Codes - """ - - state_codes: typing.List[str] = pydantic.Field() - """ - States the rule is restricted to; empty when `type` is `All`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/eligible_geographical_location_type.py b/src/vitable_connect/types/eligible_geographical_location_type.py deleted file mode 100644 index c297918..0000000 --- a/src/vitable_connect/types/eligible_geographical_location_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -EligibleGeographicalLocationType = typing.Union[typing.Literal["All", "StateCodes"], typing.Any] diff --git a/src/vitable_connect/types/employee.py b/src/vitable_connect/types/employee.py index 98b4cbc..6e171ab 100644 --- a/src/vitable_connect/types/employee.py +++ b/src/vitable_connect/types/employee.py @@ -1,145 +1,145 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import datetime as dt -import typing +from typing import List, Optional +from datetime import date, datetime +from typing_extensions import Literal -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .compensation_type import CompensationType -from .deduction_detail import DeductionDetail -from .employee_address import EmployeeAddress +from .._models import BaseModel from .employee_class import EmployeeClass -from .employee_status import EmployeeStatus +__all__ = ["Employee", "Deduction", "Address"] -class Employee(UniversalBaseModel): - id: typing.Optional[str] = pydantic.Field(default=None) - """ - Unique employee identifier with 'empl_' prefix - """ - member_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Unique member identifier with 'mbr_' prefix - """ +class Deduction(BaseModel): + benefit_name: str + """Name of the benefit plan""" - employer_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Unique identifier of the employer this employment is with (empr_*) - """ + deduction_amount_in_cents: int + """Employee deduction amount in cents""" - reference_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Partner-assigned reference ID for the employee - """ + deduction_category: Optional[str] = None + """Deduction category (reserved for future use)""" - employer_name: typing.Optional[str] = pydantic.Field(default=None) + frequency: Literal["weekly", "bi_weekly", "semi_monthly", "monthly"] """ - Name of the employer this employment is with + - `weekly` - Weekly + - `bi_weekly` - Bi Weekly + - `semi_monthly` - Semi Monthly + - `monthly` - Monthly """ - first_name: str = pydantic.Field() - """ - Employee's legal first name - """ + period_end_date: date + """Period end date (YYYY-MM-DD)""" - last_name: str = pydantic.Field() - """ - Employee's legal last name - """ + period_start_date: date + """Period start date (YYYY-MM-DD)""" - suffix: typing.Optional[str] = pydantic.Field(default=None) + tax_classification: Literal["Unknown", "Pre-tax", "Post-tax"] """ - Name suffix (e.g., Jr., Sr., III) + - `Unknown` - Unknown + - `Pre-tax` - Pre Tax + - `Post-tax` - Post Tax """ - email: str = pydantic.Field() - """ - Email address - """ - date_of_birth: dt.date = pydantic.Field() - """ - Date of birth (YYYY-MM-DD) - """ +class Address(BaseModel): + """Employee's residential address""" - gender: typing.Optional[str] = pydantic.Field(default=None) - """ - Gender identity, if provided - """ + address_line_1: str + """Primary street address""" - phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Phone number (10-digit US domestic string) - """ + city: str + """City name""" - employee_class: EmployeeClass = pydantic.Field() - """ - Employment classification affecting benefit eligibility - - * `Full Time` - Full Time - * `Part Time` - Part Time - * `Temporary` - Temporary - * `Intern` - Intern - * `Seasonal` - Seasonal - * `Individual Contractor` - Individual Contractor - """ + state: str + """Two-letter state code (e.g., CA, NY)""" - status: typing.Optional[EmployeeStatus] = pydantic.Field(default=None) - """ - Employee status (active or terminated) - """ + zipcode: str + """ZIP code (5 or 9 digit)""" - start_date: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Employee's start date with the employer - """ + address_line_2: Optional[str] = None + """Secondary street address (apt, suite, etc.)""" - classification_effective_date: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Date the employee's current classification took effect - """ - compensation_type: typing.Optional[CompensationType] = pydantic.Field(default=None) - """ - Employee's compensation type - """ +class Employee(BaseModel): + id: str + """Unique employee identifier with 'empl\\__' prefix""" - compensation_type_effective_date: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Date the employee's current compensation type took effect - """ + classification_effective_date: date + """Date the employee's current classification took effect""" - termination_date: typing.Optional[dt.date] = pydantic.Field(default=None) + compensation_type: Optional[Literal["Salary", "Hourly"]] = None """ - Employee's termination date, if terminated + - `Salary` - Salary + - `Hourly` - Hourly """ - address: typing.Optional[EmployeeAddress] = pydantic.Field(default=None) - """ - Employee's residential address - """ + compensation_type_effective_date: date + """Date the employee's current compensation type took effect""" - deductions: typing.List[DeductionDetail] = pydantic.Field() - """ - Payroll deductions from the most recent statement period. Replaced when a new statement is generated. - """ + created_at: datetime + """Timestamp when the employee was created""" - created_at: dt.datetime = pydantic.Field() - """ - Timestamp when the employee was created + date_of_birth: date + """Date of birth (YYYY-MM-DD)""" + + deductions: List[Deduction] + """Payroll deductions from the most recent statement period. + + Replaced when a new statement is generated. """ - updated_at: dt.datetime = pydantic.Field() + email: str + """Email address""" + + employee_class: EmployeeClass """ - Timestamp when the employee was last updated + - `Full Time` - Full Time + - `Part Time` - Part Time + - `Temporary` - Temporary + - `Intern` - Intern + - `Seasonal` - Seasonal + - `Individual Contractor` - Individual Contractor """ - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: + employer_id: str + """Unique identifier of the employer this employment is with (empr\\__\\**)""" + + first_name: str + """Employee's legal first name""" + + last_name: str + """Employee's legal last name""" + + member_id: str + """Unique member identifier with 'mbr\\__' prefix""" + + phone: Optional[str] = None + """Phone number (10-digit US domestic string)""" + + start_date: date + """Employee's start date with the employer""" + + status: Literal["active", "terminated"] + """Employee status (active or terminated)""" + + updated_at: datetime + """Timestamp when the employee was last updated""" + + address: Optional[Address] = None + """Employee's residential address""" + + employer_name: Optional[str] = None + """Name of the employer this employment is with""" + + gender: Optional[str] = None + """Gender identity, if provided""" + + reference_id: Optional[str] = None + """Partner-assigned reference ID for the employee""" + + suffix: Optional[str] = None + """Name suffix (e.g., Jr., Sr., III)""" - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + termination_date: Optional[date] = None + """Employee's termination date, if terminated""" diff --git a/src/vitable_connect/types/employee_address.py b/src/vitable_connect/types/employee_address.py deleted file mode 100644 index d6021c5..0000000 --- a/src/vitable_connect/types/employee_address.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata - - -class EmployeeAddress(UniversalBaseModel): - address_line1: typing_extensions.Annotated[ - str, - FieldMetadata(alias="address_line_1"), - pydantic.Field(alias="address_line_1", description="Primary street address"), - ] - """ - Primary street address - """ - - address_line2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="address_line_2"), - pydantic.Field(alias="address_line_2", description="Secondary street address (apt, suite, etc.)"), - ] = None - """ - Secondary street address (apt, suite, etc.) - """ - - city: str = pydantic.Field() - """ - City name - """ - - state: str = pydantic.Field() - """ - Two-letter state code (e.g., CA, NY) - """ - - zipcode: str = pydantic.Field() - """ - ZIP code (5 or 9 digit) - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employee_address_input.py b/src/vitable_connect/types/employee_address_input.py deleted file mode 100644 index 0e9fed7..0000000 --- a/src/vitable_connect/types/employee_address_input.py +++ /dev/null @@ -1,55 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata - - -class EmployeeAddressInput(UniversalBaseModel): - street1: typing_extensions.Annotated[ - str, FieldMetadata(alias="street_1"), pydantic.Field(alias="street_1", description="Primary street address") - ] - """ - Primary street address - """ - - street2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="street_2"), - pydantic.Field(alias="street_2", description="Secondary street address"), - ] = None - """ - Secondary street address - """ - - city: str = pydantic.Field() - """ - City name - """ - - state: str = pydantic.Field() - """ - Two-letter state code - """ - - zip_code: str = pydantic.Field() - """ - ZIP code - """ - - country: typing.Optional[str] = pydantic.Field(default=None) - """ - Country code - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employee_class.py b/src/vitable_connect/types/employee_class.py index 9abd5ba..b042772 100644 --- a/src/vitable_connect/types/employee_class.py +++ b/src/vitable_connect/types/employee_class.py @@ -1,7 +1,7 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import typing +from typing_extensions import Literal, TypeAlias -EmployeeClass = typing.Union[ - typing.Literal["Full Time", "Part Time", "Temporary", "Intern", "Seasonal", "Individual Contractor"], typing.Any -] +__all__ = ["EmployeeClass"] + +EmployeeClass: TypeAlias = Literal["Full Time", "Part Time", "Temporary", "Intern", "Seasonal", "Individual Contractor"] diff --git a/src/vitable_connect/types/employee_id.py b/src/vitable_connect/types/employee_id.py deleted file mode 100644 index ac3e620..0000000 --- a/src/vitable_connect/types/employee_id.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -EmployeeId = str -""" -Unique employee identifier (empl_*) -""" diff --git a/src/vitable_connect/types/employee_list_enrollments_params.py b/src/vitable_connect/types/employee_list_enrollments_params.py new file mode 100644 index 0000000..458062a --- /dev/null +++ b/src/vitable_connect/types/employee_list_enrollments_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["EmployeeListEnrollmentsParams"] + + +class EmployeeListEnrollmentsParams(TypedDict, total=False): + limit: int + """Items per page (default: 20, max: 100)""" + + page: int + """Page number (default: 1)""" diff --git a/src/vitable_connect/types/employee_list_response.py b/src/vitable_connect/types/employee_list_response.py deleted file mode 100644 index 831e950..0000000 --- a/src/vitable_connect/types/employee_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .employee import Employee -from .pagination import Pagination - - -class EmployeeListResponse(UniversalBaseModel): - """ - Paginated list response containing employee resources. - """ - - data: typing.List[Employee] = pydantic.Field() - """ - This page of employees. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employee_response.py b/src/vitable_connect/types/employee_response.py deleted file mode 100644 index 4b2f192..0000000 --- a/src/vitable_connect/types/employee_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .employee import Employee - - -class EmployeeResponse(UniversalBaseModel): - """ - Response containing a single employee resource. - """ - - data: Employee = pydantic.Field() - """ - The employee record. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employee_retrieve_response.py b/src/vitable_connect/types/employee_retrieve_response.py new file mode 100644 index 0000000..7d8ae10 --- /dev/null +++ b/src/vitable_connect/types/employee_retrieve_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel +from .employee import Employee + +__all__ = ["EmployeeRetrieveResponse"] + + +class EmployeeRetrieveResponse(BaseModel): + """Response containing a single employee resource.""" + + data: Employee diff --git a/src/vitable_connect/types/employee_search.py b/src/vitable_connect/types/employee_search.py deleted file mode 100644 index 43bffe9..0000000 --- a/src/vitable_connect/types/employee_search.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -EmployeeSearch = str -""" -Case-insensitive search across employee first name, last name, and email -""" diff --git a/src/vitable_connect/types/employee_status.py b/src/vitable_connect/types/employee_status.py deleted file mode 100644 index 8334058..0000000 --- a/src/vitable_connect/types/employee_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -EmployeeStatus = typing.Union[typing.Literal["active", "terminated"], typing.Any] diff --git a/src/vitable_connect/types/employee_update_params.py b/src/vitable_connect/types/employee_update_params.py new file mode 100644 index 0000000..e97bf55 --- /dev/null +++ b/src/vitable_connect/types/employee_update_params.py @@ -0,0 +1,79 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Optional +from datetime import date +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo +from .employee_class import EmployeeClass + +__all__ = ["EmployeeUpdateParams", "Address"] + + +class EmployeeUpdateParams(TypedDict, total=False): + effective_date: Required[Annotated[Union[str, date], PropertyInfo(format="iso8601")]] + """ + Past or present date applied to each tracked employment field included in this + request + """ + + address: Optional[Address] + """Employee's residential address""" + + compensation_type: Optional[Literal["Salary", "Hourly"]] + """ + - `Salary` - Salary + - `Hourly` - Hourly + """ + + email: Optional[str] + """Email address""" + + employee_class: Optional[EmployeeClass] + """ + - `Full Time` - Full Time + - `Part Time` - Part Time + - `Temporary` - Temporary + - `Intern` - Intern + - `Seasonal` - Seasonal + - `Individual Contractor` - Individual Contractor + """ + + gender: Optional[Literal["Male", "Female", "Transgender", "Non-binary", "Prefer not to respond"]] + """ + - `Male` - Male + - `Female` - Female + - `Transgender` - Transgender + - `Non-binary` - Non Binary + - `Prefer not to respond` - Prefer Not To Respond + """ + + phone: Optional[str] + """Phone number""" + + start_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] + """Employment start date""" + + +class Address(TypedDict, total=False): + """Employee's residential address""" + + city: Required[str] + """City name""" + + state: Required[str] + """Two-letter state code""" + + street_1: Required[str] + """Primary street address""" + + zip_code: Required[str] + """ZIP code""" + + country: str + """Country code""" + + street_2: Optional[str] + """Secondary street address""" diff --git a/src/vitable_connect/types/employee_update_response.py b/src/vitable_connect/types/employee_update_response.py new file mode 100644 index 0000000..05d1b91 --- /dev/null +++ b/src/vitable_connect/types/employee_update_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel +from .employee import Employee + +__all__ = ["EmployeeUpdateResponse"] + + +class EmployeeUpdateResponse(BaseModel): + """Response containing a single employee resource.""" + + data: Employee diff --git a/src/vitable_connect/types/employer.py b/src/vitable_connect/types/employer.py index 50bca4c..57f168a 100644 --- a/src/vitable_connect/types/employer.py +++ b/src/vitable_connect/types/employer.py @@ -1,89 +1,85 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import datetime as dt -import typing +from typing import Optional +from datetime import datetime -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .employer_address import EmployerAddress -from .employer_contact import EmployerContact +from .._models import BaseModel +__all__ = ["Employer", "Address", "Contact"] -class Employer(UniversalBaseModel): - """ - Serializer for Employer entity in public API responses. - """ - id: typing.Optional[str] = pydantic.Field(default=None) - """ - Unique employer identifier with 'empr_' prefix - """ +class Address(BaseModel): + """Nested address within EmployerSerializer.""" - organization_id: typing.Optional[str] = pydantic.Field(default=None) - """ - ID of the parent organization (org_*) - """ + address_line_1: str + """Primary street address""" - name: str = pydantic.Field() - """ - Display name of the employer - """ + city: str + """City name""" - legal_name: str = pydantic.Field() - """ - Legal business name for compliance and tax purposes - """ + state: str + """Two-letter state code (e.g., CA, NY)""" - ein: typing.Optional[str] = pydantic.Field(default=None) - """ - Employer Identification Number (format: XX-XXXXXXX) - """ + zipcode: str + """ZIP code (5 or 9 digit)""" - reference_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Partner-assigned reference ID for the employer - """ + address_line_2: Optional[str] = None + """Secondary street address (apt, suite, etc.)""" - email: typing.Optional[str] = pydantic.Field(default=None) - """ - Email address for billing and communications - """ - phone_number: typing.Optional[str] = pydantic.Field(default=None) +class Contact(BaseModel): """ - Employer phone number (E.164 format recommended) + Primary company-admin contact (email + phone; company admins have no person name). """ - active: bool = pydantic.Field() - """ - Whether the employer is currently active in the system - """ + email: Optional[str] = None + """Primary contact email""" - address: EmployerAddress = pydantic.Field() - """ - Employer's primary address - """ + phone: Optional[str] = None + """Primary contact phone, or null""" - contact: typing.Optional[EmployerContact] = pydantic.Field(default=None) - """ - Primary company-admin contact, or null - """ - created_at: dt.datetime = pydantic.Field() - """ - Timestamp when the employer was created - """ +class Employer(BaseModel): + """Serializer for Employer entity in public API responses.""" + + id: str + """Unique employer identifier with 'empr\\__' prefix""" + + active: bool + """Whether the employer is currently active in the system""" - updated_at: dt.datetime = pydantic.Field() + address: Address + """Nested address within EmployerSerializer.""" + + contact: Optional[Contact] = None """ - Timestamp when the employer was last updated + Primary company-admin contact (email + phone; company admins have no person + name). """ - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: + created_at: datetime + """Timestamp when the employer was created""" + + ein: Optional[str] = None + """Employer Identification Number (format: XX-XXXXXXX)""" + + legal_name: str + """Legal business name for compliance and tax purposes""" + + name: str + """Display name of the employer""" + + organization_id: Optional[str] = None + """ID of the parent organization (org\\__\\**)""" + + updated_at: datetime + """Timestamp when the employer was last updated""" + + email: Optional[str] = None + """Email address for billing and communications""" + + phone_number: Optional[str] = None + """Employer phone number (E.164 format recommended)""" - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + reference_id: Optional[str] = None + """Partner-assigned reference ID for the employer""" diff --git a/src/vitable_connect/types/employer_address.py b/src/vitable_connect/types/employer_address.py deleted file mode 100644 index ab1073a..0000000 --- a/src/vitable_connect/types/employer_address.py +++ /dev/null @@ -1,56 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata - - -class EmployerAddress(UniversalBaseModel): - """ - Nested address within EmployerSerializer. - """ - - address_line1: typing_extensions.Annotated[ - str, - FieldMetadata(alias="address_line_1"), - pydantic.Field(alias="address_line_1", description="Primary street address"), - ] - """ - Primary street address - """ - - address_line2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="address_line_2"), - pydantic.Field(alias="address_line_2", description="Secondary street address (apt, suite, etc.)"), - ] = None - """ - Secondary street address (apt, suite, etc.) - """ - - city: str = pydantic.Field() - """ - City name - """ - - state: str = pydantic.Field() - """ - Two-letter state code (e.g., CA, NY) - """ - - zipcode: str = pydantic.Field() - """ - ZIP code (5 or 9 digit) - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_address_input.py b/src/vitable_connect/types/employer_address_input.py deleted file mode 100644 index c92149e..0000000 --- a/src/vitable_connect/types/employer_address_input.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata - - -class EmployerAddressInput(UniversalBaseModel): - address_line1: typing_extensions.Annotated[ - str, - FieldMetadata(alias="address_line_1"), - pydantic.Field(alias="address_line_1", description="Primary street address"), - ] - """ - Primary street address - """ - - address_line2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="address_line_2"), - pydantic.Field(alias="address_line_2", description="Secondary street address"), - ] = None - """ - Secondary street address - """ - - city: str = pydantic.Field() - """ - City name - """ - - state: str = pydantic.Field() - """ - Two-letter state code - """ - - zipcode: str = pydantic.Field() - """ - ZIP code - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_benefit_lifecycle_stage.py b/src/vitable_connect/types/employer_benefit_lifecycle_stage.py deleted file mode 100644 index 61a947c..0000000 --- a/src/vitable_connect/types/employer_benefit_lifecycle_stage.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class EmployerBenefitLifecycleStage(UniversalBaseModel): - stage: str = pydantic.Field() - """ - Computed employer benefit-lifecycle stage: `open_enrollment`, `renewal`, `active`, `onboarding`, or `cancelled`. - """ - - as_of_date: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Anchor date for the stage (e.g. renewal date); null when not applicable. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_benefit_plan_year_response.py b/src/vitable_connect/types/employer_benefit_plan_year_response.py deleted file mode 100644 index 499e238..0000000 --- a/src/vitable_connect/types/employer_benefit_plan_year_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .benefit_plan_year_detail import BenefitPlanYearDetail - - -class EmployerBenefitPlanYearResponse(UniversalBaseModel): - """ - Response containing a single employer benefit plan year resource. - """ - - data: BenefitPlanYearDetail = pydantic.Field() - """ - The benefit plan year, including its plans and contribution rules. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_benefit_plan_years_list_response.py b/src/vitable_connect/types/employer_benefit_plan_years_list_response.py deleted file mode 100644 index af90b54..0000000 --- a/src/vitable_connect/types/employer_benefit_plan_years_list_response.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .benefit_plan_year_list import BenefitPlanYearList - - -class EmployerBenefitPlanYearsListResponse(UniversalBaseModel): - data: typing.List[BenefitPlanYearList] = pydantic.Field() - """ - The employer's benefit plan years, newest first. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_contact.py b/src/vitable_connect/types/employer_contact.py deleted file mode 100644 index ec8333e..0000000 --- a/src/vitable_connect/types/employer_contact.py +++ /dev/null @@ -1,31 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class EmployerContact(UniversalBaseModel): - """ - Primary company-admin contact (email + phone; company admins have no person name). - """ - - email: typing.Optional[str] = pydantic.Field(default=None) - """ - Primary contact email - """ - - phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Primary contact phone, or null - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_create_params.py b/src/vitable_connect/types/employer_create_params.py new file mode 100644 index 0000000..a4910c2 --- /dev/null +++ b/src/vitable_connect/types/employer_create_params.py @@ -0,0 +1,50 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["EmployerCreateParams", "Address"] + + +class EmployerCreateParams(TypedDict, total=False): + address: Required[Address] + """Employer address""" + + ein: Required[str] + """Employer Identification Number (format: XX-XXXXXXX)""" + + email: Required[str] + """Email address for billing and communications""" + + legal_name: Required[str] + """Legal business name""" + + name: Required[str] + """Employer display name""" + + phone_number: Optional[str] + """Employer phone number (10-digit US format, e.g. 5551234567)""" + + reference_id: Optional[str] + """External reference ID for this employer""" + + +class Address(TypedDict, total=False): + """Employer address""" + + address_line_1: Required[str] + """Primary street address""" + + city: Required[str] + """City name""" + + state: Required[str] + """Two-letter state code""" + + zipcode: Required[str] + """ZIP code""" + + address_line_2: Optional[str] + """Secondary street address""" diff --git a/src/vitable_connect/types/employer_ensure_payroll_integration_email_response.py b/src/vitable_connect/types/employer_ensure_payroll_integration_email_response.py new file mode 100644 index 0000000..c73abe4 --- /dev/null +++ b/src/vitable_connect/types/employer_ensure_payroll_integration_email_response.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["EmployerEnsurePayrollIntegrationEmailResponse", "Data"] + + +class Data(BaseModel): + integration_email: str + + +class EmployerEnsurePayrollIntegrationEmailResponse(BaseModel): + """Response containing a single payroll integration email resource.""" + + data: Data diff --git a/src/vitable_connect/types/employer_hris.py b/src/vitable_connect/types/employer_hris.py deleted file mode 100644 index ca83da2..0000000 --- a/src/vitable_connect/types/employer_hris.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .hris_connection import HrisConnection - - -class EmployerHris(UniversalBaseModel): - hris: typing.Optional[HrisConnection] = pydantic.Field(default=None) - """ - HRIS connection details, or null when the employer has no integration. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_hris_response.py b/src/vitable_connect/types/employer_hris_response.py deleted file mode 100644 index 2de457e..0000000 --- a/src/vitable_connect/types/employer_hris_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .employer_hris import EmployerHris - - -class EmployerHrisResponse(UniversalBaseModel): - """ - Response containing a single employer hris resource. - """ - - data: EmployerHris = pydantic.Field() - """ - The employer's HRIS connection, or `null` when no integration exists. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_hris_status.py b/src/vitable_connect/types/employer_hris_status.py deleted file mode 100644 index 1fc0ddf..0000000 --- a/src/vitable_connect/types/employer_hris_status.py +++ /dev/null @@ -1,32 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class EmployerHrisStatus(UniversalBaseModel): - provider: str = pydantic.Field() - """ - Id of the HRIS/payroll provider the employer is connected to (e.g. `paylocity`). - """ - - provider_label: str = pydantic.Field() - """ - Display name of that provider (e.g. `Paylocity`). - """ - - status: str = pydantic.Field() - """ - Connection status reported by the integration. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_id.py b/src/vitable_connect/types/employer_id.py deleted file mode 100644 index 50ec50d..0000000 --- a/src/vitable_connect/types/employer_id.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -EmployerId = str -""" -Unique employer identifier (empr_*) -""" diff --git a/src/vitable_connect/types/employer_invoice_pdf.py b/src/vitable_connect/types/employer_invoice_pdf.py deleted file mode 100644 index fe56a0b..0000000 --- a/src/vitable_connect/types/employer_invoice_pdf.py +++ /dev/null @@ -1,22 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class EmployerInvoicePdf(UniversalBaseModel): - download_url: str = pydantic.Field() - """ - Time-limited Chargebee PDF download link for the invoice. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_invoice_pdf_response.py b/src/vitable_connect/types/employer_invoice_pdf_response.py deleted file mode 100644 index b06cc9a..0000000 --- a/src/vitable_connect/types/employer_invoice_pdf_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .employer_invoice_pdf import EmployerInvoicePdf - - -class EmployerInvoicePdfResponse(UniversalBaseModel): - """ - Response containing a single employer invoice pdf resource. - """ - - data: EmployerInvoicePdf = pydantic.Field() - """ - A short-lived download link for the invoice PDF. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_invoices_list_response.py b/src/vitable_connect/types/employer_invoices_list_response.py deleted file mode 100644 index 137e8b8..0000000 --- a/src/vitable_connect/types/employer_invoices_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .invoice import Invoice -from .invoices_cursor_pagination import InvoicesCursorPagination - - -class EmployerInvoicesListResponse(UniversalBaseModel): - """ - Cursor-paginated invoices envelope: ``{ "data": [...], "pagination": { "next_offset": ... } }``. - """ - - data: typing.List[Invoice] = pydantic.Field() - """ - This page of invoices. - """ - - pagination: InvoicesCursorPagination = pydantic.Field() - """ - Cursor metadata for this result set. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_list_benefit_plan_year_enrollments_params.py b/src/vitable_connect/types/employer_list_benefit_plan_year_enrollments_params.py new file mode 100644 index 0000000..5118631 --- /dev/null +++ b/src/vitable_connect/types/employer_list_benefit_plan_year_enrollments_params.py @@ -0,0 +1,29 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["EmployerListBenefitPlanYearEnrollmentsParams"] + + +class EmployerListBenefitPlanYearEnrollmentsParams(TypedDict, total=False): + employer_id: Required[str] + """Unique employer identifier (empr\\__\\**)""" + + election_status: List[Literal["Enrolled", "Expired", "Pending", "Waived"]] + """Filter by election status. Repeat the parameter to match several.""" + + limit: int + """Items per page (default: 20, max: 100)""" + + page: int + """Page number (default: 1)""" + + search: str + """Case-insensitive search. + + Matches member name partially, and the `member_id` exactly — either your own + reference id or the prefixed `grpmbr_<...>` id. + """ diff --git a/src/vitable_connect/types/employer_list_benefit_plan_year_enrollments_response.py b/src/vitable_connect/types/employer_list_benefit_plan_year_enrollments_response.py new file mode 100644 index 0000000..0e806fa --- /dev/null +++ b/src/vitable_connect/types/employer_list_benefit_plan_year_enrollments_response.py @@ -0,0 +1,88 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["EmployerListBenefitPlanYearEnrollmentsResponse"] + + +class EmployerListBenefitPlanYearEnrollmentsResponse(BaseModel): + carrier: Optional[str] = None + """ + The carrier for this enrollment: the individual-market carrier for an ICHRA + plan, otherwise the benefit's own. Null when the benefit has no carrier. + """ + + dependent_count: int + """Dependents covered under this enrollment today. + + Counts the same dependents `premium_in_cents` is priced for, so a dependent + whose termination is dated in the future still counts. + """ + + election_status: Literal["Enrolled", "Waived", "Pending", "Expired"] + """ + - `Enrolled` - Enrolled + - `Waived` - Waived + - `Pending` - Pending + - `Expired` - Expired + """ + + employee_deduction_in_cents: Optional[int] = None + """ + What the employee is deducted monthly, in cents: `premium_in_cents` less + `employer_contribution_in_cents`, floored at zero. Null when unanswered/waived. + """ + + employee_external_reference_id: Optional[str] = None + """Your own reference id for this employee, as you supplied it. + + Null when you have not set one. + """ + + employee_id: str + """Our id for this person's employment with this employer (`empl_<...>`). + + A person who leaves and is rehired has two. + """ + + employer_contribution_in_cents: Optional[int] = None + """The employer's monthly share of `premium_in_cents`, in cents. + + Null when unanswered/waived. + """ + + member_first_name: str + """The member's first name.""" + + member_id: str + """Our id for the person (`mbr_<...>`). + + Stable across every employer they work for. + """ + + member_last_name: str + """The member's last name.""" + + plan: Optional[str] = None + """Chosen plan name, or null when unanswered/waived.""" + + policy_status: Optional[Literal["Coverage Upcoming", "Coverage Effective", "Coverage Ended", "Cancelled"]] = None + """ + - `Coverage Upcoming` - Coverage Upcoming + - `Coverage Effective` - Coverage Effective + - `Coverage Ended` - Coverage Ended + - `Cancelled` - Cancelled + """ + + premium_in_cents: Optional[int] = None + """Monthly premium in cents for the chosen plan, dependents included. + + The plan's own cost, not the employer's share of it. Null when + unanswered/waived. + """ + + tier: Optional[str] = None + """Chosen coverage tier, or null when unanswered/waived.""" diff --git a/src/vitable_connect/types/employer_list_benefit_plan_years_response.py b/src/vitable_connect/types/employer_list_benefit_plan_years_response.py new file mode 100644 index 0000000..1e52494 --- /dev/null +++ b/src/vitable_connect/types/employer_list_benefit_plan_years_response.py @@ -0,0 +1,129 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import date +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = [ + "EmployerListBenefitPlanYearsResponse", + "Data", + "DataEmployeeContribution", + "DataEmployerContribution", + "DataEnrollmentRate", +] + + +class DataEmployeeContribution(BaseModel): + """Employee contribution range.""" + + max_cents: int + """Highest per-tier contribution in cents.""" + + min_cents: int + """Lowest per-tier contribution in cents.""" + + +class DataEmployerContribution(BaseModel): + """Employer contribution range.""" + + max_cents: int + """Highest per-tier contribution in cents.""" + + min_cents: int + """Lowest per-tier contribution in cents.""" + + +class DataEnrollmentRate(BaseModel): + """Enrolled/eligible rate for this plan year.""" + + eligible: int + """Employees eligible for this plan year.""" + + enrolled: int + """Employees enrolled in this plan year.""" + + percentage: int + """`enrolled / eligible` whole-number percent (0 when none).""" + + +class Data(BaseModel): + """One plan year, list view. + + Standalone (no shared base) so the exact list payload is readable in one place; the detail + serializer is a separate class even where fields overlap. + """ + + benefit_id: str + """Prefixed benefit identifier (`bprd_*`).""" + + benefit_plan_year_id: str + """Prefixed plan-year identifier (`plyr_*`).""" + + carrier: Optional[str] = None + """Carrier name, or null (e.g. ICHRA).""" + + coverage_end: Optional[date] = None + """Coverage end.""" + + coverage_start: date + """Coverage start.""" + + employee_contribution: Optional[DataEmployeeContribution] = None + """Employee contribution range.""" + + employer_contribution: Optional[DataEmployerContribution] = None + """Employer contribution range.""" + + enrollment_rate: DataEnrollmentRate + """Enrolled/eligible rate for this plan year.""" + + family: Literal["mec", "mvp", "ichra", "vpc", "dental", "vision"] + """ + - `mec` - Mec + - `mvp` - Mvp + - `ichra` - Ichra + - `vpc` - Vpc + - `dental` - Dental + - `vision` - Vision + """ + + is_current: bool + """Whether this is the current plan year.""" + + network_names: List[str] + """ + Displayed networks: ["multi"] for ICHRA, otherwise the plan year's distinct + network names. + """ + + offered_states: List[str] + """Distinct offered state codes.""" + + open_enrollment_end: Optional[date] = None + """Open-enrollment end.""" + + open_enrollment_start: date + """Open-enrollment start.""" + + premium_in_cents: Optional[int] = None + """Monthly premium in cents; only for an ICHRA benefit with effective coverage.""" + + product_name: str + """Benefit/product display name.""" + + status: Literal["active", "upcoming", "open_enrollment", "inactive"] + """ + - `active` - Active + - `upcoming` - Upcoming + - `open_enrollment` - Open Enrollment + - `inactive` - Inactive + """ + + year: int + """Calendar coverage year.""" + + +class EmployerListBenefitPlanYearsResponse(BaseModel): + data: List[Data] diff --git a/src/vitable_connect/types/employer_list_employees_params.py b/src/vitable_connect/types/employer_list_employees_params.py new file mode 100644 index 0000000..93ab5fc --- /dev/null +++ b/src/vitable_connect/types/employer_list_employees_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["EmployerListEmployeesParams"] + + +class EmployerListEmployeesParams(TypedDict, total=False): + employment_status: Literal["active", "terminated"] + """Filter by employment status (active or terminated)""" + + limit: int + """Items per page (default: 20, max: 100)""" + + page: int + """Page number (default: 1)""" + + search: str + """Case-insensitive search across employee first name, last name, and email""" diff --git a/src/vitable_connect/types/employer_list_hris_providers_response.py b/src/vitable_connect/types/employer_list_hris_providers_response.py new file mode 100644 index 0000000..3d51ad9 --- /dev/null +++ b/src/vitable_connect/types/employer_list_hris_providers_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel + +__all__ = ["EmployerListHRISProvidersResponse", "Data"] + + +class Data(BaseModel): + provider: str + """HRIS/payroll provider id, as stored on the connection (e.g. + + `adp_run`). Filter with this. + """ + + provider_label: str + """Display name of that provider (e.g. `ADP Run`).""" + + +class EmployerListHRISProvidersResponse(BaseModel): + """Unpaginated ``{"data": [...]}`` list of organization hris providers.""" + + data: List[Data] diff --git a/src/vitable_connect/types/employer_list_invoices_params.py b/src/vitable_connect/types/employer_list_invoices_params.py new file mode 100644 index 0000000..b5a5a6e --- /dev/null +++ b/src/vitable_connect/types/employer_list_invoices_params.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["EmployerListInvoicesParams"] + + +class EmployerListInvoicesParams(TypedDict, total=False): + limit: int + """Maximum number of invoices per page""" + + offset: Optional[str] + """Opaque cursor from a previous page's next_offset""" diff --git a/src/vitable_connect/types/employer_list_invoices_response.py b/src/vitable_connect/types/employer_list_invoices_response.py new file mode 100644 index 0000000..0f48abe --- /dev/null +++ b/src/vitable_connect/types/employer_list_invoices_response.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional + +from .._models import BaseModel + +__all__ = ["EmployerListInvoicesResponse", "Data", "Pagination"] + + +class Data(BaseModel): + invoice_id: str + """Chargebee invoice id (external id, not a prefixed UUID).""" + + period: Optional[str] = None + """Invoice date as an ISO string, or null.""" + + status: Optional[str] = None + """Chargebee invoice status (e.g. `paid`), or null.""" + + total: Optional[float] = None + """Invoice total in dollars, or null.""" + + +class Pagination(BaseModel): + next_offset: Optional[str] = None + """ + Opaque JSON-encoded cursor for the next page; null when there are no more pages. + """ + + +class EmployerListInvoicesResponse(BaseModel): + """ + Cursor-paginated invoices envelope: ``{ "data": [...], "pagination": { "next_offset": ... } }``. + """ + + data: List[Data] + + pagination: Pagination diff --git a/src/vitable_connect/types/employer_list_params.py b/src/vitable_connect/types/employer_list_params.py new file mode 100644 index 0000000..e762199 --- /dev/null +++ b/src/vitable_connect/types/employer_list_params.py @@ -0,0 +1,48 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional +from typing_extensions import Literal, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["EmployerListParams"] + + +class EmployerListParams(TypedDict, total=False): + benefit_family: List[Literal["mec", "mvp", "ichra", "vpc", "dental", "vision"]] + """Filter to employers with at least one active benefit in these families.""" + + benefit_lifecycle_stage: List[Literal["open_enrollment", "renewal", "active", "onboarding", "cancelled"]] + """Filter to employers in one of these computed benefit-lifecycle stages.""" + + hris_provider: SequenceNotStr[str] + """ + Filter to employers whose HRIS connection is with one of these payroll providers + (e.g. `ADP RUN`). Matched case-insensitively; free text, so read the available + values from the HRIS-providers endpoint rather than assuming a fixed set. + """ + + hris_status: List[Literal["Pending", "Active", "Inactive", "Paused", "Terminated"]] + """Filter to employers whose HRIS connection is in one of these statuses.""" + + include_cancelled: bool + """ + Include cancelled employers (hidden by default unless their stage is explicitly + requested). + """ + + limit: int + """Items per page.""" + + page: int + """Page number.""" + + search: Optional[str] + """Employer filter. + + Matches the display name or the legal name case-insensitively as a substring, or + one of these exactly: the EIN (with or without its dash), the employer id, or + the contact email of one of the employer's non-disabled admins. + """ diff --git a/src/vitable_connect/types/employer_list_payroll_deduction_statements_params.py b/src/vitable_connect/types/employer_list_payroll_deduction_statements_params.py new file mode 100644 index 0000000..0000d7d --- /dev/null +++ b/src/vitable_connect/types/employer_list_payroll_deduction_statements_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["EmployerListPayrollDeductionStatementsParams"] + + +class EmployerListPayrollDeductionStatementsParams(TypedDict, total=False): + limit: int + """Maximum number of statements per page""" + + page: int + """Page number to retrieve (starts at 1)""" diff --git a/src/vitable_connect/types/employer_list_payroll_deduction_statements_response.py b/src/vitable_connect/types/employer_list_payroll_deduction_statements_response.py new file mode 100644 index 0000000..528c0b9 --- /dev/null +++ b/src/vitable_connect/types/employer_list_payroll_deduction_statements_response.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import date, datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["EmployerListPayrollDeductionStatementsResponse"] + + +class EmployerListPayrollDeductionStatementsResponse(BaseModel): + """One payroll-deduction statement row. + + Reads a :class:`PayrollDeductionStatementDTO` by attribute: the ``statement_id`` character field renders the + prefixed id via ``str()``, and the date/datetime fields emit ISO-8601 strings. + """ + + csv_file_url: Optional[str] = None + """Download link for the change CSV, or null.""" + + deduction_frequency: Literal["weekly", "bi_weekly", "semi_monthly", "monthly"] + """ + - `weekly` - Weekly + - `bi_weekly` - Bi Weekly + - `semi_monthly` - Semi Monthly + - `monthly` - Monthly + """ + + deduction_frequency_label: str + """Human-readable deduction frequency (e.g. `Monthly`).""" + + employee_count: int + """Distinct employees covered by the statement's entries.""" + + period_end: date + """Deduction period end date.""" + + period_start: date + """Deduction period start date.""" + + run_date: datetime + """When the statement was generated.""" + + statement_id: str + """ + Prefixed payroll-deduction-statement identifier (`pstmt_`). + """ + + total_deduction_cents: int + """Total payroll deduction for the period, in cents.""" diff --git a/src/vitable_connect/types/employer_list_response.py b/src/vitable_connect/types/employer_list_response.py new file mode 100644 index 0000000..7cf68cc --- /dev/null +++ b/src/vitable_connect/types/employer_list_response.py @@ -0,0 +1,140 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import date, datetime + +from .._models import BaseModel + +__all__ = ["EmployerListResponse", "Address", "BenefitLifecycleStage", "EnrollmentRateSummary", "HRISStatus"] + + +class Address(BaseModel): + """Shared read serializer for a postal address on public API responses. + + One definition for the address block every public resource emits (employer, employee, …), so the + 5-field shape isn't hand-rolled per endpoint. Read-only: it renders an already-built address + value object (e.g. ``AddressDVO``) whose attributes map 1:1 to these fields. + """ + + address_line_1: str + """Primary street address.""" + + address_line_2: Optional[str] = None + """Secondary street address (apt, suite, etc.).""" + + city: str + """City name.""" + + state: str + """Two-letter state code (e.g. `CA`, `NY`).""" + + zipcode: str + """ZIP code (5 or 9 digit).""" + + +class BenefitLifecycleStage(BaseModel): + as_of_date: Optional[date] = None + """Anchor date for the stage (e.g. renewal date); null when not applicable.""" + + stage: str + """ + Computed employer benefit-lifecycle stage: `open_enrollment`, `renewal`, + `active`, `onboarding`, or `cancelled`. + """ + + +class EnrollmentRateSummary(BaseModel): + """Enrolled/eligible employees roll-up.""" + + eligible: int + """Employees eligible for at least one active benefit.""" + + enrolled: int + """Employees enrolled in at least one active benefit.""" + + percentage: int + """`enrolled / eligible` as a whole-number percent (0 when none eligible).""" + + +class HRISStatus(BaseModel): + """HRIS connection, or null when the employer has none.""" + + provider: str + """Id of the HRIS/payroll provider the employer is connected to (e.g. + + `paylocity`). + """ + + provider_label: str + """Display name of that provider (e.g. `Paylocity`).""" + + status: str + """Connection status reported by the integration.""" + + +class EmployerListResponse(BaseModel): + """One employer row of the organization's book (list projection). + + Carries the enriched/computed columns (enrollment roll-up, benefit-family tags, HRIS connection, + benefit-lifecycle stage) alongside the flat CRM fields of the underlying employer (legal name, + EIN, contact, address, timestamps) for parity with the legacy ``Employer`` contract. + """ + + active: bool + """Whether the employer is currently active in the system.""" + + address: Address + """Shared read serializer for a postal address on public API responses. + + One definition for the address block every public resource emits (employer, + employee, …), so the 5-field shape isn't hand-rolled per endpoint. Read-only: it + renders an already-built address value object (e.g. `AddressDVO`) whose + attributes map 1:1 to these fields. + """ + + benefit_families: List[str] + """Distinct benefit-family tags across the employer's active benefits (e.g. + + `MEC`, `ICHRA`, `VPC`). + """ + + benefit_lifecycle_stage: BenefitLifecycleStage + + created_at: datetime + """Timestamp when the employer was created.""" + + ein: Optional[str] = None + """Employer Identification Number (masked in responses).""" + + email: Optional[str] = None + """Email address for billing and communications.""" + + employer_id: str + """Prefixed employer identifier (`empr_`).""" + + enrollment_rate_summary: EnrollmentRateSummary + """Enrolled/eligible employees roll-up.""" + + hris_status: Optional[HRISStatus] = None + """HRIS connection, or null when the employer has none.""" + + legal_name: Optional[str] = None + """Legal business name for compliance and tax purposes.""" + + name: str + """Employer name.""" + + organization_id: Optional[str] = None + """ID of the parent organization (`org_*`), or null when unknown.""" + + phone_number: Optional[str] = None + """Employer phone number.""" + + reference_id: Optional[str] = None + """ + The organization's own reference id for this employer, or null when none was + assigned. + """ + + updated_at: datetime + """Timestamp when the employer was last updated.""" diff --git a/src/vitable_connect/types/employer_payroll_deduction_statement_list_response.py b/src/vitable_connect/types/employer_payroll_deduction_statement_list_response.py deleted file mode 100644 index 0c8c9b6..0000000 --- a/src/vitable_connect/types/employer_payroll_deduction_statement_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .pagination import Pagination -from .payroll_deduction_statement import PayrollDeductionStatement - - -class EmployerPayrollDeductionStatementListResponse(UniversalBaseModel): - """ - Paginated list response containing employer payroll deduction statement resources. - """ - - data: typing.List[PayrollDeductionStatement] = pydantic.Field() - """ - This page of payroll deduction statements. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_response.py b/src/vitable_connect/types/employer_response.py index 800098e..f31cf4e 100644 --- a/src/vitable_connect/types/employer_response.py +++ b/src/vitable_connect/types/employer_response.py @@ -1,27 +1,13 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .._models import BaseModel from .employer import Employer +__all__ = ["EmployerResponse"] -class EmployerResponse(UniversalBaseModel): - """ - Response containing a single employer resource. - """ - - data: Employer = pydantic.Field() - """ - The employer record. - """ - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: +class EmployerResponse(BaseModel): + """Response containing a single employer resource.""" - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + data: Employer + """Serializer for Employer entity in public API responses.""" diff --git a/src/vitable_connect/types/employer_retrieve_benefit_plan_year_response.py b/src/vitable_connect/types/employer_retrieve_benefit_plan_year_response.py new file mode 100644 index 0000000..ebea69e --- /dev/null +++ b/src/vitable_connect/types/employer_retrieve_benefit_plan_year_response.py @@ -0,0 +1,335 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import date +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = [ + "EmployerRetrieveBenefitPlanYearResponse", + "Data", + "DataContributionStrategy", + "DataContributionStrategyContributionTier", + "DataContributionStrategyIchraContributionClass", + "DataEligibilityPolicy", + "DataEligibilityPolicyRule", + "DataEligibilityPolicyRuleEligibleGeographicalLocation", + "DataEmployeeContribution", + "DataEmployerContribution", + "DataEnrollmentRate", +] + + +class DataContributionStrategyContributionTier(BaseModel): + """ + One non-ICHRA coverage tier, mirroring the internal configuration ``CompanyBenefitPlanTierCostDTO`` + minus the tier-cost id, ``pepm`` and ``pepm_per_dependent``; ``benefit_plan_id`` is the prefixed + ``bpln_*`` form rather than a raw UUID. + """ + + benefit_plan_id: str + """Prefixed benefit-plan identifier (`bpln_*`).""" + + benefit_plan_name: str + """Benefit plan name.""" + + benefit_plan_tier_name: str + """Coverage-tier name.""" + + cost: int + """Monthly employee deduction in cents.""" + + cost_per_dependent: int + """Monthly employee deduction per dependent, in cents.""" + + coverage_tier: Literal["Unspecified", "EE", "ES", "EC", "EF"] + """ + - `Unspecified` - Unspecified + - `EE` - EE + - `ES` - ES + - `EC` - EC + - `EF` - EF + """ + + dependents_required_in: bool + """Whether dependents are required for this tier.""" + + spouse_required_in: bool + """Whether a spouse is required for this tier.""" + + +class DataContributionStrategyIchraContributionClass(BaseModel): + """ + One ICHRA contribution class, mirroring the internal configuration endpoint's + ``IchraContributionClassConfigurationEntitySerializer`` field-for-field. Two deliberate + differences for the public surface: the identifier is the opaque prefixed ``iccl_*`` form rather + than a raw UUID, and the matcher choices come from the domain enums rather than the model's. + """ + + amount_in_cents: int + """Monthly allowance in cents.""" + + compensation: Literal["Unspecified", "Salary", "Hourly"] + """ + - `Unspecified` - Unspecified + - `Salary` - Salary + - `Hourly` - Hourly + """ + + contribution_class_id: str + """Prefixed contribution-class identifier (`iccl_*`).""" + + employment: Literal["Unspecified", "Full Time", "Part Time", "Temporary", "Seasonal"] + """ + - `Unspecified` - Unspecified + - `Full Time` - Full Time + - `Part Time` - Part Time + - `Temporary` - Temporary + - `Seasonal` - Seasonal + """ + + family_status: Literal["Unspecified", "EE", "ES", "EC", "EF"] + """ + - `Unspecified` - Unspecified + - `EE` - EE + - `ES` - ES + - `EC` - EC + - `EF` - EF + """ + + location: Literal["Unspecified", "State"] + """ + - `Unspecified` - Unspecified + - `State` - State + """ + + location_value: Optional[str] = None + """Location matcher value (CSV of state codes), or null.""" + + max_age: Optional[int] = None + """Age-band upper bound, or null.""" + + min_age: Optional[int] = None + """Age-band lower bound, or null.""" + + +class DataContributionStrategy(BaseModel): + """ + How the plan year prices contributions: exactly one collection is populated, determined by the + plan year's ``family`` (ICHRA vs tier-priced). + """ + + contribution_tiers: List[DataContributionStrategyContributionTier] + """Coverage tiers and their costs; empty for ICHRA benefits.""" + + ichra_contribution_classes: List[DataContributionStrategyIchraContributionClass] + """ICHRA contribution classes; empty for tier-priced benefits.""" + + +class DataEligibilityPolicyRuleEligibleGeographicalLocation(BaseModel): + """Geographic matcher.""" + + state_codes: List[str] + """States the rule is restricted to; empty when `type` is `All`.""" + + type: Literal["All", "StateCodes"] + """ + - `All` - All + - `StateCodes` - State Codes + """ + + +class DataEligibilityPolicyRule(BaseModel): + """One eligibility rule — the workforce slice it makes eligible. + + Mirrors the internal + ``PlanYearEligibilityPolicyRuleDTO`` minus the raw ids and timestamps. + """ + + compensation_type: Literal["Salary", "Hourly", "All"] + """ + - `Salary` - Salary + - `Hourly` - Hourly + - `All` - All + """ + + eligible_geographical_location: DataEligibilityPolicyRuleEligibleGeographicalLocation + """Geographic matcher.""" + + employee_class: Literal["Full Time", "Part Time", "Temporary", "Intern", "Seasonal", "Individual Contractor", "All"] + """ + - `Full Time` - Full Time + - `Part Time` - Part Time + - `Temporary` - Temporary + - `Intern` - Intern + - `Seasonal` - Seasonal + - `Individual Contractor` - Individual Contractor + - `All` - All + """ + + +class DataEligibilityPolicy(BaseModel): + """The plan year's active eligibility policy. + + Mirrors the internal ``PlanYearEligibilityPolicyDTO`` + but exposes only the public subset (no raw ids, ``active_in``, or timestamps). + """ + + rules: List[DataEligibilityPolicyRule] + """Eligibility rules; never empty for a valid policy.""" + + termination_n_months: Optional[int] = None + """ + Months of continued coverage; set only when `termination_rule` is + `END_OF_N_MONTHS`. + """ + + termination_rule: Literal["END_OF_N_MONTHS", "END_OF_PLAN_YEAR"] + """ + - `END_OF_N_MONTHS` - End Of N Months + - `END_OF_PLAN_YEAR` - End Of Plan Year + """ + + waiting_period: Optional[Literal["FIRST_OF_FOLLOWING_MONTH", "THIRTY_DAYS", "SIXTY_DAYS"]] = None + """ + - `FIRST_OF_FOLLOWING_MONTH` - First Of Following Month + - `THIRTY_DAYS` - Thirty Days + - `SIXTY_DAYS` - Sixty Days + """ + + +class DataEmployeeContribution(BaseModel): + """Employee contribution range.""" + + max_cents: int + """Highest per-tier contribution in cents.""" + + min_cents: int + """Lowest per-tier contribution in cents.""" + + +class DataEmployerContribution(BaseModel): + """Employer contribution range.""" + + max_cents: int + """Highest per-tier contribution in cents.""" + + min_cents: int + """Lowest per-tier contribution in cents.""" + + +class DataEnrollmentRate(BaseModel): + """Enrolled/eligible rate for this plan year.""" + + eligible: int + """Employees eligible for this plan year.""" + + enrolled: int + """Employees enrolled in this plan year.""" + + percentage: int + """`enrolled / eligible` whole-number percent (0 when none).""" + + +class Data(BaseModel): + """One plan year, detail view. + + Standalone (no shared base) so the exact detail payload is readable in one place; the list + serializer is a separate class even where fields overlap. Detail carries the SPD link and omits + the list-only ``is_current`` flag. + """ + + benefit_id: str + """Prefixed benefit identifier (`bprd_*`).""" + + benefit_plan_year_id: str + """Prefixed plan-year identifier (`plyr_*`).""" + + carrier: Optional[str] = None + """Carrier name, or null (e.g. ICHRA).""" + + contribution_strategy: DataContributionStrategy + """ + How the plan year prices contributions: exactly one collection is populated, + determined by the plan year's `family` (ICHRA vs tier-priced). + """ + + coverage_end: Optional[date] = None + """Coverage end.""" + + coverage_start: date + """Coverage start.""" + + eligibility_policy: Optional[DataEligibilityPolicy] = None + """The plan year's active eligibility policy. + + Mirrors the internal `PlanYearEligibilityPolicyDTO` but exposes only the public + subset (no raw ids, `active_in`, or timestamps). + """ + + employee_contribution: Optional[DataEmployeeContribution] = None + """Employee contribution range.""" + + employer_contribution: Optional[DataEmployerContribution] = None + """Employer contribution range.""" + + enrollment_rate: DataEnrollmentRate + """Enrolled/eligible rate for this plan year.""" + + family: Literal["mec", "mvp", "ichra", "vpc", "dental", "vision"] + """ + - `mec` - Mec + - `mvp` - Mvp + - `ichra` - Ichra + - `vpc` - Vpc + - `dental` - Dental + - `vision` - Vision + """ + + network_names: List[str] + """ + Displayed networks: ["multi"] for ICHRA, otherwise the plan year's distinct + network names. + """ + + offered_states: List[str] + """Distinct offered state codes.""" + + open_enrollment_end: Optional[date] = None + """Open-enrollment end.""" + + open_enrollment_start: date + """Open-enrollment start.""" + + premium_in_cents: Optional[int] = None + """Monthly premium in cents; only for an ICHRA benefit with effective coverage.""" + + product_name: str + """Benefit/product display name.""" + + spd_file_url: Optional[str] = None + """Summary Plan Description (SPD) link, or null.""" + + status: Literal["active", "upcoming", "open_enrollment", "inactive"] + """ + - `active` - Active + - `upcoming` - Upcoming + - `open_enrollment` - Open Enrollment + - `inactive` - Inactive + """ + + year: int + """Calendar coverage year.""" + + +class EmployerRetrieveBenefitPlanYearResponse(BaseModel): + """Response containing a single employer benefit plan year resource.""" + + data: Data + """One plan year, detail view. + + Standalone (no shared base) so the exact detail payload is readable in one + place; the list serializer is a separate class even where fields overlap. Detail + carries the SPD link and omits the list-only `is_current` flag. + """ diff --git a/src/vitable_connect/types/employer_retrieve_hris_response.py b/src/vitable_connect/types/employer_retrieve_hris_response.py new file mode 100644 index 0000000..9e80db7 --- /dev/null +++ b/src/vitable_connect/types/employer_retrieve_hris_response.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["EmployerRetrieveHRISResponse", "Data", "DataHRIS"] + + +class DataHRIS(BaseModel): + """HRIS connection details, or null when the employer has no integration.""" + + last_sync_on: Optional[datetime] = None + """When the last sync completed, or null when none has.""" + + provider: str + """Id of the HRIS/payroll provider the employer is connected to (e.g. + + `paylocity`). + """ + + provider_label: str + """Display name of that provider (e.g. `Paylocity`).""" + + status: str + """Connection status reported by the integration.""" + + synced_row_count: Optional[int] = None + """Rows in the latest completed sync, or null when none has.""" + + +class Data(BaseModel): + hris: Optional[DataHRIS] = None + """HRIS connection details, or null when the employer has no integration.""" + + +class EmployerRetrieveHRISResponse(BaseModel): + """Response containing a single employer hris resource.""" + + data: Data diff --git a/src/vitable_connect/types/employer_retrieve_invoice_pdf_response.py b/src/vitable_connect/types/employer_retrieve_invoice_pdf_response.py new file mode 100644 index 0000000..d8dbe7a --- /dev/null +++ b/src/vitable_connect/types/employer_retrieve_invoice_pdf_response.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["EmployerRetrieveInvoicePdfResponse", "Data"] + + +class Data(BaseModel): + download_url: str + """Time-limited Chargebee PDF download link for the invoice.""" + + +class EmployerRetrieveInvoicePdfResponse(BaseModel): + """Response containing a single employer invoice pdf resource.""" + + data: Data diff --git a/src/vitable_connect/types/employer_retrieve_payroll_access_setup_response.py b/src/vitable_connect/types/employer_retrieve_payroll_access_setup_response.py new file mode 100644 index 0000000..85bab39 --- /dev/null +++ b/src/vitable_connect/types/employer_retrieve_payroll_access_setup_response.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["EmployerRetrievePayrollAccessSetupResponse", "Data"] + + +class Data(BaseModel): + completed: bool + + submitted_at: Optional[datetime] = None + + +class EmployerRetrievePayrollAccessSetupResponse(BaseModel): + """Response containing a single payroll access setup status resource.""" + + data: Data diff --git a/src/vitable_connect/types/employer_search.py b/src/vitable_connect/types/employer_search.py deleted file mode 100644 index 2f23c40..0000000 --- a/src/vitable_connect/types/employer_search.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -EmployerSearch = str -""" -Case-insensitive search. Matches member name partially, and the `member_id` exactly — either your own reference id or the prefixed `grpmbr_<...>` id. -""" diff --git a/src/vitable_connect/types/employer_settings.py b/src/vitable_connect/types/employer_settings.py deleted file mode 100644 index ca3653b..0000000 --- a/src/vitable_connect/types/employer_settings.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .deduction_frequency import DeductionFrequency - - -class EmployerSettings(UniversalBaseModel): - pay_frequency: typing.Optional[DeductionFrequency] = pydantic.Field(default=None) - """ - Payroll deduction frequency. One of: weekly, bi_weekly, semi_monthly, monthly - - * `weekly` - Weekly - * `bi_weekly` - Bi-Weekly - * `semi_monthly` - Semi-Monthly - * `monthly` - Monthly - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_settings_response.py b/src/vitable_connect/types/employer_settings_response.py deleted file mode 100644 index 2d11d1a..0000000 --- a/src/vitable_connect/types/employer_settings_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .employer_settings import EmployerSettings - - -class EmployerSettingsResponse(UniversalBaseModel): - """ - Response containing a single employer settings resource. - """ - - data: EmployerSettings = pydantic.Field() - """ - The employer's current settings. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/employer_submit_census_sync_params.py b/src/vitable_connect/types/employer_submit_census_sync_params.py new file mode 100644 index 0000000..6a75e2e --- /dev/null +++ b/src/vitable_connect/types/employer_submit_census_sync_params.py @@ -0,0 +1,192 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from datetime import date +from typing_extensions import Literal, Required, Annotated, TypedDict + +from .._utils import PropertyInfo +from .employee_class import EmployeeClass + +__all__ = ["EmployerSubmitCensusSyncParams", "Employee", "EmployeeAddress"] + + +class EmployerSubmitCensusSyncParams(TypedDict, total=False): + employees: Required[Iterable[Employee]] + + +class EmployeeAddress(TypedDict, total=False): + address_line_1: Required[str] + + city: Required[str] + + state: Required[ + Literal[ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DC", + "DE", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WI", + "WV", + "WY", + "PR", + "GU", + "AS", + "VI", + "MP", + "MH", + "PW", + "FM", + "AE", + "AA", + "AP", + ] + ] + """ + - `AL` - AL + - `AK` - AK + - `AZ` - AZ + - `AR` - AR + - `CA` - CA + - `CO` - CO + - `CT` - CT + - `DC` - DC + - `DE` - DE + - `FL` - FL + - `GA` - GA + - `HI` - HI + - `ID` - ID + - `IL` - IL + - `IN` - IN + - `IA` - IA + - `KS` - KS + - `KY` - KY + - `LA` - LA + - `ME` - ME + - `MD` - MD + - `MA` - MA + - `MI` - MI + - `MN` - MN + - `MS` - MS + - `MO` - MO + - `MT` - MT + - `NE` - NE + - `NV` - NV + - `NH` - NH + - `NJ` - NJ + - `NM` - NM + - `NY` - NY + - `NC` - NC + - `ND` - ND + - `OH` - OH + - `OK` - OK + - `OR` - OR + - `PA` - PA + - `RI` - RI + - `SC` - SC + - `SD` - SD + - `TN` - TN + - `TX` - TX + - `UT` - UT + - `VT` - VT + - `VA` - VA + - `WA` - WA + - `WI` - WI + - `WV` - WV + - `WY` - WY + - `PR` - PR + - `GU` - GU + - `AS` - AS + - `VI` - VI + - `MP` - MP + - `MH` - MH + - `PW` - PW + - `FM` - FM + - `AE` - AE + - `AA` - AA + - `AP` - AP + """ + + zipcode: Required[str] + + address_line_2: Optional[str] + + +class Employee(TypedDict, total=False): + date_of_birth: Required[Annotated[Union[str, date], PropertyInfo(format="iso8601")]] + + email: Required[str] + + first_name: Required[str] + + last_name: Required[str] + + address: Optional[EmployeeAddress] + + compensation_type: Optional[Literal["Salary", "Hourly"]] + """ + - `Salary` - Salary + - `Hourly` - Hourly + """ + + employee_class: Optional[EmployeeClass] + """ + - `Full Time` - Full Time + - `Part Time` - Part Time + - `Temporary` - Temporary + - `Intern` - Intern + - `Seasonal` - Seasonal + - `Individual Contractor` - Individual Contractor + """ + + phone: Optional[str] + """Phone number""" + + reference_id: Optional[str] + + start_date: Annotated[Union[str, date, None], PropertyInfo(format="iso8601")] diff --git a/src/vitable_connect/types/employer_submit_census_sync_response.py b/src/vitable_connect/types/employer_submit_census_sync_response.py new file mode 100644 index 0000000..1e53430 --- /dev/null +++ b/src/vitable_connect/types/employer_submit_census_sync_response.py @@ -0,0 +1,19 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["EmployerSubmitCensusSyncResponse", "Data"] + + +class Data(BaseModel): + accepted_at: datetime + + employer_id: str + + +class EmployerSubmitCensusSyncResponse(BaseModel): + """Response containing a single census sync detail resource.""" + + data: Data diff --git a/src/vitable_connect/types/employer_submit_payroll_access_setup_params.py b/src/vitable_connect/types/employer_submit_payroll_access_setup_params.py new file mode 100644 index 0000000..8723024 --- /dev/null +++ b/src/vitable_connect/types/employer_submit_payroll_access_setup_params.py @@ -0,0 +1,78 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +from .._types import SequenceNotStr + +__all__ = ["EmployerSubmitPayrollAccessSetupParams"] + + +class EmployerSubmitPayrollAccessSetupParams(TypedDict, total=False): + access_method: Required[Literal["SELF_SETUP", "NEEDS_HELP"]] + """ + - `SELF_SETUP` - SELF_SETUP + - `NEEDS_HELP` - NEEDS_HELP + """ + + all_benefit_eligible_employees_present: Required[bool] + + classifications_accurate: Required[bool] + + employees_in_payroll_acknowledged: Required[bool] + + has_additional_payroll_system: Required[bool] + + is_controlled_group: Required[bool] + + payroll_data_impacts_eligibility_acknowledged: Required[bool] + + additional_access_method: Optional[Literal["SELF_SETUP", "NEEDS_HELP"]] + """ + - `SELF_SETUP` - SELF_SETUP + - `NEEDS_HELP` - NEEDS_HELP + """ + + additional_integration_confirmed: Optional[bool] + + additional_login_url: Optional[str] + + additional_password: Optional[str] + + additional_phone: Optional[str] + + additional_username: Optional[str] + + classification_correction_source: Optional[Literal["ENTER_NAMES", "EMAIL_LIST"]] + """ + - `ENTER_NAMES` - ENTER_NAMES + - `EMAIL_LIST` - EMAIL_LIST + """ + + integration_confirmed: Optional[bool] + + login_url: Optional[str] + + misclassified_employee_names: SequenceNotStr[str] + + missing_employee_resolution: Optional[Literal["EMAIL_CENSUS", "SECOND_SYSTEM_ACCESS"]] + """ + - `EMAIL_CENSUS` - EMAIL_CENSUS + - `SECOND_SYSTEM_ACCESS` - SECOND_SYSTEM_ACCESS + """ + + password: Optional[str] + + phone: Optional[str] + + remaining_employee_action: Optional[Literal["VITABLE_UPDATE", "EMPLOYER_UPDATE"]] + """ + - `VITABLE_UPDATE` - VITABLE_UPDATE + - `EMPLOYER_UPDATE` - EMPLOYER_UPDATE + """ + + same_payroll_covers_other_eins: Optional[bool] + + username: Optional[str] diff --git a/src/vitable_connect/types/employer_submit_payroll_access_setup_response.py b/src/vitable_connect/types/employer_submit_payroll_access_setup_response.py new file mode 100644 index 0000000..6479016 --- /dev/null +++ b/src/vitable_connect/types/employer_submit_payroll_access_setup_response.py @@ -0,0 +1,20 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["EmployerSubmitPayrollAccessSetupResponse", "Data"] + + +class Data(BaseModel): + completed: bool + + submitted_at: Optional[datetime] = None + + +class EmployerSubmitPayrollAccessSetupResponse(BaseModel): + """Response containing a single payroll access setup status resource.""" + + data: Data diff --git a/src/vitable_connect/types/employer_update_params.py b/src/vitable_connect/types/employer_update_params.py new file mode 100644 index 0000000..ffd189f --- /dev/null +++ b/src/vitable_connect/types/employer_update_params.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Required, TypedDict + +__all__ = ["EmployerUpdateParams", "Address"] + + +class EmployerUpdateParams(TypedDict, total=False): + active: Optional[bool] + """Whether the employer is active""" + + address: Optional[Address] + """Employer address""" + + legal_name: Optional[str] + """Legal business name""" + + name: Optional[str] + """Employer display name""" + + +class Address(TypedDict, total=False): + """Employer address""" + + address_line_1: Required[str] + """Primary street address""" + + city: Required[str] + """City name""" + + state: Required[str] + """Two-letter state code""" + + zipcode: Required[str] + """ZIP code""" + + address_line_2: Optional[str] + """Secondary street address""" diff --git a/src/vitable_connect/types/employer_update_settings_params.py b/src/vitable_connect/types/employer_update_settings_params.py new file mode 100644 index 0000000..7b2bf3c --- /dev/null +++ b/src/vitable_connect/types/employer_update_settings_params.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["EmployerUpdateSettingsParams"] + + +class EmployerUpdateSettingsParams(TypedDict, total=False): + pay_frequency: Required[Literal["weekly", "bi_weekly", "semi_monthly", "monthly"]] + """ + - `weekly` - Weekly + - `bi_weekly` - Bi Weekly + - `semi_monthly` - Semi Monthly + - `monthly` - Monthly + """ diff --git a/src/vitable_connect/types/employer_update_settings_response.py b/src/vitable_connect/types/employer_update_settings_response.py new file mode 100644 index 0000000..0d0f2d3 --- /dev/null +++ b/src/vitable_connect/types/employer_update_settings_response.py @@ -0,0 +1,24 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["EmployerUpdateSettingsResponse", "Data"] + + +class Data(BaseModel): + pay_frequency: Optional[Literal["weekly", "bi_weekly", "semi_monthly", "monthly"]] = None + """ + - `weekly` - Weekly + - `bi_weekly` - Bi Weekly + - `semi_monthly` - Semi Monthly + - `monthly` - Monthly + """ + + +class EmployerUpdateSettingsResponse(BaseModel): + """Response containing a single employer settings resource.""" + + data: Data diff --git a/src/vitable_connect/types/enrollment.py b/src/vitable_connect/types/enrollment.py index 7f30987..3afb590 100644 --- a/src/vitable_connect/types/enrollment.py +++ b/src/vitable_connect/types/enrollment.py @@ -1,94 +1,111 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import datetime as dt -import typing +from typing import Optional +from datetime import date, datetime +from typing_extensions import Literal -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .benefit_summary import BenefitSummary +from .._models import BaseModel from .enrollment_status import EnrollmentStatus +__all__ = ["Enrollment", "Benefit"] -class Enrollment(UniversalBaseModel): - """ - One enrollment as it appears in a list row. - """ - id: str = pydantic.Field() - """ - Unique enrollment identifier (enrl_*) - """ +class Benefit(BaseModel): + """Nested benefit product summary""" - employee_id: str = pydantic.Field() - """ - Employee ID (empl_*) - """ + id: str + """Benefit product ID (bprd\\__\\**)""" - employer_id: str = pydantic.Field() + category: Literal["Medical", "Dental", "Vision", "Hospital"] """ - Employer ID (empr_*) + - `Medical` - Medical + - `Dental` - Dental + - `Vision` - Vision + - `Hospital` - Hospital """ - benefit: BenefitSummary = pydantic.Field() - """ - Nested benefit product summary - """ + name: str + """Display name of the benefit product""" - status: EnrollmentStatus = pydantic.Field() + product_code: Literal[ + "EBA", + "VPC", + "VPC_CORE", + "MEC", + "MEC2", + "MEC_PLUS", + "MVP", + "MVP2", + "MVPSL", + "MVPSL2", + "VD", + "VV", + "ICHRA", + "ICHRA_PREMIUM_PLUS", + "ICHRA_REIMBURSEMENT_ONLY", + ] """ - Enrollment status - - * `pending` - Pending - * `enrolled` - Enrolled - * `waived` - Waived - * `inactive` - Inactive + - `EBA` - Eba Mec + - `VPC` - Vpc Enhanced + - `VPC_CORE` - Vpc Core + - `MEC` - Vpc Mec + - `MEC2` - Mec2 + - `MEC_PLUS` - Mec Plus + - `MVP` - Mvp + - `MVP2` - Mvp2 + - `MVPSL` - Mvpsl + - `MVPSL2` - Mvpsl2 + - `VD` - Dental + - `VV` - Vision + - `ICHRA` - Ichra + - `ICHRA_PREMIUM_PLUS` - Ichra Premium Plus + - `ICHRA_REIMBURSEMENT_ONLY` - Ichra Reimbursement Only """ - coverage_start: dt.date = pydantic.Field() - """ - Coverage period start date - """ - coverage_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Coverage period end date - """ +class Enrollment(BaseModel): + """One enrollment as it appears in a list row.""" - employee_deduction_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Employee monthly payroll deduction in cents - """ + id: str + """Unique enrollment identifier (enrl\\__\\**)""" - employer_contribution_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Employer monthly contribution in cents - """ + answered_at: Optional[datetime] = None + """When the employee enrolled or waived""" - answered_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When the employee enrolled or waived - """ + benefit: Benefit + """Nested benefit product summary""" - terminated_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When coverage was terminated - """ + coverage_end: Optional[date] = None + """Coverage period end date""" - created_at: dt.datetime = pydantic.Field() - """ - When the enrollment was created - """ + coverage_start: date + """Coverage period start date""" + + created_at: datetime + """When the enrollment was created""" + + employee_deduction_in_cents: Optional[int] = None + """Employee monthly payroll deduction in cents""" + + employee_id: str + """Employee ID (empl\\__\\**)""" + + employer_contribution_in_cents: Optional[int] = None + """Employer monthly contribution in cents""" + + employer_id: str + """Employer ID (empr\\__\\**)""" - updated_at: dt.datetime = pydantic.Field() + status: EnrollmentStatus """ - When the enrollment was last updated + - `pending` - Pending + - `enrolled` - Enrolled + - `waived` - Waived + - `inactive` - Inactive """ - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: + terminated_at: Optional[datetime] = None + """When coverage was terminated""" - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + updated_at: datetime + """When the enrollment was last updated""" diff --git a/src/vitable_connect/types/enrollment_detail.py b/src/vitable_connect/types/enrollment_detail.py deleted file mode 100644 index 32b5e42..0000000 --- a/src/vitable_connect/types/enrollment_detail.py +++ /dev/null @@ -1,102 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .benefit_summary import BenefitSummary -from .enrollment_status import EnrollmentStatus - - -class EnrollmentDetail(UniversalBaseModel): - """ - A single enrollment, including the enrolled plan's documents. - - The plan documents are resolved one enrollment at a time and are therefore not part of the - list row. - """ - - id: str = pydantic.Field() - """ - Unique enrollment identifier (enrl_*) - """ - - employee_id: str = pydantic.Field() - """ - Employee ID (empl_*) - """ - - employer_id: str = pydantic.Field() - """ - Employer ID (empr_*) - """ - - benefit: BenefitSummary = pydantic.Field() - """ - Nested benefit product summary - """ - - status: EnrollmentStatus = pydantic.Field() - """ - Enrollment status - - * `pending` - Pending - * `enrolled` - Enrolled - * `waived` - Waived - * `inactive` - Inactive - """ - - coverage_start: dt.date = pydantic.Field() - """ - Coverage period start date - """ - - coverage_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Coverage period end date - """ - - employee_deduction_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Employee monthly payroll deduction in cents - """ - - employer_contribution_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Employer monthly contribution in cents - """ - - answered_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When the employee enrolled or waived - """ - - terminated_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When coverage was terminated - """ - - created_at: dt.datetime = pydantic.Field() - """ - When the enrollment was created - """ - - updated_at: dt.datetime = pydantic.Field() - """ - When the enrollment was last updated - """ - - sbc_url: typing.Optional[str] = pydantic.Field(default=None) - """ - Summary of Benefits and Coverage (SBC) document URL for the enrolled plan; null when not on file. Only individual (ICHRA) plans carry an SBC — group plans have no SBC on record and always resolve to null. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/enrollment_id.py b/src/vitable_connect/types/enrollment_id.py deleted file mode 100644 index 5f192ad..0000000 --- a/src/vitable_connect/types/enrollment_id.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -EnrollmentId = str -""" -Unique enrollment identifier (enrl_*) -""" diff --git a/src/vitable_connect/types/enrollment_list_response.py b/src/vitable_connect/types/enrollment_list_response.py deleted file mode 100644 index bcd99e3..0000000 --- a/src/vitable_connect/types/enrollment_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .enrollment import Enrollment -from .pagination import Pagination - - -class EnrollmentListResponse(UniversalBaseModel): - """ - Paginated list response containing enrollment resources. - """ - - data: typing.List[Enrollment] = pydantic.Field() - """ - This page of enrollments. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/enrollment_rate.py b/src/vitable_connect/types/enrollment_rate.py deleted file mode 100644 index 16b1c40..0000000 --- a/src/vitable_connect/types/enrollment_rate.py +++ /dev/null @@ -1,32 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class EnrollmentRate(UniversalBaseModel): - eligible: int = pydantic.Field() - """ - Employees eligible for this plan year. - """ - - enrolled: int = pydantic.Field() - """ - Employees enrolled in this plan year. - """ - - percentage: int = pydantic.Field() - """ - ``enrolled / eligible`` whole-number percent (0 when none). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/enrollment_rate_summary.py b/src/vitable_connect/types/enrollment_rate_summary.py deleted file mode 100644 index bac3b8c..0000000 --- a/src/vitable_connect/types/enrollment_rate_summary.py +++ /dev/null @@ -1,32 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class EnrollmentRateSummary(UniversalBaseModel): - eligible: int = pydantic.Field() - """ - Employees eligible for at least one active benefit. - """ - - enrolled: int = pydantic.Field() - """ - Employees enrolled in at least one active benefit. - """ - - percentage: int = pydantic.Field() - """ - ``enrolled / eligible`` as a whole-number percent (0 when none eligible). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/enrollment_reissue_params.py b/src/vitable_connect/types/enrollment_reissue_params.py new file mode 100644 index 0000000..a0fa2e5 --- /dev/null +++ b/src/vitable_connect/types/enrollment_reissue_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["EnrollmentReissueParams"] + + +class EnrollmentReissueParams(TypedDict, total=False): + qualifying_life_event_id: Optional[str] + """Accepted member qualifying life event identifier (qle\\__\\**)""" + + reason: Optional[str] + """ + Audit reason for the reissue; required for user-backed callers and optional for + long-lived organization API-key callers + """ + + ticket_number: Optional[str] + """Optional support or operational ticket number""" diff --git a/src/vitable_connect/types/enrollment_reissue_response.py b/src/vitable_connect/types/enrollment_reissue_response.py new file mode 100644 index 0000000..03d6a42 --- /dev/null +++ b/src/vitable_connect/types/enrollment_reissue_response.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel + +__all__ = ["EnrollmentReissueResponse", "Data"] + + +class Data(BaseModel): + enrollment_id: str + """Opaque identifier for the new unanswered enrollment""" + + +class EnrollmentReissueResponse(BaseModel): + """Response containing a single reissue enrollment resource.""" + + data: Data diff --git a/src/vitable_connect/types/enrollment_response.py b/src/vitable_connect/types/enrollment_response.py deleted file mode 100644 index 2e2ba86..0000000 --- a/src/vitable_connect/types/enrollment_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .enrollment_detail import EnrollmentDetail - - -class EnrollmentResponse(UniversalBaseModel): - """ - Response containing a single enrollment resource. - """ - - data: EnrollmentDetail = pydantic.Field() - """ - The enrollment, including its elected plans. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/enrollment_retrieve_response.py b/src/vitable_connect/types/enrollment_retrieve_response.py new file mode 100644 index 0000000..71ddd56 --- /dev/null +++ b/src/vitable_connect/types/enrollment_retrieve_response.py @@ -0,0 +1,133 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import date, datetime +from typing_extensions import Literal + +from .._models import BaseModel +from .enrollment_status import EnrollmentStatus + +__all__ = ["EnrollmentRetrieveResponse", "Data", "DataBenefit"] + + +class DataBenefit(BaseModel): + """Nested benefit product summary""" + + id: str + """Benefit product ID (bprd\\__\\**)""" + + category: Literal["Medical", "Dental", "Vision", "Hospital"] + """ + - `Medical` - Medical + - `Dental` - Dental + - `Vision` - Vision + - `Hospital` - Hospital + """ + + name: str + """Display name of the benefit product""" + + product_code: Literal[ + "EBA", + "VPC", + "VPC_CORE", + "MEC", + "MEC2", + "MEC_PLUS", + "MVP", + "MVP2", + "MVPSL", + "MVPSL2", + "VD", + "VV", + "ICHRA", + "ICHRA_PREMIUM_PLUS", + "ICHRA_REIMBURSEMENT_ONLY", + ] + """ + - `EBA` - Eba Mec + - `VPC` - Vpc Enhanced + - `VPC_CORE` - Vpc Core + - `MEC` - Vpc Mec + - `MEC2` - Mec2 + - `MEC_PLUS` - Mec Plus + - `MVP` - Mvp + - `MVP2` - Mvp2 + - `MVPSL` - Mvpsl + - `MVPSL2` - Mvpsl2 + - `VD` - Dental + - `VV` - Vision + - `ICHRA` - Ichra + - `ICHRA_PREMIUM_PLUS` - Ichra Premium Plus + - `ICHRA_REIMBURSEMENT_ONLY` - Ichra Reimbursement Only + """ + + +class Data(BaseModel): + """A single enrollment, including the enrolled plan's documents. + + The plan documents are resolved one enrollment at a time and are therefore not part of the + list row. + """ + + id: str + """Unique enrollment identifier (enrl\\__\\**)""" + + answered_at: Optional[datetime] = None + """When the employee enrolled or waived""" + + benefit: DataBenefit + """Nested benefit product summary""" + + coverage_end: Optional[date] = None + """Coverage period end date""" + + coverage_start: date + """Coverage period start date""" + + created_at: datetime + """When the enrollment was created""" + + employee_deduction_in_cents: Optional[int] = None + """Employee monthly payroll deduction in cents""" + + employee_id: str + """Employee ID (empl\\__\\**)""" + + employer_contribution_in_cents: Optional[int] = None + """Employer monthly contribution in cents""" + + employer_id: str + """Employer ID (empr\\__\\**)""" + + status: EnrollmentStatus + """ + - `pending` - Pending + - `enrolled` - Enrolled + - `waived` - Waived + - `inactive` - Inactive + """ + + terminated_at: Optional[datetime] = None + """When coverage was terminated""" + + updated_at: datetime + """When the enrollment was last updated""" + + sbc_url: Optional[str] = None + """ + Summary of Benefits and Coverage (SBC) document URL for the enrolled plan; null + when not on file. Only individual (ICHRA) plans carry an SBC — group plans have + no SBC on record and always resolve to null. + """ + + +class EnrollmentRetrieveResponse(BaseModel): + """Response containing a single enrollment resource.""" + + data: Data + """A single enrollment, including the enrolled plan's documents. + + The plan documents are resolved one enrollment at a time and are therefore not + part of the list row. + """ diff --git a/src/vitable_connect/types/enrollment_status.py b/src/vitable_connect/types/enrollment_status.py index 8d9d4ca..b8b8f78 100644 --- a/src/vitable_connect/types/enrollment_status.py +++ b/src/vitable_connect/types/enrollment_status.py @@ -1,5 +1,7 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import typing +from typing_extensions import Literal, TypeAlias -EnrollmentStatus = typing.Union[typing.Literal["pending", "enrolled", "waived", "inactive"], typing.Any] +__all__ = ["EnrollmentStatus"] + +EnrollmentStatus: TypeAlias = Literal["pending", "enrolled", "waived", "inactive"] diff --git a/src/vitable_connect/types/enrollment_terminate_params.py b/src/vitable_connect/types/enrollment_terminate_params.py new file mode 100644 index 0000000..28c7273 --- /dev/null +++ b/src/vitable_connect/types/enrollment_terminate_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["EnrollmentTerminateParams"] + + +class EnrollmentTerminateParams(TypedDict, total=False): + qualifying_life_event_id: Optional[str] + """Accepted member qualifying life event identifier (qle\\__\\**)""" + + reason: Optional[str] + """ + Audit reason for the termination; required for user-backed callers and optional + for long-lived organization API-key callers + """ + + ticket_number: Optional[str] + """Optional support or operational ticket number""" diff --git a/src/vitable_connect/types/error_response.py b/src/vitable_connect/types/error_response.py deleted file mode 100644 index d1300cb..0000000 --- a/src/vitable_connect/types/error_response.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class ErrorResponse(UniversalBaseModel): - timestamp: dt.datetime = pydantic.Field() - """ - ISO 8601 timestamp when the error occurred - """ - - message: str = pydantic.Field() - """ - Human-readable error message - """ - - error: str = pydantic.Field() - """ - Error message (same as message, for backwards compatibility) - """ - - app_error_code: typing.Optional[str] = pydantic.Field(default=None) - """ - Application-specific error code for programmatic handling - """ - - trace_id: str = pydantic.Field() - """ - Unique trace ID for debugging and support requests - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/event_id.py b/src/vitable_connect/types/event_id.py deleted file mode 100644 index 8927a4f..0000000 --- a/src/vitable_connect/types/event_id.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -EventId = str diff --git a/src/vitable_connect/types/event_name.py b/src/vitable_connect/types/event_name.py deleted file mode 100644 index b406699..0000000 --- a/src/vitable_connect/types/event_name.py +++ /dev/null @@ -1,20 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -EventName = typing.Union[ - typing.Literal[ - "enrollment.accepted", - "enrollment.terminated", - "enrollment.termination_rescheduled", - "enrollment.elected", - "enrollment.granted", - "enrollment.waived", - "enrollment.started", - "employee.eligibility_granted", - "employee.eligibility_terminated", - "employee.deactivated", - "employee.deduction_created", - ], - typing.Any, -] diff --git a/src/vitable_connect/types/gender.py b/src/vitable_connect/types/gender.py deleted file mode 100644 index 9942446..0000000 --- a/src/vitable_connect/types/gender.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -Gender = typing.Union[ - typing.Literal["Male", "Female", "Transgender", "Non-binary", "Prefer not to respond"], typing.Any -] diff --git a/src/vitable_connect/types/grant_type.py b/src/vitable_connect/types/grant_type.py deleted file mode 100644 index ff881cf..0000000 --- a/src/vitable_connect/types/grant_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -GrantType = typing.Union[typing.Literal["client_credentials"], typing.Any] diff --git a/src/vitable_connect/types/group.py b/src/vitable_connect/types/group.py index 77b756f..161e728 100644 --- a/src/vitable_connect/types/group.py +++ b/src/vitable_connect/types/group.py @@ -1,48 +1,28 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class Group(UniversalBaseModel): - id: str = pydantic.Field() - """ - Prefixed group identifier (`grp_`). - """ - - organization_id: str = pydantic.Field() - """ - Prefixed organization identifier (`org_`). - """ - - name: str = pydantic.Field() - """ - Human-readable group name. - """ - - external_reference_id: str = pydantic.Field() - """ - Stable identifier for this group in the integrator's own system. - """ - - created_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - Group creation timestamp (ISO 8601, UTC). - """ - - updated_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - Last-update timestamp (ISO 8601, UTC). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["Group"] + + +class Group(BaseModel): + id: str + """Prefixed group identifier (`grp_`).""" + + created_at: Optional[datetime] = None + """Group creation timestamp (ISO 8601, UTC).""" + + external_reference_id: str + """Stable identifier for this group in the integrator's own system.""" + + name: str + """Human-readable group name.""" + + organization_id: str + """Prefixed organization identifier (`org_`).""" + + updated_at: Optional[datetime] = None + """Last-update timestamp (ISO 8601, UTC).""" diff --git a/src/vitable_connect/types/group_create_params.py b/src/vitable_connect/types/group_create_params.py new file mode 100644 index 0000000..642eb0b --- /dev/null +++ b/src/vitable_connect/types/group_create_params.py @@ -0,0 +1,13 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["GroupCreateParams"] + + +class GroupCreateParams(TypedDict, total=False): + external_reference_id: Required[str] + + name: Required[str] diff --git a/src/vitable_connect/types/group_id.py b/src/vitable_connect/types/group_id.py deleted file mode 100644 index c246e38..0000000 --- a/src/vitable_connect/types/group_id.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -GroupId = str -""" -Unique group identifier (grp_*) -""" diff --git a/src/vitable_connect/types/group_list_params.py b/src/vitable_connect/types/group_list_params.py new file mode 100644 index 0000000..94b2d21 --- /dev/null +++ b/src/vitable_connect/types/group_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["GroupListParams"] + + +class GroupListParams(TypedDict, total=False): + limit: int + """Items per page (default: 20, max: 100)""" + + page: int + """Page number (default: 1)""" diff --git a/src/vitable_connect/types/group_list_response.py b/src/vitable_connect/types/group_list_response.py deleted file mode 100644 index 678cb27..0000000 --- a/src/vitable_connect/types/group_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .group import Group -from .pagination import Pagination - - -class GroupListResponse(UniversalBaseModel): - """ - Paginated list response containing group resources. - """ - - data: typing.List[Group] = pydantic.Field() - """ - This page of groups. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/group_member_sync_detail_response.py b/src/vitable_connect/types/group_member_sync_detail_response.py deleted file mode 100644 index 459d1ce..0000000 --- a/src/vitable_connect/types/group_member_sync_detail_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .group_member_sync_response import GroupMemberSyncResponse - - -class GroupMemberSyncDetailResponse(UniversalBaseModel): - """ - Response containing a single group member sync detail resource. - """ - - data: GroupMemberSyncResponse = pydantic.Field() - """ - The group member sync request, including its current processing status. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/group_member_sync_failure.py b/src/vitable_connect/types/group_member_sync_failure.py deleted file mode 100644 index 73fd3db..0000000 --- a/src/vitable_connect/types/group_member_sync_failure.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .operation import Operation - - -class GroupMemberSyncFailure(UniversalBaseModel): - reference_id: str = pydantic.Field() - """ - Your identifier for the member that failed to sync. - """ - - operation: Operation = pydantic.Field() - """ - The sync operation that failed for this member. - """ - - reason: str = pydantic.Field() - """ - Human-readable explanation of why the operation failed. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/group_member_sync_member_request.py b/src/vitable_connect/types/group_member_sync_member_request.py deleted file mode 100644 index 12ba5c3..0000000 --- a/src/vitable_connect/types/group_member_sync_member_request.py +++ /dev/null @@ -1,59 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .address_request import AddressRequest - - -class GroupMemberSyncMemberRequest(UniversalBaseModel): - reference_id: str = pydantic.Field() - """ - Your own identifier for this member. Vitable echoes it back on sync failures so you can match them to your records. - """ - - first_name: str = pydantic.Field() - """ - Member's legal first name. - """ - - last_name: str = pydantic.Field() - """ - Member's legal last name. - """ - - date_of_birth: dt.date = pydantic.Field() - """ - Member's date of birth as `YYYY-MM-DD`. - """ - - phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Member's contact phone number in E.164 format. - """ - - plan_id: str = pydantic.Field() - """ - Plan the member should be subscribed to (`plan_*`). - """ - - address: AddressRequest = pydantic.Field() - """ - The member's postal address. - """ - - email: typing.Optional[str] = pydantic.Field(default=None) - """ - Member's email address. Vitable sends care and coverage notices here. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/group_member_sync_request_detail.py b/src/vitable_connect/types/group_member_sync_request_detail.py deleted file mode 100644 index 737d461..0000000 --- a/src/vitable_connect/types/group_member_sync_request_detail.py +++ /dev/null @@ -1,44 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .group_member_sync_results import GroupMemberSyncResults - - -class GroupMemberSyncRequestDetail(UniversalBaseModel): - request_id: str = pydantic.Field() - """ - Identifier for the sync request being reported on. - """ - - group_id: str = pydantic.Field() - """ - Group whose roster was synced (`grp_*`). - """ - - accepted_at: dt.datetime = pydantic.Field() - """ - When Vitable accepted the sync request for processing (RFC 3339). - """ - - completed_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When processing finished (RFC 3339), or `null` while the sync is still in progress. - """ - - results: typing.Optional[GroupMemberSyncResults] = pydantic.Field(default=None) - """ - Outcome of the sync, or `null` until processing completes. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/group_member_sync_request_detail_response.py b/src/vitable_connect/types/group_member_sync_request_detail_response.py deleted file mode 100644 index b22cd90..0000000 --- a/src/vitable_connect/types/group_member_sync_request_detail_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .group_member_sync_request_detail import GroupMemberSyncRequestDetail - - -class GroupMemberSyncRequestDetailResponse(UniversalBaseModel): - """ - Response containing a single group member sync request detail resource. - """ - - data: GroupMemberSyncRequestDetail = pydantic.Field() - """ - The group member sync request, including per-member results. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/group_member_sync_response.py b/src/vitable_connect/types/group_member_sync_response.py deleted file mode 100644 index a01957d..0000000 --- a/src/vitable_connect/types/group_member_sync_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class GroupMemberSyncResponse(UniversalBaseModel): - request_id: str = pydantic.Field() - """ - Identifier for this sync request. Pass it to `GET /v1/groups/{group_id}/members/sync/{request_id}` to poll for results. - """ - - group_id: str = pydantic.Field() - """ - Group whose roster is being synced (`grp_*`). - """ - - accepted_at: dt.datetime = pydantic.Field() - """ - When Vitable accepted the sync request for processing (RFC 3339). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/group_member_sync_results.py b/src/vitable_connect/types/group_member_sync_results.py deleted file mode 100644 index 3f65a11..0000000 --- a/src/vitable_connect/types/group_member_sync_results.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .group_member_sync_failure import GroupMemberSyncFailure - - -class GroupMemberSyncResults(UniversalBaseModel): - added_group_member_ids: typing.List[str] = pydantic.Field() - """ - IDs of members added to the group by this sync. - """ - - removed_group_member_ids: typing.List[str] = pydantic.Field() - """ - IDs of members removed from the group by this sync. - """ - - failures: typing.List[GroupMemberSyncFailure] = pydantic.Field() - """ - Members that could not be synced, with a reason for each. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/group_response.py b/src/vitable_connect/types/group_response.py index b019dcb..da2cf5e 100644 --- a/src/vitable_connect/types/group_response.py +++ b/src/vitable_connect/types/group_response.py @@ -1,27 +1,12 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel from .group import Group +from .._models import BaseModel +__all__ = ["GroupResponse"] -class GroupResponse(UniversalBaseModel): - """ - Response containing a single group resource. - """ - - data: Group = pydantic.Field() - """ - The group record. - """ - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: +class GroupResponse(BaseModel): + """Response containing a single group resource.""" - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + data: Group diff --git a/src/vitable_connect/types/group_update_params.py b/src/vitable_connect/types/group_update_params.py new file mode 100644 index 0000000..993e527 --- /dev/null +++ b/src/vitable_connect/types/group_update_params.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import TypedDict + +__all__ = ["GroupUpdateParams"] + + +class GroupUpdateParams(TypedDict, total=False): + external_reference_id: Optional[str] + + name: Optional[str] diff --git a/src/vitable_connect/types/groups/__init__.py b/src/vitable_connect/types/groups/__init__.py new file mode 100644 index 0000000..f8ee8b1 --- /dev/null +++ b/src/vitable_connect/types/groups/__init__.py @@ -0,0 +1,3 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations diff --git a/src/vitable_connect/types/groups/members/__init__.py b/src/vitable_connect/types/groups/members/__init__.py new file mode 100644 index 0000000..83bc15b --- /dev/null +++ b/src/vitable_connect/types/groups/members/__init__.py @@ -0,0 +1,7 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from .sync_submit_params import SyncSubmitParams as SyncSubmitParams +from .sync_submit_response import SyncSubmitResponse as SyncSubmitResponse +from .sync_retrieve_response import SyncRetrieveResponse as SyncRetrieveResponse diff --git a/src/vitable_connect/types/groups/members/sync_retrieve_response.py b/src/vitable_connect/types/groups/members/sync_retrieve_response.py new file mode 100644 index 0000000..5f2219e --- /dev/null +++ b/src/vitable_connect/types/groups/members/sync_retrieve_response.py @@ -0,0 +1,47 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime +from typing_extensions import Literal + +from ...._models import BaseModel + +__all__ = ["SyncRetrieveResponse", "Data", "DataResults", "DataResultsFailure"] + + +class DataResultsFailure(BaseModel): + operation: Literal["add", "remove"] + """ + - `add` - add + - `remove` - remove + """ + + reason: str + + reference_id: str + + +class DataResults(BaseModel): + added_group_member_ids: List[str] + + failures: List[DataResultsFailure] + + removed_group_member_ids: List[str] + + +class Data(BaseModel): + accepted_at: datetime + + completed_at: Optional[datetime] = None + + group_id: str + + request_id: str + + results: Optional[DataResults] = None + + +class SyncRetrieveResponse(BaseModel): + """Response containing a single group member sync request detail resource.""" + + data: Data diff --git a/src/vitable_connect/types/groups/members/sync_submit_params.py b/src/vitable_connect/types/groups/members/sync_submit_params.py new file mode 100644 index 0000000..6170772 --- /dev/null +++ b/src/vitable_connect/types/groups/members/sync_submit_params.py @@ -0,0 +1,45 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union, Iterable, Optional +from datetime import date +from typing_extensions import Required, Annotated, TypedDict + +from ...._utils import PropertyInfo + +__all__ = ["SyncSubmitParams", "Member", "MemberAddress"] + + +class SyncSubmitParams(TypedDict, total=False): + members: Required[Iterable[Member]] + + +class MemberAddress(TypedDict, total=False): + address_line_1: Required[str] + + city: Required[str] + + state: Required[str] + + zipcode: Required[str] + + address_line_2: Optional[str] + + +class Member(TypedDict, total=False): + address: Required[MemberAddress] + + date_of_birth: Required[Annotated[Union[str, date], PropertyInfo(format="iso8601")]] + + first_name: Required[str] + + last_name: Required[str] + + plan_id: Required[str] + + reference_id: Required[str] + + email: Optional[str] + + phone: Optional[str] diff --git a/src/vitable_connect/types/groups/members/sync_submit_response.py b/src/vitable_connect/types/groups/members/sync_submit_response.py new file mode 100644 index 0000000..3e5f0d3 --- /dev/null +++ b/src/vitable_connect/types/groups/members/sync_submit_response.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from datetime import datetime + +from ...._models import BaseModel + +__all__ = ["SyncSubmitResponse", "Data"] + + +class Data(BaseModel): + accepted_at: datetime + + group_id: str + + request_id: str + + +class SyncSubmitResponse(BaseModel): + """Response containing a single group member sync detail resource.""" + + data: Data diff --git a/src/vitable_connect/types/household_member.py b/src/vitable_connect/types/household_member.py deleted file mode 100644 index 448f404..0000000 --- a/src/vitable_connect/types/household_member.py +++ /dev/null @@ -1,68 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .household_member_relationship import HouseholdMemberRelationship -from .household_member_type import HouseholdMemberType - - -class HouseholdMember(UniversalBaseModel): - """ - Wire serializer for :class:`HouseholdMemberDTO` (one household participant). - """ - - member_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Member identifier with 'mbr_' prefix - """ - - first_name: str = pydantic.Field() - """ - Household member's first name - """ - - last_name: str = pydantic.Field() - """ - Household member's last name - """ - - relationship: typing.Optional[HouseholdMemberRelationship] = pydantic.Field(default=None) - """ - Relationship to the head of household; null for the account holder themselves - - * `Child` - Child - * `Spouse` - Spouse - * `Roommate` - Roommate - * `Other` - Other - """ - - member_type: HouseholdMemberType = pydantic.Field() - """ - What this participant is to the household: `Account Holder`, `Dependent`, or `Inactive`. - - * `Account Holder` - Account Holder - * `Dependent` - Dependent - * `Inactive` - Inactive - """ - - date_of_birth: dt.date = pydantic.Field() - """ - Date of birth (YYYY-MM-DD) - """ - - household_admin_in: bool = pydantic.Field() - """ - Whether this participant is a household admin (the account holder always is) - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/household_member_relationship.py b/src/vitable_connect/types/household_member_relationship.py deleted file mode 100644 index 777b859..0000000 --- a/src/vitable_connect/types/household_member_relationship.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -HouseholdMemberRelationship = typing.Union[typing.Literal["Child", "Spouse", "Roommate", "Other"], typing.Any] diff --git a/src/vitable_connect/types/household_member_type.py b/src/vitable_connect/types/household_member_type.py deleted file mode 100644 index dbd532d..0000000 --- a/src/vitable_connect/types/household_member_type.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -HouseholdMemberType = typing.Union[typing.Literal["Account Holder", "Dependent", "Inactive"], typing.Any] diff --git a/src/vitable_connect/types/household_members_response.py b/src/vitable_connect/types/household_members_response.py deleted file mode 100644 index f674390..0000000 --- a/src/vitable_connect/types/household_members_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .household_member import HouseholdMember - - -class HouseholdMembersResponse(UniversalBaseModel): - """ - Unpaginated ``{"data": [...]}`` list of the members of a member's household. - """ - - data: typing.List[HouseholdMember] = pydantic.Field() - """ - The members of the household. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/hris_connection.py b/src/vitable_connect/types/hris_connection.py deleted file mode 100644 index 8082e26..0000000 --- a/src/vitable_connect/types/hris_connection.py +++ /dev/null @@ -1,43 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class HrisConnection(UniversalBaseModel): - provider: str = pydantic.Field() - """ - Id of the HRIS/payroll provider the employer is connected to (e.g. `paylocity`). - """ - - provider_label: str = pydantic.Field() - """ - Display name of that provider (e.g. `Paylocity`). - """ - - status: str = pydantic.Field() - """ - Connection status reported by the integration. - """ - - last_sync_on: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When the last sync completed, or null when none has. - """ - - synced_row_count: typing.Optional[int] = pydantic.Field(default=None) - """ - Rows in the latest completed sync, or null when none has. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/hris_status_item.py b/src/vitable_connect/types/hris_status_item.py deleted file mode 100644 index 8e34ad7..0000000 --- a/src/vitable_connect/types/hris_status_item.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -HrisStatusItem = typing.Union[typing.Literal["Pending", "Active", "Inactive", "Paused", "Terminated"], typing.Any] diff --git a/src/vitable_connect/types/ichra_contribution_class.py b/src/vitable_connect/types/ichra_contribution_class.py deleted file mode 100644 index aae9654..0000000 --- a/src/vitable_connect/types/ichra_contribution_class.py +++ /dev/null @@ -1,92 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .contribution_class_compensation import ContributionClassCompensation -from .contribution_class_employment import ContributionClassEmployment -from .contribution_class_location import ContributionClassLocation -from .coverage_tier import CoverageTier - - -class IchraContributionClass(UniversalBaseModel): - """ - One ICHRA contribution class, mirroring the internal configuration endpoint's - ``IchraContributionClassConfigurationEntitySerializer`` field-for-field. Two deliberate - differences for the public surface: the identifier is the opaque prefixed ``iccl_*`` form rather - than a raw UUID, and the matcher choices come from the domain enums rather than the model's. - """ - - contribution_class_id: str = pydantic.Field() - """ - Prefixed contribution-class identifier (`iccl_*`). - """ - - amount_in_cents: int = pydantic.Field() - """ - Monthly allowance in cents. - """ - - employment: ContributionClassEmployment = pydantic.Field() - """ - Employment matcher. - - * `Unspecified` - Unspecified - * `Full Time` - Full Time - * `Part Time` - Part Time - * `Temporary` - Temporary - * `Seasonal` - Seasonal - """ - - compensation: ContributionClassCompensation = pydantic.Field() - """ - Compensation matcher. - - * `Unspecified` - Unspecified - * `Salary` - Salary - * `Hourly` - Hourly - """ - - location: ContributionClassLocation = pydantic.Field() - """ - Location matcher type. - - * `Unspecified` - Unspecified - * `State` - State - """ - - location_value: typing.Optional[str] = pydantic.Field(default=None) - """ - Location matcher value (CSV of state codes), or null. - """ - - family_status: CoverageTier = pydantic.Field() - """ - Family-status matcher. - - * `Unspecified` - Unspecified - * `EE` - Ee - * `ES` - Es - * `EC` - Ec - * `EF` - Ef - """ - - min_age: typing.Optional[int] = pydantic.Field(default=None) - """ - Age-band lower bound, or null. - """ - - max_age: typing.Optional[int] = pydantic.Field(default=None) - """ - Age-band upper bound, or null. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/idp_provider.py b/src/vitable_connect/types/idp_provider.py deleted file mode 100644 index 33ad696..0000000 --- a/src/vitable_connect/types/idp_provider.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -IdpProvider = typing.Union[typing.Literal["workos", "vitable"], typing.Any] diff --git a/src/vitable_connect/types/include_cancelled.py b/src/vitable_connect/types/include_cancelled.py deleted file mode 100644 index b051fae..0000000 --- a/src/vitable_connect/types/include_cancelled.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -IncludeCancelled = bool -""" -Include cancelled employers (hidden by default unless their stage is explicitly requested). -""" diff --git a/src/vitable_connect/types/invoice.py b/src/vitable_connect/types/invoice.py deleted file mode 100644 index dd29784..0000000 --- a/src/vitable_connect/types/invoice.py +++ /dev/null @@ -1,37 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class Invoice(UniversalBaseModel): - invoice_id: str = pydantic.Field() - """ - Chargebee invoice id (external id, not a prefixed UUID). - """ - - period: typing.Optional[str] = pydantic.Field(default=None) - """ - Invoice date as an ISO string, or null. - """ - - total: typing.Optional[float] = pydantic.Field(default=None) - """ - Invoice total in dollars, or null. - """ - - status: typing.Optional[str] = pydantic.Field(default=None) - """ - Chargebee invoice status (e.g. `paid`), or null. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/invoice_id.py b/src/vitable_connect/types/invoice_id.py deleted file mode 100644 index eba57ac..0000000 --- a/src/vitable_connect/types/invoice_id.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -InvoiceId = str -""" -External Chargebee invoice id (not a prefixed UUID). -""" diff --git a/src/vitable_connect/types/invoices_cursor_pagination.py b/src/vitable_connect/types/invoices_cursor_pagination.py deleted file mode 100644 index 084a305..0000000 --- a/src/vitable_connect/types/invoices_cursor_pagination.py +++ /dev/null @@ -1,22 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class InvoicesCursorPagination(UniversalBaseModel): - next_offset: typing.Optional[str] = pydantic.Field(default=None) - """ - Opaque JSON-encoded cursor for the next page; null when there are no more pages. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/limit.py b/src/vitable_connect/types/limit.py deleted file mode 100644 index 93c60ad..0000000 --- a/src/vitable_connect/types/limit.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -Limit = int -""" -Items per page (default: 20, max: 100) -""" diff --git a/src/vitable_connect/types/list_webhook_event_deliveries_response.py b/src/vitable_connect/types/list_webhook_event_deliveries_response.py deleted file mode 100644 index 743c23d..0000000 --- a/src/vitable_connect/types/list_webhook_event_deliveries_response.py +++ /dev/null @@ -1,23 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .webhook_delivery import WebhookDelivery - - -class ListWebhookEventDeliveriesResponse(UniversalBaseModel): - data: typing.List[WebhookDelivery] = pydantic.Field() - """ - Delivery attempts for the webhook event, most recent first. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/marital_status.py b/src/vitable_connect/types/marital_status.py deleted file mode 100644 index 37afa63..0000000 --- a/src/vitable_connect/types/marital_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -MaritalStatus = typing.Union[typing.Literal["Single", "Married"], typing.Any] diff --git a/src/vitable_connect/types/member.py b/src/vitable_connect/types/member.py deleted file mode 100644 index e7aeb02..0000000 --- a/src/vitable_connect/types/member.py +++ /dev/null @@ -1,143 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .gender import Gender -from .marital_status import MaritalStatus -from .member_address import MemberAddress -from .member_status import MemberStatus -from .name_suffix import NameSuffix -from .preferred_language import PreferredLanguage -from .sex_at_birth import SexAtBirth - - -class Member(UniversalBaseModel): - """ - A member's profile: identity, contact details, address, demographics, and onboarding status. - """ - - id: typing.Optional[str] = pydantic.Field(default=None) - """ - Unique member identifier with 'mbr_' prefix - """ - - first_name: str = pydantic.Field() - """ - Member's legal first name - """ - - last_name: str = pydantic.Field() - """ - Member's legal last name - """ - - email: typing.Optional[str] = pydantic.Field(default=None) - """ - Email address - """ - - phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Phone number (10-digit US domestic string) - """ - - address: typing.Optional[MemberAddress] = pydantic.Field(default=None) - """ - Member's residential address - """ - - name: typing.Optional[str] = pydantic.Field(default=None) - """ - Member's full legal name - """ - - middle_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Member's legal middle name - """ - - suffix: typing.Optional[NameSuffix] = pydantic.Field(default=None) - """ - Member's name suffix - - * `Sr` - Senior - * `Jr` - Junior - * `I` - First - * `II` - Second - * `III` - Third - * `IV` - Fourth - * `V` - Fifth - """ - - date_of_birth: dt.date = pydantic.Field() - """ - Date of birth (YYYY-MM-DD) - """ - - age: int = pydantic.Field() - """ - Member's age in years, derived from date of birth - """ - - sex_at_birth: typing.Optional[SexAtBirth] = pydantic.Field(default=None) - """ - Sex assigned at birth, if provided - - * `Male` - Male - * `Female` - Female - * `Other` - Other - * `Unknown` - Unknown - """ - - gender: typing.Optional[Gender] = pydantic.Field(default=None) - """ - Member's gender identity - - * `Male` - Male - * `Female` - Female - * `Transgender` - Transgender - * `Non-binary` - Non Binary - * `Prefer not to respond` - Prefer Not To Respond - """ - - preferred_language: typing.Optional[PreferredLanguage] = pydantic.Field(default=None) - """ - Member's preferred language code (e.g., en, es) - - * `en` - English - * `es` - Spanish - * `zh` - Chinese - * `ru` - Russian - * `sw` - Swahili - * `th` - Thai - """ - - tobacco_status: typing.Optional[bool] = pydantic.Field(default=None) - """ - Whether the member uses tobacco, if known - """ - - marital_status: typing.Optional[MaritalStatus] = pydantic.Field(default=None) - """ - Member's marital status, if provided - - * `Single` - Single - * `Married` - Married - """ - - status: typing.Optional[MemberStatus] = pydantic.Field(default=None) - """ - Member profile status (onboarded or pending onboarding) - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_address.py b/src/vitable_connect/types/member_address.py deleted file mode 100644 index cfe1284..0000000 --- a/src/vitable_connect/types/member_address.py +++ /dev/null @@ -1,116 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata -from .state import State - - -class MemberAddress(UniversalBaseModel): - address_line1: typing_extensions.Annotated[ - str, - FieldMetadata(alias="address_line_1"), - pydantic.Field(alias="address_line_1", description="Primary street address"), - ] - """ - Primary street address - """ - - address_line2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="address_line_2"), - pydantic.Field(alias="address_line_2", description="Secondary street address (apt, suite, etc.)"), - ] = None - """ - Secondary street address (apt, suite, etc.) - """ - - city: str = pydantic.Field() - """ - City name - """ - - state: State = pydantic.Field() - """ - Two-letter state code (e.g., CA, NY) - - * `AL` - Al - * `AK` - Ak - * `AZ` - Az - * `AR` - Ar - * `CA` - Ca - * `CO` - Co - * `CT` - Ct - * `DC` - Dc - * `DE` - De - * `FL` - Fl - * `GA` - Ga - * `HI` - Hi - * `ID` - Id - * `IL` - Il - * `IN` - In - * `IA` - Ia - * `KS` - Ks - * `KY` - Ky - * `LA` - La - * `ME` - Me - * `MD` - Md - * `MA` - Ma - * `MI` - Mi - * `MN` - Mn - * `MS` - Ms - * `MO` - Mo - * `MT` - Mt - * `NE` - Ne - * `NV` - Nv - * `NH` - Nh - * `NJ` - Nj - * `NM` - Nm - * `NY` - Ny - * `NC` - Nc - * `ND` - Nd - * `OH` - Oh - * `OK` - Ok - * `OR` - Or - * `PA` - Pa - * `RI` - Ri - * `SC` - Sc - * `SD` - Sd - * `TN` - Tn - * `TX` - Tx - * `UT` - Ut - * `VT` - Vt - * `VA` - Va - * `WA` - Wa - * `WI` - Wi - * `WV` - Wv - * `WY` - Wy - * `PR` - Pr - * `GU` - Gu - * `AS` - As - * `VI` - Vi - * `MP` - Mp - * `MH` - Mh - * `PW` - Pw - * `FM` - Fm - * `AE` - Ae - * `AA` - Aa - * `AP` - Ap - """ - - zipcode: str = pydantic.Field() - """ - ZIP code (5 or 9 digit) - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_dependents_response.py b/src/vitable_connect/types/member_dependents_response.py deleted file mode 100644 index 3b187f9..0000000 --- a/src/vitable_connect/types/member_dependents_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .member_legal_dependent import MemberLegalDependent - - -class MemberDependentsResponse(UniversalBaseModel): - """ - Unpaginated ``{"data": [...]}`` list of member dependents. - """ - - data: typing.List[MemberLegalDependent] = pydantic.Field() - """ - The member's legal dependents. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_digital_benefit_cards_response.py b/src/vitable_connect/types/member_digital_benefit_cards_response.py deleted file mode 100644 index 8b985f3..0000000 --- a/src/vitable_connect/types/member_digital_benefit_cards_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .digital_benefit_card import DigitalBenefitCard - - -class MemberDigitalBenefitCardsResponse(UniversalBaseModel): - """ - Unpaginated ``{"data": [...]}`` list of member digital benefit cards. - """ - - data: typing.List[DigitalBenefitCard] = pydantic.Field() - """ - The member's digital benefit cards. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_employments_response.py b/src/vitable_connect/types/member_employments_response.py deleted file mode 100644 index a04b182..0000000 --- a/src/vitable_connect/types/member_employments_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .employee import Employee - - -class MemberEmploymentsResponse(UniversalBaseModel): - """ - Unpaginated ``{"data": [...]}`` list of member employments. - """ - - data: typing.List[Employee] = pydantic.Field() - """ - The member's employment records, one per employer. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_enrollment.py b/src/vitable_connect/types/member_enrollment.py deleted file mode 100644 index e5d1763..0000000 --- a/src/vitable_connect/types/member_enrollment.py +++ /dev/null @@ -1,182 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .category import Category -from .plan_year_enrollment_election_status import PlanYearEnrollmentElectionStatus -from .plan_year_enrollment_policy_status import PlanYearEnrollmentPolicyStatus -from .product_code import ProductCode - - -class MemberEnrollment(UniversalBaseModel): - """ - Wire serializer for :class:`MemberEnrollmentDTO` (one benefit enrollment row). - """ - - id: str = pydantic.Field() - """ - Opaque, stable enrollment identifier used to target enrollment actions - """ - - employer_id: str = pydantic.Field() - """ - Exact employer identifier for this enrollment - """ - - benefit_plan_year_id: str = pydantic.Field() - """ - Exact benefit plan-year identifier for this enrollment - """ - - benefit_type: Category = pydantic.Field() - """ - High-level benefit grouping (e.g., Medical, Dental) - - * `Medical` - Medical - * `Dental` - Dental - * `Vision` - Vision - * `Hospital` - Hospital - """ - - product_code: ProductCode = pydantic.Field() - """ - The benefit product this enrollment is for - - * `EBA` - Eba Mec - * `VPC` - Vpc Enhanced - * `VPC_CORE` - Vpc Core - * `MEC` - Vpc Mec - * `MEC2` - Mec2 - * `MEC_PLUS` - Mec Plus - * `MVP` - Mvp - * `MVP2` - Mvp2 - * `MVPSL` - Mvpsl - * `MVPSL2` - Mvpsl2 - * `VD` - Dental - * `VV` - Vision - * `ICHRA` - Ichra - * `ICHRA_PREMIUM_PLUS` - Ichra Premium Plus - * `ICHRA_REIMBURSEMENT_ONLY` - Ichra Reimbursement Only - """ - - requires_qle_for_reissue: bool = pydantic.Field() - """ - Whether a qualifying life event would be required to reissue this enrollment under the product and open-enrollment rule at the time this list was read - """ - - product_name: str = pydantic.Field() - """ - Display name of the benefit product - """ - - employer_name: str = pydantic.Field() - """ - Name of the employer the enrollment is through - """ - - carrier_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Insurance carrier name; null when no active carrier period is resolvable - """ - - plan_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Chosen benefit plan name; null unless the row is an election - """ - - tier_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Chosen benefit plan tier name (e.g., Employee Only); null unless the row is an election - """ - - employee_deduction_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Employee monthly payroll deduction in cents; null unless the row is an election - """ - - employer_contribution_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Employer monthly contribution in cents; null unless the row is an election - """ - - premium_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Total monthly plan premium in cents; null unless the row is an election - """ - - coverage_start: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Coverage window start date (YYYY-MM-DD) - """ - - coverage_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Coverage window end date (YYYY-MM-DD); null while coverage is open-ended - """ - - cancelled_date: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Earliest applicable coverage boundary (YYYY-MM-DD) when coverage was cancelled before its effective start; null unless the enrollment was cancelled - """ - - plan_year_coverage_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Benefit plan-year coverage end date (YYYY-MM-DD), distinct from this enrollment's coverage_end; null when the plan year is open-ended - """ - - issued_date: dt.date = pydantic.Field() - """ - Date the enrollment record was created (YYYY-MM-DD), the value Ops labels Issued on. Present on every row whatever the member answered, and distinct from `coverage_start`. It equals `enrollment_window_start` for a row issued once its open enrollment had already opened, and precedes it for a row issued ahead of open enrollment. - """ - - enrollment_window_start: dt.date = pydantic.Field() - """ - First date the member could answer this enrollment (YYYY-MM-DD). Open enrollment's start date for a row issued before open enrollment opened, otherwise the date the row was issued -- an enrollment is never answerable before it exists, so this is never earlier than `issued_date`. - """ - - enrollment_window_end: typing.Optional[dt.date] = pydantic.Field(default=None) - """ - Enrollment / open-enrollment window end date (YYYY-MM-DD); null when open-ended - """ - - election_status: PlanYearEnrollmentElectionStatus = pydantic.Field() - """ - Where the member stands: `Enrolled` or `Waived` if they answered, otherwise `Pending` while their enrollment window is open and `Expired` once it has closed. - - * `Enrolled` - Enrolled - * `Waived` - Waived - * `Pending` - Pending - * `Expired` - Expired - """ - - policy_status: typing.Optional[PlanYearEnrollmentPolicyStatus] = pydantic.Field(default=None) - """ - Where the member's coverage stands: `Coverage Upcoming`, `Coverage Effective`, `Coverage Ended`, or `Cancelled`. Null unless the member enrolled. - - * `Coverage Upcoming` - Coverage Upcoming - * `Coverage Effective` - Coverage Effective - * `Coverage Ended` - Coverage Ended - * `Cancelled` - Cancelled - """ - - in_last_month_of_coverage: bool = pydantic.Field() - """ - True when today falls in the final month of the plan-year coverage window; drives end-of-coverage enrollment actions on the client. - """ - - is_within_enrollment_window: bool = pydantic.Field() - """ - True when today falls inside the enrollment window this member has to answer in; drives enrollment-action availability on the client. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_enrollments_response.py b/src/vitable_connect/types/member_enrollments_response.py deleted file mode 100644 index 3e425f5..0000000 --- a/src/vitable_connect/types/member_enrollments_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .member_enrollment import MemberEnrollment - - -class MemberEnrollmentsResponse(UniversalBaseModel): - """ - Unpaginated ``{"data": [...]}`` list of member enrollments. - """ - - data: typing.List[MemberEnrollment] = pydantic.Field() - """ - The member's benefit enrollments. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_id.py b/src/vitable_connect/types/member_id.py deleted file mode 100644 index a267609..0000000 --- a/src/vitable_connect/types/member_id.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -MemberId = str -""" -Unique member identifier (mbr_*) -""" diff --git a/src/vitable_connect/types/member_legal_dependent.py b/src/vitable_connect/types/member_legal_dependent.py deleted file mode 100644 index 6a717bb..0000000 --- a/src/vitable_connect/types/member_legal_dependent.py +++ /dev/null @@ -1,72 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .relationship import Relationship -from .sex_at_birth import SexAtBirth - - -class MemberLegalDependent(UniversalBaseModel): - """ - Wire serializer for :class:`MemberLegalDependentDTO` (one legal-dependent row). - """ - - member_id: typing.Optional[str] = pydantic.Field(default=None) - """ - The dependent's own member identifier with 'mbr_' prefix - """ - - primary_member_id: typing.Optional[str] = pydantic.Field(default=None) - """ - The primary member's identifier with 'mbr_' prefix - """ - - first_name: str = pydantic.Field() - """ - Dependent's first name - """ - - last_name: str = pydantic.Field() - """ - Dependent's last name - """ - - relationship: Relationship = pydantic.Field() - """ - Relationship of the dependent to the member (e.g., Spouse, Child) - - * `Spouse` - Spouse - * `Child` - Child - """ - - date_of_birth: dt.date = pydantic.Field() - """ - Date of birth (YYYY-MM-DD) - """ - - age: int = pydantic.Field() - """ - Dependent's age in years, derived from date of birth - """ - - sex_at_birth: typing.Optional[SexAtBirth] = pydantic.Field(default=None) - """ - Sex assigned at birth, if provided - - * `Male` - Male - * `Female` - Female - * `Other` - Other - * `Unknown` - Unknown - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_list_dependents_response.py b/src/vitable_connect/types/member_list_dependents_response.py new file mode 100644 index 0000000..2788bec --- /dev/null +++ b/src/vitable_connect/types/member_list_dependents_response.py @@ -0,0 +1,51 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import date +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["MemberListDependentsResponse", "Data"] + + +class Data(BaseModel): + """Wire serializer for :class:`MemberLegalDependentDTO` (one legal-dependent row).""" + + age: int + """Dependent's age in years, derived from date of birth""" + + date_of_birth: date + """Date of birth (YYYY-MM-DD)""" + + first_name: str + """Dependent's first name""" + + last_name: str + """Dependent's last name""" + + member_id: str + """The dependent's own member identifier with 'mbr\\__' prefix""" + + primary_member_id: str + """The primary member's identifier with 'mbr\\__' prefix""" + + relationship: Literal["Spouse", "Child"] + """ + - `Spouse` - Spouse + - `Child` - Child + """ + + sex_at_birth: Optional[Literal["Male", "Female", "Other", "Unknown"]] = None + """ + - `Male` - Male + - `Female` - Female + - `Other` - Other + - `Unknown` - Unknown + """ + + +class MemberListDependentsResponse(BaseModel): + """Unpaginated ``{"data": [...]}`` list of member dependents.""" + + data: List[Data] diff --git a/src/vitable_connect/types/member_list_employments_response.py b/src/vitable_connect/types/member_list_employments_response.py new file mode 100644 index 0000000..a0ff7f3 --- /dev/null +++ b/src/vitable_connect/types/member_list_employments_response.py @@ -0,0 +1,14 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List + +from .._models import BaseModel +from .employee import Employee + +__all__ = ["MemberListEmploymentsResponse"] + + +class MemberListEmploymentsResponse(BaseModel): + """Unpaginated ``{"data": [...]}`` list of member employments.""" + + data: List[Employee] diff --git a/src/vitable_connect/types/member_list_enrollments_response.py b/src/vitable_connect/types/member_list_enrollments_response.py new file mode 100644 index 0000000..9abd4d0 --- /dev/null +++ b/src/vitable_connect/types/member_list_enrollments_response.py @@ -0,0 +1,170 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import date +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["MemberListEnrollmentsResponse", "Data"] + + +class Data(BaseModel): + """Wire serializer for :class:`MemberEnrollmentDTO` (one benefit enrollment row).""" + + id: str + """Opaque, stable enrollment identifier used to target enrollment actions""" + + benefit_plan_year_id: str + """Exact benefit plan-year identifier for this enrollment""" + + benefit_type: Literal["Medical", "Dental", "Vision", "Hospital"] + """ + - `Medical` - Medical + - `Dental` - Dental + - `Vision` - Vision + - `Hospital` - Hospital + """ + + cancelled_date: Optional[date] = None + """ + Earliest applicable coverage boundary (YYYY-MM-DD) when coverage was cancelled + before its effective start; null unless the enrollment was cancelled + """ + + election_status: Literal["Enrolled", "Waived", "Pending", "Expired"] + """ + - `Enrolled` - Enrolled + - `Waived` - Waived + - `Pending` - Pending + - `Expired` - Expired + """ + + employer_id: str + """Exact employer identifier for this enrollment""" + + employer_name: str + """Name of the employer the enrollment is through""" + + enrollment_window_start: date + """First date the member could answer this enrollment (YYYY-MM-DD). + + Open enrollment's start date for a row issued before open enrollment opened, + otherwise the date the row was issued -- an enrollment is never answerable + before it exists, so this is never earlier than `issued_date`. + """ + + in_last_month_of_coverage: bool + """ + True when today falls in the final month of the plan-year coverage window; + drives end-of-coverage enrollment actions on the client. + """ + + is_within_enrollment_window: bool + """ + True when today falls inside the enrollment window this member has to answer in; + drives enrollment-action availability on the client. + """ + + issued_date: date + """ + Date the enrollment record was created (YYYY-MM-DD), the value Ops labels Issued + on. Present on every row whatever the member answered, and distinct from + `coverage_start`. It equals `enrollment_window_start` for a row issued once its + open enrollment had already opened, and precedes it for a row issued ahead of + open enrollment. + """ + + plan_year_coverage_end: Optional[date] = None + """ + Benefit plan-year coverage end date (YYYY-MM-DD), distinct from this + enrollment's coverage_end; null when the plan year is open-ended + """ + + policy_status: Optional[Literal["Coverage Upcoming", "Coverage Effective", "Coverage Ended", "Cancelled"]] = None + """ + - `Coverage Upcoming` - Coverage Upcoming + - `Coverage Effective` - Coverage Effective + - `Coverage Ended` - Coverage Ended + - `Cancelled` - Cancelled + """ + + product_code: Literal[ + "EBA", + "VPC", + "VPC_CORE", + "MEC", + "MEC2", + "MEC_PLUS", + "MVP", + "MVP2", + "MVPSL", + "MVPSL2", + "VD", + "VV", + "ICHRA", + "ICHRA_PREMIUM_PLUS", + "ICHRA_REIMBURSEMENT_ONLY", + ] + """ + - `EBA` - Eba Mec + - `VPC` - Vpc Enhanced + - `VPC_CORE` - Vpc Core + - `MEC` - Vpc Mec + - `MEC2` - Mec2 + - `MEC_PLUS` - Mec Plus + - `MVP` - Mvp + - `MVP2` - Mvp2 + - `MVPSL` - Mvpsl + - `MVPSL2` - Mvpsl2 + - `VD` - Dental + - `VV` - Vision + - `ICHRA` - Ichra + - `ICHRA_PREMIUM_PLUS` - Ichra Premium Plus + - `ICHRA_REIMBURSEMENT_ONLY` - Ichra Reimbursement Only + """ + + product_name: str + """Display name of the benefit product""" + + requires_qle_for_reissue: bool + """ + Whether a qualifying life event would be required to reissue this enrollment + under the product and open-enrollment rule at the time this list was read + """ + + carrier_name: Optional[str] = None + """Insurance carrier name; null when no active carrier period is resolvable""" + + coverage_end: Optional[date] = None + """Coverage window end date (YYYY-MM-DD); null while coverage is open-ended""" + + coverage_start: Optional[date] = None + """Coverage window start date (YYYY-MM-DD)""" + + employee_deduction_in_cents: Optional[int] = None + """Employee monthly payroll deduction in cents; null unless the row is an election""" + + employer_contribution_in_cents: Optional[int] = None + """Employer monthly contribution in cents; null unless the row is an election""" + + enrollment_window_end: Optional[date] = None + """Enrollment / open-enrollment window end date (YYYY-MM-DD); null when open-ended""" + + plan_name: Optional[str] = None + """Chosen benefit plan name; null unless the row is an election""" + + premium_in_cents: Optional[int] = None + """Total monthly plan premium in cents; null unless the row is an election""" + + tier_name: Optional[str] = None + """ + Chosen benefit plan tier name (e.g., Employee Only); null unless the row is an + election + """ + + +class MemberListEnrollmentsResponse(BaseModel): + """Unpaginated ``{"data": [...]}`` list of member enrollments.""" + + data: List[Data] diff --git a/src/vitable_connect/types/member_list_id_cards_response.py b/src/vitable_connect/types/member_list_id_cards_response.py new file mode 100644 index 0000000..2d07a55 --- /dev/null +++ b/src/vitable_connect/types/member_list_id_cards_response.py @@ -0,0 +1,161 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["MemberListIDCardsResponse", "Data", "DataNetwork", "DataNetworkAddress"] + + +class DataNetworkAddress(BaseModel): + """Shared read serializer for a postal address on public API responses. + + One definition for the address block every public resource emits (employer, employee, …), so the + 5-field shape isn't hand-rolled per endpoint. Read-only: it renders an already-built address + value object (e.g. ``AddressDVO``) whose attributes map 1:1 to these fields. + """ + + address_line_1: str + """Primary street address.""" + + address_line_2: Optional[str] = None + """Secondary street address (apt, suite, etc.).""" + + city: str + """City name.""" + + state: str + """Two-letter state code (e.g. `CA`, `NY`).""" + + zipcode: str + """ZIP code (5 or 9 digit).""" + + +class DataNetwork(BaseModel): + """Provider network shown on the card; null for an rx card""" + + id: str + + address: DataNetworkAddress + """Shared read serializer for a postal address on public API responses. + + One definition for the address block every public resource emits (employer, + employee, …), so the 5-field shape isn't hand-rolled per endpoint. Read-only: it + renders an already-built address value object (e.g. `AddressDVO`) whose + attributes map 1:1 to these fields. + """ + + logo: Optional[str] = None + + member_phone: Optional[str] = None + + name: str + """Name of the network""" + + phone: str + + provider_phone: Optional[str] = None + + website: str + """Website of the network""" + + edi: Optional[str] = None + """Network's EDI""" + + member_website: Optional[str] = None + """Website for members""" + + provider_website: Optional[str] = None + """Website for providers""" + + +class Data(BaseModel): + """Wire serializer for :class:`DigitalBenefitCardDTO` (one benefit ID card).""" + + card_type: Literal["medical", "dental", "vision", "rx"] + """ + - `medical` - medical + - `dental` - dental + - `vision` - vision + - `rx` - rx + """ + + group_id: str + """Group number printed on the card (the rx group id for an rx card)""" + + group_member_id: str + """Member id printed on the card (the Ventegra cardholder id for an rx card)""" + + member_name: str + """Name of the member the card is issued to""" + + nsa_table: List[List[str]] + """No Surprises Act cost-sharing table rendered on the card; empty for an rx card""" + + benefit_code: Optional[ + Literal[ + "EBA", + "VPC", + "VPC_CORE", + "MEC", + "MEC2", + "MEC_PLUS", + "MVP", + "MVP2", + "MVPSL", + "MVPSL2", + "VD", + "VV", + "ICHRA", + "ICHRA_PREMIUM_PLUS", + "ICHRA_REIMBURSEMENT_ONLY", + ] + ] = None + """ + - `EBA` - Eba Mec + - `VPC` - Vpc Enhanced + - `VPC_CORE` - Vpc Core + - `MEC` - Vpc Mec + - `MEC2` - Mec2 + - `MEC_PLUS` - Mec Plus + - `MVP` - Mvp + - `MVP2` - Mvp2 + - `MVPSL` - Mvpsl + - `MVPSL2` - Mvpsl2 + - `VD` - Dental + - `VV` - Vision + - `ICHRA` - Ichra + - `ICHRA_PREMIUM_PLUS` - Ichra Premium Plus + - `ICHRA_REIMBURSEMENT_ONLY` - Ichra Reimbursement Only + """ + + carrier_phone: Optional[str] = None + """Carrier phone number on the card; null for an rx card""" + + carrier_website: Optional[str] = None + """Carrier website on the card; null for an rx card""" + + claims_payer_display_name: Optional[str] = None + """Claims payer shown on the card""" + + employer_name: Optional[str] = None + """Employer the card's coverage is through; null for an rx card without group info""" + + general_disclaimer: Optional[str] = None + """General disclaimer text; null for an rx card""" + + network: Optional[DataNetwork] = None + """Provider network shown on the card; null for an rx card""" + + plan_disclaimer: Optional[str] = None + """Plan-specific disclaimer text; null for an rx card""" + + plan_name: Optional[str] = None + """Benefit plan name on the card; null for a consumer-membership rx card""" + + +class MemberListIDCardsResponse(BaseModel): + """Unpaginated ``{"data": [...]}`` list of member digital benefit cards.""" + + data: List[Data] diff --git a/src/vitable_connect/types/member_list_item.py b/src/vitable_connect/types/member_list_item.py deleted file mode 100644 index 49da21e..0000000 --- a/src/vitable_connect/types/member_list_item.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .member_address import MemberAddress - - -class MemberListItem(UniversalBaseModel): - """ - A member in the organization's directory: identity, contact details, address, and join date. - """ - - id: typing.Optional[str] = pydantic.Field(default=None) - """ - Unique member identifier with 'mbr_' prefix - """ - - first_name: str = pydantic.Field() - """ - Member's legal first name - """ - - last_name: str = pydantic.Field() - """ - Member's legal last name - """ - - email: typing.Optional[str] = pydantic.Field(default=None) - """ - Email address - """ - - phone: typing.Optional[str] = pydantic.Field(default=None) - """ - Phone number (10-digit US domestic string) - """ - - address: typing.Optional[MemberAddress] = pydantic.Field(default=None) - """ - Member's residential address - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_list_params.py b/src/vitable_connect/types/member_list_params.py new file mode 100644 index 0000000..7626b78 --- /dev/null +++ b/src/vitable_connect/types/member_list_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["MemberListParams"] + + +class MemberListParams(TypedDict, total=False): + limit: int + """Items per page (default: 20, max: 100)""" + + page: int + """Page number (default: 1)""" + + search: str + """ + Case-insensitive search across member name, email, and phone number; exact match + on member id (prefixed or raw uuid) + """ diff --git a/src/vitable_connect/types/member_list_qualifying_life_events_params.py b/src/vitable_connect/types/member_list_qualifying_life_events_params.py new file mode 100644 index 0000000..4fb550f --- /dev/null +++ b/src/vitable_connect/types/member_list_qualifying_life_events_params.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, TypedDict + +__all__ = ["MemberListQualifyingLifeEventsParams"] + + +class MemberListQualifyingLifeEventsParams(TypedDict, total=False): + limit: int + """Items per page (default: 20, max: 100)""" + + page: int + """Page number (default: 1)""" + + status: Literal["approved", "denied", "pending"] + """Optional. Filter to a single QLE status; omit to return all statuses.""" diff --git a/src/vitable_connect/types/member_list_qualifying_life_events_response.py b/src/vitable_connect/types/member_list_qualifying_life_events_response.py new file mode 100644 index 0000000..3d5fff1 --- /dev/null +++ b/src/vitable_connect/types/member_list_qualifying_life_events_response.py @@ -0,0 +1,78 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import datetime +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["MemberListQualifyingLifeEventsResponse"] + + +class MemberListQualifyingLifeEventsResponse(BaseModel): + id: str + """Opaque qualifying life event identifier""" + + event_type: Literal[ + "lost_job_based_coverage", + "aged_off_parent_plan", + "lost_medicaid_chip_medicare", + "lost_decertified_individual_plan", + "married", + "divorced", + "had_baby", + "adopted_child", + "foster_care_placement", + "death_of_spouse_or_dependent", + "court_ordered", + "moved_to_new_coverage_area", + "moved_to_us", + "moved_to_or_from_school_housing", + "moved_to_or_from_seasonal_work_housing", + "became_us_citizen", + "left_incarceration", + "gained_tribal_status", + "started_or_ended_americorps_service", + "new_child", + "other", + ] + """ + - `lost_job_based_coverage` - lost_job_based_coverage + - `aged_off_parent_plan` - aged_off_parent_plan + - `lost_medicaid_chip_medicare` - lost_medicaid_chip_medicare + - `lost_decertified_individual_plan` - lost_decertified_individual_plan + - `married` - married + - `divorced` - divorced + - `had_baby` - had_baby + - `adopted_child` - adopted_child + - `foster_care_placement` - foster_care_placement + - `death_of_spouse_or_dependent` - death_of_spouse_or_dependent + - `court_ordered` - court_ordered + - `moved_to_new_coverage_area` - moved_to_new_coverage_area + - `moved_to_us` - moved_to_us + - `moved_to_or_from_school_housing` - moved_to_or_from_school_housing + - `moved_to_or_from_seasonal_work_housing` - + moved_to_or_from_seasonal_work_housing + - `became_us_citizen` - became_us_citizen + - `left_incarceration` - left_incarceration + - `gained_tribal_status` - gained_tribal_status + - `started_or_ended_americorps_service` - started_or_ended_americorps_service + - `new_child` - new_child + - `other` - other + """ + + event_type_label: str + """Human-readable label for event_type""" + + other_event: Optional[str] = None + """Custom event description when event_type is Other; otherwise normally null""" + + status: Literal["pending", "approved", "denied"] + """ + - `pending` - Pending + - `approved` - Approved + - `denied` - Denied + """ + + submitted_at: datetime + """When the member submitted the event""" diff --git a/src/vitable_connect/types/member_list_response.py b/src/vitable_connect/types/member_list_response.py index 821cf53..4bda986 100644 --- a/src/vitable_connect/types/member_list_response.py +++ b/src/vitable_connect/types/member_list_response.py @@ -1,33 +1,177 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import typing +from typing import Optional +from typing_extensions import Literal -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .member_list_item import MemberListItem -from .pagination import Pagination +from .._models import BaseModel +__all__ = ["MemberListResponse", "Address"] -class MemberListResponse(UniversalBaseModel): - """ - Paginated list response containing member resources. - """ - data: typing.List[MemberListItem] = pydantic.Field() +class Address(BaseModel): + """Member's residential address""" + + address_line_1: str + """Primary street address""" + + city: str + """City name""" + + state: Literal[ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DC", + "DE", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WI", + "WV", + "WY", + "PR", + "GU", + "AS", + "VI", + "MP", + "MH", + "PW", + "FM", + "AE", + "AA", + "AP", + ] """ - This page of members. + - `AL` - AL + - `AK` - AK + - `AZ` - AZ + - `AR` - AR + - `CA` - CA + - `CO` - CO + - `CT` - CT + - `DC` - DC + - `DE` - DE + - `FL` - FL + - `GA` - GA + - `HI` - HI + - `ID` - ID + - `IL` - IL + - `IN` - IN + - `IA` - IA + - `KS` - KS + - `KY` - KY + - `LA` - LA + - `ME` - ME + - `MD` - MD + - `MA` - MA + - `MI` - MI + - `MN` - MN + - `MS` - MS + - `MO` - MO + - `MT` - MT + - `NE` - NE + - `NV` - NV + - `NH` - NH + - `NJ` - NJ + - `NM` - NM + - `NY` - NY + - `NC` - NC + - `ND` - ND + - `OH` - OH + - `OK` - OK + - `OR` - OR + - `PA` - PA + - `RI` - RI + - `SC` - SC + - `SD` - SD + - `TN` - TN + - `TX` - TX + - `UT` - UT + - `VT` - VT + - `VA` - VA + - `WA` - WA + - `WI` - WI + - `WV` - WV + - `WY` - WY + - `PR` - PR + - `GU` - GU + - `AS` - AS + - `VI` - VI + - `MP` - MP + - `MH` - MH + - `PW` - PW + - `FM` - FM + - `AE` - AE + - `AA` - AA + - `AP` - AP """ - pagination: Pagination = pydantic.Field() + zipcode: str + """ZIP code (5 or 9 digit)""" + + address_line_2: Optional[str] = None + """Secondary street address (apt, suite, etc.)""" + + +class MemberListResponse(BaseModel): """ - Page metadata. Request the next page by incrementing `page`. + A member in the organization's directory: identity, contact details, address, and join date. """ - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: + id: str + """Unique member identifier with 'mbr\\__' prefix""" + + first_name: str + """Member's legal first name""" + + last_name: str + """Member's legal last name""" + + address: Optional[Address] = None + """Member's residential address""" + + email: Optional[str] = None + """Email address""" - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + phone: Optional[str] = None + """Phone number (10-digit US domestic string)""" diff --git a/src/vitable_connect/types/member_qualifying_life_event.py b/src/vitable_connect/types/member_qualifying_life_event.py deleted file mode 100644 index 22d742e..0000000 --- a/src/vitable_connect/types/member_qualifying_life_event.py +++ /dev/null @@ -1,76 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .qualifying_life_event_status import QualifyingLifeEventStatus -from .qualifying_life_event_type import QualifyingLifeEventType - - -class MemberQualifyingLifeEvent(UniversalBaseModel): - id: str = pydantic.Field() - """ - Opaque qualifying life event identifier - """ - - event_type: QualifyingLifeEventType = pydantic.Field() - """ - The member-submitted qualifying life event type - - * `lost_job_based_coverage` - Losing job-based health insurance - * `aged_off_parent_plan` - Aging off a parent's plan at age 26 - * `lost_medicaid_chip_medicare` - Losing Medicaid, CHIP, or Medicare eligibility - * `lost_decertified_individual_plan` - Losing individual coverage because a plan was decertified - * `married` - Getting married - * `divorced` - Getting divorced or legally separated - * `had_baby` - Having a baby - * `adopted_child` - Adopting a child - * `foster_care_placement` - Foster-care placement - * `death_of_spouse_or_dependent` - Death of a covered spouse or dependent - * `court_ordered` - Court order to add a dependent - * `moved_to_new_coverage_area` - Moving to a new ZIP code, county, or state where different plans are available - * `moved_to_us` - Moving to the U.S. from another country - * `moved_to_or_from_school_housing` - Moving to or from school housing - * `moved_to_or_from_seasonal_work_housing` - Moving to or from housing for seasonal work - * `became_us_citizen` - Becoming a U.S. citizen - * `left_incarceration` - Leaving incarceration - * `gained_tribal_status` - Gaining status as a member of a federally recognized tribe or Alaska Native corporation - * `started_or_ended_americorps_service` - Starting or ending AmeriCorps service - * `new_child` - New child - * `other` - Other - """ - - event_type_label: str = pydantic.Field() - """ - Human-readable label for event_type - """ - - other_event: typing.Optional[str] = pydantic.Field(default=None) - """ - Custom event description when event_type is Other; otherwise normally null - """ - - status: QualifyingLifeEventStatus = pydantic.Field() - """ - Review status of the qualifying life event - - * `pending` - Pending - * `approved` - Approved - * `denied` - Denied - """ - - submitted_at: dt.datetime = pydantic.Field() - """ - When the member submitted the event - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_qualifying_life_event_list_response.py b/src/vitable_connect/types/member_qualifying_life_event_list_response.py deleted file mode 100644 index f045457..0000000 --- a/src/vitable_connect/types/member_qualifying_life_event_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .member_qualifying_life_event import MemberQualifyingLifeEvent -from .pagination import Pagination - - -class MemberQualifyingLifeEventListResponse(UniversalBaseModel): - """ - Paginated list response containing member qualifying life event resources. - """ - - data: typing.List[MemberQualifyingLifeEvent] = pydantic.Field() - """ - This page of qualifying life events. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_response.py b/src/vitable_connect/types/member_response.py deleted file mode 100644 index 89bb09a..0000000 --- a/src/vitable_connect/types/member_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .member import Member - - -class MemberResponse(UniversalBaseModel): - """ - Response containing a single member resource. - """ - - data: Member = pydantic.Field() - """ - The member record. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/member_retrieve_household_response.py b/src/vitable_connect/types/member_retrieve_household_response.py new file mode 100644 index 0000000..8fb1b96 --- /dev/null +++ b/src/vitable_connect/types/member_retrieve_household_response.py @@ -0,0 +1,49 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import date +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["MemberRetrieveHouseholdResponse", "Data"] + + +class Data(BaseModel): + """Wire serializer for :class:`HouseholdMemberDTO` (one household participant).""" + + date_of_birth: date + """Date of birth (YYYY-MM-DD)""" + + first_name: str + """Household member's first name""" + + household_admin_in: bool + """Whether this participant is a household admin (the account holder always is)""" + + last_name: str + """Household member's last name""" + + member_id: str + """Member identifier with 'mbr\\__' prefix""" + + member_type: Literal["Account Holder", "Dependent", "Inactive"] + """ + - `Account Holder` - Account Holder + - `Dependent` - Dependent + - `Inactive` - Inactive + """ + + relationship: Optional[Literal["Child", "Spouse", "Roommate", "Other"]] = None + """ + - `Child` - Child + - `Spouse` - Spouse + - `Roommate` - Roommate + - `Other` - Other + """ + + +class MemberRetrieveHouseholdResponse(BaseModel): + """Unpaginated ``{"data": [...]}`` list of the members of a member's household.""" + + data: List[Data] diff --git a/src/vitable_connect/types/member_retrieve_response.py b/src/vitable_connect/types/member_retrieve_response.py new file mode 100644 index 0000000..d75e8b4 --- /dev/null +++ b/src/vitable_connect/types/member_retrieve_response.py @@ -0,0 +1,250 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from datetime import date +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["MemberRetrieveResponse", "Data", "DataAddress"] + + +class DataAddress(BaseModel): + """Member's residential address""" + + address_line_1: str + """Primary street address""" + + city: str + """City name""" + + state: Literal[ + "AL", + "AK", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DC", + "DE", + "FL", + "GA", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MN", + "MS", + "MO", + "MT", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "RI", + "SC", + "SD", + "TN", + "TX", + "UT", + "VT", + "VA", + "WA", + "WI", + "WV", + "WY", + "PR", + "GU", + "AS", + "VI", + "MP", + "MH", + "PW", + "FM", + "AE", + "AA", + "AP", + ] + """ + - `AL` - AL + - `AK` - AK + - `AZ` - AZ + - `AR` - AR + - `CA` - CA + - `CO` - CO + - `CT` - CT + - `DC` - DC + - `DE` - DE + - `FL` - FL + - `GA` - GA + - `HI` - HI + - `ID` - ID + - `IL` - IL + - `IN` - IN + - `IA` - IA + - `KS` - KS + - `KY` - KY + - `LA` - LA + - `ME` - ME + - `MD` - MD + - `MA` - MA + - `MI` - MI + - `MN` - MN + - `MS` - MS + - `MO` - MO + - `MT` - MT + - `NE` - NE + - `NV` - NV + - `NH` - NH + - `NJ` - NJ + - `NM` - NM + - `NY` - NY + - `NC` - NC + - `ND` - ND + - `OH` - OH + - `OK` - OK + - `OR` - OR + - `PA` - PA + - `RI` - RI + - `SC` - SC + - `SD` - SD + - `TN` - TN + - `TX` - TX + - `UT` - UT + - `VT` - VT + - `VA` - VA + - `WA` - WA + - `WI` - WI + - `WV` - WV + - `WY` - WY + - `PR` - PR + - `GU` - GU + - `AS` - AS + - `VI` - VI + - `MP` - MP + - `MH` - MH + - `PW` - PW + - `FM` - FM + - `AE` - AE + - `AA` - AA + - `AP` - AP + """ + + zipcode: str + """ZIP code (5 or 9 digit)""" + + address_line_2: Optional[str] = None + """Secondary street address (apt, suite, etc.)""" + + +class Data(BaseModel): + """ + A member's profile: identity, contact details, address, demographics, and onboarding status. + """ + + id: str + """Unique member identifier with 'mbr\\__' prefix""" + + age: int + """Member's age in years, derived from date of birth""" + + date_of_birth: date + """Date of birth (YYYY-MM-DD)""" + + first_name: str + """Member's legal first name""" + + last_name: str + """Member's legal last name""" + + name: str + """Member's full legal name""" + + status: Literal["onboarded", "pending_onboarding"] + """Member profile status (onboarded or pending onboarding)""" + + address: Optional[DataAddress] = None + """Member's residential address""" + + email: Optional[str] = None + """Email address""" + + gender: Optional[Literal["Male", "Female", "Transgender", "Non-binary", "Prefer not to respond"]] = None + """ + - `Male` - Male + - `Female` - Female + - `Transgender` - Transgender + - `Non-binary` - Non Binary + - `Prefer not to respond` - Prefer Not To Respond + """ + + marital_status: Optional[Literal["Single", "Married"]] = None + """ + - `Single` - Single + - `Married` - Married + """ + + middle_name: Optional[str] = None + """Member's legal middle name""" + + phone: Optional[str] = None + """Phone number (10-digit US domestic string)""" + + preferred_language: Optional[Literal["en", "es", "zh", "ru", "sw", "th"]] = None + """ + - `en` - English + - `es` - Spanish + - `zh` - Chinese + - `ru` - Russian + - `sw` - Swahili + - `th` - Thai + """ + + sex_at_birth: Optional[Literal["Male", "Female", "Other", "Unknown"]] = None + """ + - `Male` - Male + - `Female` - Female + - `Other` - Other + - `Unknown` - Unknown + """ + + suffix: Optional[Literal["Sr", "Jr", "I", "II", "III", "IV", "V"]] = None + """ + - `Sr` - Sr + - `Jr` - Jr + - `I` - I + - `II` - II + - `III` - III + - `IV` - IV + - `V` - V + """ + + tobacco_status: Optional[bool] = None + """Whether the member uses tobacco, if known""" + + +class MemberRetrieveResponse(BaseModel): + """Response containing a single member resource.""" + + data: Data + """ + A member's profile: identity, contact details, address, demographics, and + onboarding status. + """ diff --git a/src/vitable_connect/types/member_search.py b/src/vitable_connect/types/member_search.py deleted file mode 100644 index 7f9786b..0000000 --- a/src/vitable_connect/types/member_search.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -MemberSearch = str -""" -Case-insensitive search across member name, email, and phone number; exact match on member id (prefixed or raw uuid) -""" diff --git a/src/vitable_connect/types/member_status.py b/src/vitable_connect/types/member_status.py deleted file mode 100644 index 413341c..0000000 --- a/src/vitable_connect/types/member_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -MemberStatus = typing.Union[typing.Literal["onboarded", "pending_onboarding"], typing.Any] diff --git a/src/vitable_connect/types/missing_employee_resolution.py b/src/vitable_connect/types/missing_employee_resolution.py deleted file mode 100644 index 1de62c8..0000000 --- a/src/vitable_connect/types/missing_employee_resolution.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -MissingEmployeeResolution = typing.Union[typing.Literal["EMAIL_CENSUS", "SECOND_SYSTEM_ACCESS"], typing.Any] diff --git a/src/vitable_connect/types/name_suffix.py b/src/vitable_connect/types/name_suffix.py deleted file mode 100644 index 5b6921c..0000000 --- a/src/vitable_connect/types/name_suffix.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -NameSuffix = typing.Union[typing.Literal["Sr", "Jr", "I", "II", "III", "IV", "V"], typing.Any] diff --git a/src/vitable_connect/types/offset.py b/src/vitable_connect/types/offset.py deleted file mode 100644 index 2453005..0000000 --- a/src/vitable_connect/types/offset.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -Offset = typing.Optional[str] -""" -Opaque cursor from a previous page's next_offset -""" diff --git a/src/vitable_connect/types/operation.py b/src/vitable_connect/types/operation.py deleted file mode 100644 index c7ac883..0000000 --- a/src/vitable_connect/types/operation.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -Operation = typing.Union[typing.Literal["add", "remove"], typing.Any] diff --git a/src/vitable_connect/types/organization.py b/src/vitable_connect/types/organization.py deleted file mode 100644 index 367c85e..0000000 --- a/src/vitable_connect/types/organization.py +++ /dev/null @@ -1,59 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .idp_provider import IdpProvider -from .organization_type import OrganizationType - - -class Organization(UniversalBaseModel): - id: str = pydantic.Field() - """ - Prefixed organization identifier (`org_`). - """ - - name: str = pydantic.Field() - """ - Human-readable organization name. - """ - - type: typing.Optional[OrganizationType] = pydantic.Field(default=None) - """ - Partner type (e.g. BROKERAGE, TPA). - - * `BROKERAGE` - Brokerage - * `TPA` - TPA - * `GENERAL_AGENT` - General Agent - * `CHANNEL_PARTNER` - Channel Partner - * `CONSULTING_FIRM` - Consulting Firm - * `API_PLATFORM` - API Platform - """ - - idp_org_id: typing.Optional[str] = pydantic.Field(default=None) - """ - IdP-issued tenant id (WorkOS org id). - """ - - idp_provider: typing.Optional[IdpProvider] = pydantic.Field(default=None) - """ - IdP that minted idp_org_id (e.g. 'workos'). - - * `workos` - WorkOS - * `vitable` - Vitable - """ - - super_in: bool = pydantic.Field() - """ - Whether this organization reads across every organization. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/organization_create_params.py b/src/vitable_connect/types/organization_create_params.py new file mode 100644 index 0000000..1dc0659 --- /dev/null +++ b/src/vitable_connect/types/organization_create_params.py @@ -0,0 +1,22 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Optional +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["OrganizationCreateParams"] + + +class OrganizationCreateParams(TypedDict, total=False): + name: Required[str] + + type: Optional[Literal["BROKERAGE", "TPA", "GENERAL_AGENT", "CHANNEL_PARTNER", "CONSULTING_FIRM", "API_PLATFORM"]] + """ + - `BROKERAGE` - BROKERAGE + - `TPA` - TPA + - `GENERAL_AGENT` - GENERAL_AGENT + - `CHANNEL_PARTNER` - CHANNEL_PARTNER + - `CONSULTING_FIRM` - CONSULTING_FIRM + - `API_PLATFORM` - API_PLATFORM + """ diff --git a/src/vitable_connect/types/organization_create_response.py b/src/vitable_connect/types/organization_create_response.py new file mode 100644 index 0000000..1fbe862 --- /dev/null +++ b/src/vitable_connect/types/organization_create_response.py @@ -0,0 +1,40 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["OrganizationCreateResponse"] + + +class OrganizationCreateResponse(BaseModel): + id: str + """Prefixed organization identifier (`org_`).""" + + idp_org_id: Optional[str] = None + """IdP-issued tenant id (WorkOS org id).""" + + idp_provider: Optional[Literal["workos", "vitable"]] = None + """ + - `workos` - WorkOS + - `vitable` - Vitable + """ + + name: str + """Human-readable organization name.""" + + super_in: bool + """Whether this organization reads across every organization.""" + + type: Optional[ + Literal["BROKERAGE", "TPA", "GENERAL_AGENT", "CHANNEL_PARTNER", "CONSULTING_FIRM", "API_PLATFORM"] + ] = None + """ + - `BROKERAGE` - Brokerage + - `TPA` - TPA + - `GENERAL_AGENT` - General Agent + - `CHANNEL_PARTNER` - Channel Partner + - `CONSULTING_FIRM` - Consulting Firm + - `API_PLATFORM` - API Platform + """ diff --git a/src/vitable_connect/types/organization_employer.py b/src/vitable_connect/types/organization_employer.py deleted file mode 100644 index 6826978..0000000 --- a/src/vitable_connect/types/organization_employer.py +++ /dev/null @@ -1,110 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .address import Address -from .employer_benefit_lifecycle_stage import EmployerBenefitLifecycleStage -from .employer_hris_status import EmployerHrisStatus -from .enrollment_rate_summary import EnrollmentRateSummary - - -class OrganizationEmployer(UniversalBaseModel): - """ - One employer row of the organization's book (list projection). - - Carries the enriched/computed columns (enrollment roll-up, benefit-family tags, HRIS connection, - benefit-lifecycle stage) alongside the flat CRM fields of the underlying employer (legal name, - EIN, contact, address, timestamps) for parity with the legacy ``Employer`` contract. - """ - - employer_id: str = pydantic.Field() - """ - Prefixed employer identifier (`empr_`). - """ - - organization_id: typing.Optional[str] = pydantic.Field(default=None) - """ - ID of the parent organization (`org_*`), or null when unknown. - """ - - reference_id: typing.Optional[str] = pydantic.Field(default=None) - """ - The organization's own reference id for this employer, or null when none was assigned. - """ - - name: str = pydantic.Field() - """ - Employer name. - """ - - legal_name: typing.Optional[str] = pydantic.Field(default=None) - """ - Legal business name for compliance and tax purposes. - """ - - ein: typing.Optional[str] = pydantic.Field(default=None) - """ - Employer Identification Number (masked in responses). - """ - - email: typing.Optional[str] = pydantic.Field(default=None) - """ - Email address for billing and communications. - """ - - phone_number: typing.Optional[str] = pydantic.Field(default=None) - """ - Employer phone number. - """ - - active: bool = pydantic.Field() - """ - Whether the employer is currently active in the system. - """ - - address: Address = pydantic.Field() - """ - Employer's primary address. - """ - - created_at: dt.datetime = pydantic.Field() - """ - Timestamp when the employer was created. - """ - - updated_at: dt.datetime = pydantic.Field() - """ - Timestamp when the employer was last updated. - """ - - enrollment_rate_summary: EnrollmentRateSummary = pydantic.Field() - """ - Enrolled/eligible employees roll-up. - """ - - benefit_families: typing.List[str] = pydantic.Field() - """ - Distinct benefit-family tags across the employer's active benefits (e.g. `MEC`, `ICHRA`, `VPC`). - """ - - hris_status: typing.Optional[EmployerHrisStatus] = pydantic.Field(default=None) - """ - HRIS connection, or null when the employer has none. - """ - - benefit_lifecycle_stage: EmployerBenefitLifecycleStage = pydantic.Field() - """ - Where the employer currently sits in the benefits setup lifecycle. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/organization_employer_list_response.py b/src/vitable_connect/types/organization_employer_list_response.py deleted file mode 100644 index 8030dfd..0000000 --- a/src/vitable_connect/types/organization_employer_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .organization_employer import OrganizationEmployer -from .pagination import Pagination - - -class OrganizationEmployerListResponse(UniversalBaseModel): - """ - Paginated list response containing organization employer resources. - """ - - data: typing.List[OrganizationEmployer] = pydantic.Field() - """ - This page of employers in the organization. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/organization_hris_provider.py b/src/vitable_connect/types/organization_hris_provider.py deleted file mode 100644 index 5c93cb2..0000000 --- a/src/vitable_connect/types/organization_hris_provider.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class OrganizationHrisProvider(UniversalBaseModel): - provider: str = pydantic.Field() - """ - HRIS/payroll provider id, as stored on the connection (e.g. `adp_run`). Filter with this. - """ - - provider_label: str = pydantic.Field() - """ - Display name of that provider (e.g. `ADP Run`). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/organization_hris_providers_response.py b/src/vitable_connect/types/organization_hris_providers_response.py deleted file mode 100644 index c61c2d9..0000000 --- a/src/vitable_connect/types/organization_hris_providers_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .organization_hris_provider import OrganizationHrisProvider - - -class OrganizationHrisProvidersResponse(UniversalBaseModel): - """ - Unpaginated ``{"data": [...]}`` list of organization hris providers. - """ - - data: typing.List[OrganizationHrisProvider] = pydantic.Field() - """ - The HRIS providers available to the organization. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/organization_list_response.py b/src/vitable_connect/types/organization_list_response.py new file mode 100644 index 0000000..cddada6 --- /dev/null +++ b/src/vitable_connect/types/organization_list_response.py @@ -0,0 +1,49 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["OrganizationListResponse", "Organization"] + + +class Organization(BaseModel): + id: str + """Prefixed organization identifier (`org_`).""" + + idp_org_id: Optional[str] = None + """IdP-issued tenant id (WorkOS org id).""" + + idp_provider: Optional[Literal["workos", "vitable"]] = None + """ + - `workos` - WorkOS + - `vitable` - Vitable + """ + + name: str + """Human-readable organization name.""" + + super_in: bool + """Whether this organization reads across every organization.""" + + type: Optional[ + Literal["BROKERAGE", "TPA", "GENERAL_AGENT", "CHANNEL_PARTNER", "CONSULTING_FIRM", "API_PLATFORM"] + ] = None + """ + - `BROKERAGE` - Brokerage + - `TPA` - TPA + - `GENERAL_AGENT` - General Agent + - `CHANNEL_PARTNER` - Channel Partner + - `CONSULTING_FIRM` - Consulting Firm + - `API_PLATFORM` - API Platform + """ + + +class OrganizationListResponse(BaseModel): + """Envelope for the caller's organization memberships (paginated).""" + + organizations: List[Organization] + + total: int + """Total number of organizations the caller belongs to.""" diff --git a/src/vitable_connect/types/organization_type.py b/src/vitable_connect/types/organization_type.py deleted file mode 100644 index b900bb7..0000000 --- a/src/vitable_connect/types/organization_type.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -OrganizationType = typing.Union[ - typing.Literal["BROKERAGE", "TPA", "GENERAL_AGENT", "CHANNEL_PARTNER", "CONSULTING_FIRM", "API_PLATFORM"], - typing.Any, -] diff --git a/src/vitable_connect/types/organizations_list_response.py b/src/vitable_connect/types/organizations_list_response.py deleted file mode 100644 index 66daeb4..0000000 --- a/src/vitable_connect/types/organizations_list_response.py +++ /dev/null @@ -1,32 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .organization import Organization - - -class OrganizationsListResponse(UniversalBaseModel): - """ - Envelope for the caller's organization memberships (paginated). - """ - - organizations: typing.List[Organization] = pydantic.Field() - """ - This page of organizations. - """ - - total: int = pydantic.Field() - """ - Total number of organizations the caller belongs to. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/page.py b/src/vitable_connect/types/page.py deleted file mode 100644 index 229aa7d..0000000 --- a/src/vitable_connect/types/page.py +++ /dev/null @@ -1,6 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -Page = int -""" -Page number (default: 1) -""" diff --git a/src/vitable_connect/types/pagination.py b/src/vitable_connect/types/pagination.py index d30e02e..93a9a40 100644 --- a/src/vitable_connect/types/pagination.py +++ b/src/vitable_connect/types/pagination.py @@ -1,41 +1,21 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import typing +from .._models import BaseModel -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +__all__ = ["Pagination"] -class Pagination(UniversalBaseModel): - """ - Pagination metadata for list responses. - """ +class Pagination(BaseModel): + """Pagination metadata for list responses.""" - page: int = pydantic.Field() - """ - Current page number - """ + limit: int + """Items per page""" - limit: int = pydantic.Field() - """ - Items per page - """ + page: int + """Current page number""" - total: int = pydantic.Field() - """ - Total number of items - """ + total: int + """Total number of items""" - total_pages: int = pydantic.Field() - """ - Total number of pages - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + total_pages: int + """Total number of pages""" diff --git a/src/vitable_connect/types/payroll_access_setup_status.py b/src/vitable_connect/types/payroll_access_setup_status.py deleted file mode 100644 index 57ed806..0000000 --- a/src/vitable_connect/types/payroll_access_setup_status.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class PayrollAccessSetupStatus(UniversalBaseModel): - completed: bool = pydantic.Field() - """ - Whether payroll access setup has been completed for this employer. - """ - - submitted_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When the setup form was submitted (RFC 3339), or `null` if it has not been submitted. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/payroll_access_setup_status_response.py b/src/vitable_connect/types/payroll_access_setup_status_response.py deleted file mode 100644 index b31b3d3..0000000 --- a/src/vitable_connect/types/payroll_access_setup_status_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .payroll_access_setup_status import PayrollAccessSetupStatus - - -class PayrollAccessSetupStatusResponse(UniversalBaseModel): - """ - Response containing a single payroll access setup status resource. - """ - - data: PayrollAccessSetupStatus = pydantic.Field() - """ - Current status of the employer's payroll access setup. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/payroll_deduction_statement.py b/src/vitable_connect/types/payroll_deduction_statement.py deleted file mode 100644 index cb9c759..0000000 --- a/src/vitable_connect/types/payroll_deduction_statement.py +++ /dev/null @@ -1,76 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .deduction_frequency import DeductionFrequency - - -class PayrollDeductionStatement(UniversalBaseModel): - """ - One payroll-deduction statement row. - - Reads a :class:`PayrollDeductionStatementDTO` by attribute: the ``statement_id`` character field renders the - prefixed id via ``str()``, and the date/datetime fields emit ISO-8601 strings. - """ - - statement_id: str = pydantic.Field() - """ - Prefixed payroll-deduction-statement identifier (`pstmt_`). - """ - - period_start: dt.date = pydantic.Field() - """ - Deduction period start date. - """ - - period_end: dt.date = pydantic.Field() - """ - Deduction period end date. - """ - - run_date: dt.datetime = pydantic.Field() - """ - When the statement was generated. - """ - - employee_count: int = pydantic.Field() - """ - Distinct employees covered by the statement's entries. - """ - - total_deduction_cents: int = pydantic.Field() - """ - Total payroll deduction for the period, in cents. - """ - - csv_file_url: typing.Optional[str] = pydantic.Field(default=None) - """ - Download link for the change CSV, or null. - """ - - deduction_frequency: DeductionFrequency = pydantic.Field() - """ - Deduction frequency code (e.g. `monthly`). - - * `weekly` - Weekly - * `bi_weekly` - Bi Weekly - * `semi_monthly` - Semi Monthly - * `monthly` - Monthly - """ - - deduction_frequency_label: str = pydantic.Field() - """ - Human-readable deduction frequency (e.g. `Monthly`). - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/payroll_integration_email.py b/src/vitable_connect/types/payroll_integration_email.py deleted file mode 100644 index 41f2cc3..0000000 --- a/src/vitable_connect/types/payroll_integration_email.py +++ /dev/null @@ -1,22 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class PayrollIntegrationEmail(UniversalBaseModel): - integration_email: str = pydantic.Field() - """ - Email address Vitable uses to exchange payroll files with the employer. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/payroll_integration_email_response.py b/src/vitable_connect/types/payroll_integration_email_response.py deleted file mode 100644 index 1a17536..0000000 --- a/src/vitable_connect/types/payroll_integration_email_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .payroll_integration_email import PayrollIntegrationEmail - - -class PayrollIntegrationEmailResponse(UniversalBaseModel): - """ - Response containing a single payroll integration email resource. - """ - - data: PayrollIntegrationEmail = pydantic.Field() - """ - The payroll integration email address for the employer. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/plan.py b/src/vitable_connect/types/plan.py deleted file mode 100644 index faac2a4..0000000 --- a/src/vitable_connect/types/plan.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class Plan(UniversalBaseModel): - id: str = pydantic.Field() - """ - Unique identifier for the plan (`plan_*`). - """ - - name: str = pydantic.Field() - """ - Human-readable plan name as shown to members. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/plan_list_params.py b/src/vitable_connect/types/plan_list_params.py new file mode 100644 index 0000000..bbed71c --- /dev/null +++ b/src/vitable_connect/types/plan_list_params.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["PlanListParams"] + + +class PlanListParams(TypedDict, total=False): + limit: int + """Items per page (default: 20, max: 100)""" + + page: int + """Page number (default: 1)""" diff --git a/src/vitable_connect/types/plan_list_response.py b/src/vitable_connect/types/plan_list_response.py index 7406a59..a27b3b8 100644 --- a/src/vitable_connect/types/plan_list_response.py +++ b/src/vitable_connect/types/plan_list_response.py @@ -1,33 +1,11 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import typing +from .._models import BaseModel -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .pagination import Pagination -from .plan import Plan +__all__ = ["PlanListResponse"] -class PlanListResponse(UniversalBaseModel): - """ - Paginated list response containing plan resources. - """ +class PlanListResponse(BaseModel): + id: str - data: typing.List[Plan] = pydantic.Field() - """ - This page of plans. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + name: str diff --git a/src/vitable_connect/types/plan_year_enrollment.py b/src/vitable_connect/types/plan_year_enrollment.py deleted file mode 100644 index 38a28a5..0000000 --- a/src/vitable_connect/types/plan_year_enrollment.py +++ /dev/null @@ -1,99 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .plan_year_enrollment_election_status import PlanYearEnrollmentElectionStatus -from .plan_year_enrollment_policy_status import PlanYearEnrollmentPolicyStatus - - -class PlanYearEnrollment(UniversalBaseModel): - member_id: str = pydantic.Field() - """ - Our id for the person (`mbr_<...>`). Stable across every employer they work for. - """ - - employee_id: str = pydantic.Field() - """ - Our id for this person's employment with this employer (`empl_<...>`). A person who leaves and is rehired has two. - """ - - employee_external_reference_id: typing.Optional[str] = pydantic.Field(default=None) - """ - Your own reference id for this employee, as you supplied it. Null when you have not set one. - """ - - member_first_name: str = pydantic.Field() - """ - The member's first name. - """ - - member_last_name: str = pydantic.Field() - """ - The member's last name. - """ - - election_status: PlanYearEnrollmentElectionStatus = pydantic.Field() - """ - Where the member stands: `Enrolled` or `Waived` if they answered, otherwise `Pending` while their enrollment window is open and `Expired` once it has closed. - - * `Enrolled` - Enrolled - * `Waived` - Waived - * `Pending` - Pending - * `Expired` - Expired - """ - - policy_status: typing.Optional[PlanYearEnrollmentPolicyStatus] = pydantic.Field(default=None) - """ - Where the member's coverage stands: `Coverage Upcoming`, `Coverage Effective`, `Coverage Ended`, or `Cancelled`. Null unless the member enrolled. - - * `Coverage Upcoming` - Coverage Upcoming - * `Coverage Effective` - Coverage Effective - * `Coverage Ended` - Coverage Ended - * `Cancelled` - Cancelled - """ - - dependent_count: int = pydantic.Field() - """ - Dependents covered under this enrollment today. Counts the same dependents `premium_in_cents` is priced for, so a dependent whose termination is dated in the future still counts. - """ - - carrier: typing.Optional[str] = pydantic.Field(default=None) - """ - The carrier for this enrollment: the individual-market carrier for an ICHRA plan, otherwise the benefit's own. Null when the benefit has no carrier. - """ - - plan: typing.Optional[str] = pydantic.Field(default=None) - """ - Chosen plan name, or null when unanswered/waived. - """ - - tier: typing.Optional[str] = pydantic.Field(default=None) - """ - Chosen coverage tier, or null when unanswered/waived. - """ - - premium_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - Monthly premium in cents for the chosen plan, dependents included. The plan's own cost, not the employer's share of it. Null when unanswered/waived. - """ - - employer_contribution_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - The employer's monthly share of `premium_in_cents`, in cents. Null when unanswered/waived. - """ - - employee_deduction_in_cents: typing.Optional[int] = pydantic.Field(default=None) - """ - What the employee is deducted monthly, in cents: `premium_in_cents` less `employer_contribution_in_cents`, floored at zero. Null when unanswered/waived. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/plan_year_enrollment_election_status.py b/src/vitable_connect/types/plan_year_enrollment_election_status.py deleted file mode 100644 index 9320bb6..0000000 --- a/src/vitable_connect/types/plan_year_enrollment_election_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -PlanYearEnrollmentElectionStatus = typing.Union[typing.Literal["Enrolled", "Waived", "Pending", "Expired"], typing.Any] diff --git a/src/vitable_connect/types/plan_year_enrollment_list_response.py b/src/vitable_connect/types/plan_year_enrollment_list_response.py deleted file mode 100644 index 3c9e947..0000000 --- a/src/vitable_connect/types/plan_year_enrollment_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .pagination import Pagination -from .plan_year_enrollment import PlanYearEnrollment - - -class PlanYearEnrollmentListResponse(UniversalBaseModel): - """ - Paginated list response containing plan year enrollment resources. - """ - - data: typing.List[PlanYearEnrollment] = pydantic.Field() - """ - This page of plan year enrollments. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/plan_year_enrollment_policy_status.py b/src/vitable_connect/types/plan_year_enrollment_policy_status.py deleted file mode 100644 index 28185cf..0000000 --- a/src/vitable_connect/types/plan_year_enrollment_policy_status.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -PlanYearEnrollmentPolicyStatus = typing.Union[ - typing.Literal["Coverage Upcoming", "Coverage Effective", "Coverage Ended", "Cancelled"], typing.Any -] diff --git a/src/vitable_connect/types/preferred_language.py b/src/vitable_connect/types/preferred_language.py deleted file mode 100644 index 9e59199..0000000 --- a/src/vitable_connect/types/preferred_language.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -PreferredLanguage = typing.Union[typing.Literal["en", "es", "zh", "ru", "sw", "th"], typing.Any] diff --git a/src/vitable_connect/types/product_code.py b/src/vitable_connect/types/product_code.py deleted file mode 100644 index 089c998..0000000 --- a/src/vitable_connect/types/product_code.py +++ /dev/null @@ -1,24 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ProductCode = typing.Union[ - typing.Literal[ - "EBA", - "VPC", - "VPC_CORE", - "MEC", - "MEC2", - "MEC_PLUS", - "MVP", - "MVP2", - "MVPSL", - "MVPSL2", - "VD", - "VV", - "ICHRA", - "ICHRA_PREMIUM_PLUS", - "ICHRA_REIMBURSEMENT_ONLY", - ], - typing.Any, -] diff --git a/src/vitable_connect/types/qualifying_life_event_status.py b/src/vitable_connect/types/qualifying_life_event_status.py deleted file mode 100644 index 3259591..0000000 --- a/src/vitable_connect/types/qualifying_life_event_status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -QualifyingLifeEventStatus = typing.Union[typing.Literal["pending", "approved", "denied"], typing.Any] diff --git a/src/vitable_connect/types/qualifying_life_event_type.py b/src/vitable_connect/types/qualifying_life_event_type.py deleted file mode 100644 index 0529708..0000000 --- a/src/vitable_connect/types/qualifying_life_event_type.py +++ /dev/null @@ -1,30 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -QualifyingLifeEventType = typing.Union[ - typing.Literal[ - "lost_job_based_coverage", - "aged_off_parent_plan", - "lost_medicaid_chip_medicare", - "lost_decertified_individual_plan", - "married", - "divorced", - "had_baby", - "adopted_child", - "foster_care_placement", - "death_of_spouse_or_dependent", - "court_ordered", - "moved_to_new_coverage_area", - "moved_to_us", - "moved_to_or_from_school_housing", - "moved_to_or_from_seasonal_work_housing", - "became_us_citizen", - "left_incarceration", - "gained_tribal_status", - "started_or_ended_americorps_service", - "new_child", - "other", - ], - typing.Any, -] diff --git a/src/vitable_connect/types/reissue_enrollment.py b/src/vitable_connect/types/reissue_enrollment.py deleted file mode 100644 index 22d153e..0000000 --- a/src/vitable_connect/types/reissue_enrollment.py +++ /dev/null @@ -1,22 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class ReissueEnrollment(UniversalBaseModel): - enrollment_id: str = pydantic.Field() - """ - Opaque identifier for the new unanswered enrollment - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/reissue_enrollment_response.py b/src/vitable_connect/types/reissue_enrollment_response.py deleted file mode 100644 index 48da1d3..0000000 --- a/src/vitable_connect/types/reissue_enrollment_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .reissue_enrollment import ReissueEnrollment - - -class ReissueEnrollmentResponse(UniversalBaseModel): - """ - Response containing a single reissue enrollment resource. - """ - - data: ReissueEnrollment = pydantic.Field() - """ - The reissued enrollment. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/relationship.py b/src/vitable_connect/types/relationship.py deleted file mode 100644 index 33db45d..0000000 --- a/src/vitable_connect/types/relationship.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -Relationship = typing.Union[typing.Literal["Spouse", "Child"], typing.Any] diff --git a/src/vitable_connect/types/remaining_employee_action.py b/src/vitable_connect/types/remaining_employee_action.py deleted file mode 100644 index da1ef5f..0000000 --- a/src/vitable_connect/types/remaining_employee_action.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -RemainingEmployeeAction = typing.Union[typing.Literal["VITABLE_UPDATE", "EMPLOYER_UPDATE"], typing.Any] diff --git a/src/vitable_connect/types/request_id.py b/src/vitable_connect/types/request_id.py deleted file mode 100644 index 7d06510..0000000 --- a/src/vitable_connect/types/request_id.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -RequestId = str diff --git a/src/vitable_connect/types/resource_id.py b/src/vitable_connect/types/resource_id.py deleted file mode 100644 index 211b410..0000000 --- a/src/vitable_connect/types/resource_id.py +++ /dev/null @@ -1,3 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -ResourceId = str diff --git a/src/vitable_connect/types/resource_type.py b/src/vitable_connect/types/resource_type.py deleted file mode 100644 index eb6a855..0000000 --- a/src/vitable_connect/types/resource_type.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -ResourceType = typing.Union[ - typing.Literal["enrollment", "employee", "employer", "dependent", "plan_year", "payroll_deduction"], typing.Any -] diff --git a/src/vitable_connect/types/search.py b/src/vitable_connect/types/search.py deleted file mode 100644 index c6ea1b4..0000000 --- a/src/vitable_connect/types/search.py +++ /dev/null @@ -1,8 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -Search = typing.Optional[str] -""" -Employer filter. Matches the display name or the legal name case-insensitively as a substring, or one of these exactly: the EIN (with or without its dash), the employer id, or the contact email of one of the employer's non-disabled admins. -""" diff --git a/src/vitable_connect/types/sex_at_birth.py b/src/vitable_connect/types/sex_at_birth.py deleted file mode 100644 index b64ab85..0000000 --- a/src/vitable_connect/types/sex_at_birth.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -SexAtBirth = typing.Union[typing.Literal["Male", "Female", "Other", "Unknown"], typing.Any] diff --git a/src/vitable_connect/types/state.py b/src/vitable_connect/types/state.py deleted file mode 100644 index 9eaaa0b..0000000 --- a/src/vitable_connect/types/state.py +++ /dev/null @@ -1,71 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -State = typing.Union[ - typing.Literal[ - "AL", - "AK", - "AZ", - "AR", - "CA", - "CO", - "CT", - "DC", - "DE", - "FL", - "GA", - "HI", - "ID", - "IL", - "IN", - "IA", - "KS", - "KY", - "LA", - "ME", - "MD", - "MA", - "MI", - "MN", - "MS", - "MO", - "MT", - "NE", - "NV", - "NH", - "NJ", - "NM", - "NY", - "NC", - "ND", - "OH", - "OK", - "OR", - "PA", - "RI", - "SC", - "SD", - "TN", - "TX", - "UT", - "VT", - "VA", - "WA", - "WI", - "WV", - "WY", - "PR", - "GU", - "AS", - "VI", - "MP", - "MH", - "PW", - "FM", - "AE", - "AA", - "AP", - ], - typing.Any, -] diff --git a/src/vitable_connect/types/status.py b/src/vitable_connect/types/status.py deleted file mode 100644 index ed79297..0000000 --- a/src/vitable_connect/types/status.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -Status = typing.Union[typing.Literal["approved", "denied", "pending"], typing.Any] diff --git a/src/vitable_connect/types/tax_classification.py b/src/vitable_connect/types/tax_classification.py deleted file mode 100644 index 0fe86c8..0000000 --- a/src/vitable_connect/types/tax_classification.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -TaxClassification = typing.Union[typing.Literal["Unknown", "Pre-tax", "Post-tax"], typing.Any] diff --git a/src/vitable_connect/types/termination_rule.py b/src/vitable_connect/types/termination_rule.py deleted file mode 100644 index 5030197..0000000 --- a/src/vitable_connect/types/termination_rule.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -TerminationRule = typing.Union[typing.Literal["END_OF_N_MONTHS", "END_OF_PLAN_YEAR"], typing.Any] diff --git a/src/vitable_connect/types/update_employer_address_input.py b/src/vitable_connect/types/update_employer_address_input.py deleted file mode 100644 index e2a76ab..0000000 --- a/src/vitable_connect/types/update_employer_address_input.py +++ /dev/null @@ -1,52 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -import typing_extensions -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from ..core.serialization import FieldMetadata - - -class UpdateEmployerAddressInput(UniversalBaseModel): - address_line1: typing_extensions.Annotated[ - str, - FieldMetadata(alias="address_line_1"), - pydantic.Field(alias="address_line_1", description="Primary street address"), - ] - """ - Primary street address - """ - - address_line2: typing_extensions.Annotated[ - typing.Optional[str], - FieldMetadata(alias="address_line_2"), - pydantic.Field(alias="address_line_2", description="Secondary street address"), - ] = None - """ - Secondary street address - """ - - city: str = pydantic.Field() - """ - City name - """ - - state: str = pydantic.Field() - """ - Two-letter state code - """ - - zipcode: str = pydantic.Field() - """ - ZIP code - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/waiting_period.py b/src/vitable_connect/types/waiting_period.py deleted file mode 100644 index f6b2f22..0000000 --- a/src/vitable_connect/types/waiting_period.py +++ /dev/null @@ -1,5 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -WaitingPeriod = typing.Union[typing.Literal["FIRST_OF_FOLLOWING_MONTH", "THIRTY_DAYS", "SIXTY_DAYS"], typing.Any] diff --git a/src/vitable_connect/types/webhook_delivery.py b/src/vitable_connect/types/webhook_delivery.py deleted file mode 100644 index 43a3ab4..0000000 --- a/src/vitable_connect/types/webhook_delivery.py +++ /dev/null @@ -1,63 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel - - -class WebhookDelivery(UniversalBaseModel): - id: str = pydantic.Field() - """ - Prefixed unique identifier for this delivery (e.g., `wdlv_...`). - """ - - webhook_event_id: str = pydantic.Field() - """ - The webhook event this delivery belongs to. - """ - - subscription_id: str = pydantic.Field() - """ - The webhook subscription this delivery was sent to. - """ - - status: str = pydantic.Field() - """ - Current delivery status: Pending, In Progress, Delivered, or Failed. - """ - - started_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When the delivery attempt started, in UTC. - """ - - delivered_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When the delivery was successfully received, in UTC. - """ - - failed_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) - """ - When the delivery was marked as permanently failed, in UTC. - """ - - failure_reason: str = pydantic.Field() - """ - Reason for failure, if applicable. - """ - - created_at: dt.datetime = pydantic.Field() - """ - When this delivery record was created, in UTC. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/webhook_event.py b/src/vitable_connect/types/webhook_event.py index 3bf4218..d03f4a2 100644 --- a/src/vitable_connect/types/webhook_event.py +++ b/src/vitable_connect/types/webhook_event.py @@ -1,48 +1,33 @@ -# This file was auto-generated by Fern from our API Definition. +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import datetime as dt -import typing +from datetime import datetime -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .._models import BaseModel +__all__ = ["WebhookEvent"] -class WebhookEvent(UniversalBaseModel): - id: str = pydantic.Field() - """ - Prefixed unique identifier for this webhook event (e.g., `wevt_...`). - """ - organization_id: str = pydantic.Field() - """ - The organization this event belongs to. - """ +class WebhookEvent(BaseModel): + id: str + """Prefixed unique identifier for this webhook event (e.g., `wevt_...`).""" - event_name: str = pydantic.Field() - """ - The event type, formatted as `{resource}.{action}` (e.g., `enrollment.accepted`). - """ + created_at: datetime + """When the event occurred, in UTC.""" - resource_type: str = pydantic.Field() + event_name: str """ - The type of resource affected (e.g., `enrollment`, `employee`). + The event type, formatted as `{resource}.{action}` (e.g., + `enrollment.accepted`). """ - resource_id: str = pydantic.Field() - """ - Prefixed ID of the affected resource. Use this to fetch the current state from the API. - """ + organization_id: str + """The organization this event belongs to.""" - created_at: dt.datetime = pydantic.Field() - """ - When the event occurred, in UTC. - """ + resource_id: str + """Prefixed ID of the affected resource. - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: + Use this to fetch the current state from the API. + """ - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow + resource_type: str + """The type of resource affected (e.g., `enrollment`, `employee`).""" diff --git a/src/vitable_connect/types/webhook_event_list_deliveries_response.py b/src/vitable_connect/types/webhook_event_list_deliveries_response.py new file mode 100644 index 0000000..db9c7f9 --- /dev/null +++ b/src/vitable_connect/types/webhook_event_list_deliveries_response.py @@ -0,0 +1,41 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import List, Optional +from datetime import datetime + +from .._models import BaseModel + +__all__ = ["WebhookEventListDeliveriesResponse", "Data"] + + +class Data(BaseModel): + id: str + """Prefixed unique identifier for this delivery (e.g., `wdlv_...`).""" + + created_at: datetime + """When this delivery record was created, in UTC.""" + + delivered_at: Optional[datetime] = None + """When the delivery was successfully received, in UTC.""" + + failed_at: Optional[datetime] = None + """When the delivery was marked as permanently failed, in UTC.""" + + failure_reason: str + """Reason for failure, if applicable.""" + + started_at: Optional[datetime] = None + """When the delivery attempt started, in UTC.""" + + status: str + """Current delivery status: Pending, In Progress, Delivered, or Failed.""" + + subscription_id: str + """The webhook subscription this delivery was sent to.""" + + webhook_event_id: str + """The webhook event this delivery belongs to.""" + + +class WebhookEventListDeliveriesResponse(BaseModel): + data: List[Data] diff --git a/src/vitable_connect/types/webhook_event_list_params.py b/src/vitable_connect/types/webhook_event_list_params.py new file mode 100644 index 0000000..63c3eb1 --- /dev/null +++ b/src/vitable_connect/types/webhook_event_list_params.py @@ -0,0 +1,62 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import Union +from datetime import datetime +from typing_extensions import Literal, Annotated, TypedDict + +from .._utils import PropertyInfo + +__all__ = ["WebhookEventListParams"] + + +class WebhookEventListParams(TypedDict, total=False): + created_after: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + + created_before: Annotated[Union[str, datetime], PropertyInfo(format="iso8601")] + + event_name: Literal[ + "enrollment.accepted", + "enrollment.terminated", + "enrollment.termination_rescheduled", + "enrollment.elected", + "enrollment.granted", + "enrollment.waived", + "enrollment.started", + "employee.eligibility_granted", + "employee.eligibility_terminated", + "employee.deactivated", + "employee.deduction_created", + ] + """ + - `enrollment.accepted` - Enrollment Accepted + - `enrollment.terminated` - Enrollment Terminated + - `enrollment.termination_rescheduled` - Enrollment Termination Rescheduled + - `enrollment.elected` - Enrollment Elected + - `enrollment.granted` - Enrollment Granted + - `enrollment.waived` - Enrollment Waived + - `enrollment.started` - Enrollment Started + - `employee.eligibility_granted` - Employee Eligibility Granted + - `employee.eligibility_terminated` - Employee Eligibility Terminated + - `employee.deactivated` - Employee Deactivated + - `employee.deduction_created` - Employee Deduction Created + """ + + limit: int + """Items per page (default: 20, max: 100)""" + + page: int + """Page number (default: 1)""" + + resource_id: str + + resource_type: Literal["enrollment", "employee", "employer", "dependent", "plan_year", "payroll_deduction"] + """ + - `enrollment` - Enrollment + - `employee` - Employee + - `employer` - Employer + - `dependent` - Dependent + - `plan_year` - Plan Year + - `payroll_deduction` - Payroll Deduction + """ diff --git a/src/vitable_connect/types/webhook_event_list_response.py b/src/vitable_connect/types/webhook_event_list_response.py deleted file mode 100644 index 4d1f44c..0000000 --- a/src/vitable_connect/types/webhook_event_list_response.py +++ /dev/null @@ -1,33 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .pagination import Pagination -from .webhook_event import WebhookEvent - - -class WebhookEventListResponse(UniversalBaseModel): - """ - Paginated list response containing webhook event resources. - """ - - data: typing.List[WebhookEvent] = pydantic.Field() - """ - This page of webhook events. - """ - - pagination: Pagination = pydantic.Field() - """ - Page metadata. Request the next page by incrementing `page`. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/webhook_event_response.py b/src/vitable_connect/types/webhook_event_response.py deleted file mode 100644 index c8eb450..0000000 --- a/src/vitable_connect/types/webhook_event_response.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -import pydantic -from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel -from .webhook_event import WebhookEvent - - -class WebhookEventResponse(UniversalBaseModel): - """ - Response containing a single webhook event resource. - """ - - data: WebhookEvent = pydantic.Field() - """ - The webhook event. - """ - - if IS_PYDANTIC_V2: - model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 - else: - - class Config: - frozen = True - smart_union = True - extra = pydantic.Extra.allow diff --git a/src/vitable_connect/types/webhook_event_retrieve_response.py b/src/vitable_connect/types/webhook_event_retrieve_response.py new file mode 100644 index 0000000..9a35e89 --- /dev/null +++ b/src/vitable_connect/types/webhook_event_retrieve_response.py @@ -0,0 +1,12 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .._models import BaseModel +from .webhook_event import WebhookEvent + +__all__ = ["WebhookEventRetrieveResponse"] + + +class WebhookEventRetrieveResponse(BaseModel): + """Response containing a single webhook event resource.""" + + data: WebhookEvent diff --git a/src/vitable_connect/version.py b/src/vitable_connect/version.py deleted file mode 100644 index 493704c..0000000 --- a/src/vitable_connect/version.py +++ /dev/null @@ -1,3 +0,0 @@ -from importlib import metadata - -__version__ = metadata.version("vitable-connect") diff --git a/src/vitable_connect/webhook_events/__init__.py b/src/vitable_connect/webhook_events/__init__.py deleted file mode 100644 index 5cde020..0000000 --- a/src/vitable_connect/webhook_events/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# isort: skip_file - diff --git a/src/vitable_connect/webhook_events/client.py b/src/vitable_connect/webhook_events/client.py deleted file mode 100644 index ea74e53..0000000 --- a/src/vitable_connect/webhook_events/client.py +++ /dev/null @@ -1,373 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing - -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.pagination import AsyncPager, SyncPager -from ..core.request_options import RequestOptions -from ..types.created_after import CreatedAfter -from ..types.created_before import CreatedBefore -from ..types.event_id import EventId -from ..types.event_name import EventName -from ..types.limit import Limit -from ..types.list_webhook_event_deliveries_response import ListWebhookEventDeliveriesResponse -from ..types.page import Page -from ..types.resource_id import ResourceId -from ..types.resource_type import ResourceType -from ..types.webhook_event import WebhookEvent -from ..types.webhook_event_list_response import WebhookEventListResponse -from ..types.webhook_event_response import WebhookEventResponse -from .raw_client import AsyncRawWebhookEventsClient, RawWebhookEventsClient - - -class WebhookEventsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._raw_client = RawWebhookEventsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> RawWebhookEventsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - RawWebhookEventsClient - """ - return self._raw_client - - def list( - self, - *, - created_after: typing.Optional[CreatedAfter] = None, - created_before: typing.Optional[CreatedBefore] = None, - event_name: typing.Optional[EventName] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - resource_id: typing.Optional[ResourceId] = None, - resource_type: typing.Optional[ResourceType] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[WebhookEvent, WebhookEventListResponse]: - """ - Retrieves a paginated list of webhook events for the authenticated organization. Supports filtering by event name, resource type, resource ID, and date range. - - Parameters - ---------- - created_after : typing.Optional[CreatedAfter] - - created_before : typing.Optional[CreatedBefore] - - event_name : typing.Optional[EventName] - * `enrollment.accepted` - Enrollment Accepted - * `enrollment.terminated` - Enrollment Terminated - * `enrollment.termination_rescheduled` - Enrollment Termination Rescheduled - * `enrollment.elected` - Enrollment Elected - * `enrollment.granted` - Enrollment Granted - * `enrollment.waived` - Enrollment Waived - * `enrollment.started` - Enrollment Started - * `employee.eligibility_granted` - Employee Eligibility Granted - * `employee.eligibility_terminated` - Employee Eligibility Terminated - * `employee.deactivated` - Employee Deactivated - * `employee.deduction_created` - Employee Deduction Created - - limit : typing.Optional[Limit] - - page : typing.Optional[Page] - - resource_id : typing.Optional[ResourceId] - - resource_type : typing.Optional[ResourceType] - * `enrollment` - Enrollment - * `employee` - Employee - * `employer` - Employer - * `dependent` - Dependent - * `plan_year` - Plan Year - * `payroll_deduction` - Payroll Deduction - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[WebhookEvent, WebhookEventListResponse] - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - response = client.webhook_events.list( - limit=20, - page=1, - ) - for item in response: - yield item - # alternatively, you can paginate page-by-page - for page in response.iter_pages(): - yield page - """ - return self._raw_client.list( - created_after=created_after, - created_before=created_before, - event_name=event_name, - limit=limit, - page=page, - resource_id=resource_id, - resource_type=resource_type, - request_options=request_options, - ) - - def get( - self, event_id: EventId, *, request_options: typing.Optional[RequestOptions] = None - ) -> WebhookEventResponse: - """ - Retrieves a single webhook event by its prefixed ID. Returns 404 if the event does not exist or belongs to a different organization. - - Parameters - ---------- - event_id : EventId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - WebhookEventResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.webhook_events.get( - event_id="event_id", - ) - """ - _response = self._raw_client.get(event_id, request_options=request_options) - return _response.data - - def list_deliveries( - self, event_id: EventId, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListWebhookEventDeliveriesResponse: - """ - Retrieves all delivery attempts for a webhook event. Returns up to 100 deliveries. Each delivery includes a computed status field (Pending, In Progress, Delivered, or Failed). - - Parameters - ---------- - event_id : EventId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListWebhookEventDeliveriesResponse - - - Examples - -------- - from vitable_connect import VitableConnect - - client = VitableConnect( - api_key="YOUR_API_KEY", - ) - client.webhook_events.list_deliveries( - event_id="event_id", - ) - """ - _response = self._raw_client.list_deliveries(event_id, request_options=request_options) - return _response.data - - -class AsyncWebhookEventsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._raw_client = AsyncRawWebhookEventsClient(client_wrapper=client_wrapper) - - @property - def with_raw_response(self) -> AsyncRawWebhookEventsClient: - """ - Retrieves a raw implementation of this client that returns raw responses. - - Returns - ------- - AsyncRawWebhookEventsClient - """ - return self._raw_client - - async def list( - self, - *, - created_after: typing.Optional[CreatedAfter] = None, - created_before: typing.Optional[CreatedBefore] = None, - event_name: typing.Optional[EventName] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - resource_id: typing.Optional[ResourceId] = None, - resource_type: typing.Optional[ResourceType] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[WebhookEvent, WebhookEventListResponse]: - """ - Retrieves a paginated list of webhook events for the authenticated organization. Supports filtering by event name, resource type, resource ID, and date range. - - Parameters - ---------- - created_after : typing.Optional[CreatedAfter] - - created_before : typing.Optional[CreatedBefore] - - event_name : typing.Optional[EventName] - * `enrollment.accepted` - Enrollment Accepted - * `enrollment.terminated` - Enrollment Terminated - * `enrollment.termination_rescheduled` - Enrollment Termination Rescheduled - * `enrollment.elected` - Enrollment Elected - * `enrollment.granted` - Enrollment Granted - * `enrollment.waived` - Enrollment Waived - * `enrollment.started` - Enrollment Started - * `employee.eligibility_granted` - Employee Eligibility Granted - * `employee.eligibility_terminated` - Employee Eligibility Terminated - * `employee.deactivated` - Employee Deactivated - * `employee.deduction_created` - Employee Deduction Created - - limit : typing.Optional[Limit] - - page : typing.Optional[Page] - - resource_id : typing.Optional[ResourceId] - - resource_type : typing.Optional[ResourceType] - * `enrollment` - Enrollment - * `employee` - Employee - * `employer` - Employer - * `dependent` - Dependent - * `plan_year` - Plan Year - * `payroll_deduction` - Payroll Deduction - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[WebhookEvent, WebhookEventListResponse] - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - response = await client.webhook_events.list( - limit=20, - page=1, - ) - async for item in response: - yield item - - # alternatively, you can paginate page-by-page - async for page in response.iter_pages(): - yield page - - - asyncio.run(main()) - """ - return await self._raw_client.list( - created_after=created_after, - created_before=created_before, - event_name=event_name, - limit=limit, - page=page, - resource_id=resource_id, - resource_type=resource_type, - request_options=request_options, - ) - - async def get( - self, event_id: EventId, *, request_options: typing.Optional[RequestOptions] = None - ) -> WebhookEventResponse: - """ - Retrieves a single webhook event by its prefixed ID. Returns 404 if the event does not exist or belongs to a different organization. - - Parameters - ---------- - event_id : EventId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - WebhookEventResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.webhook_events.get( - event_id="event_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.get(event_id, request_options=request_options) - return _response.data - - async def list_deliveries( - self, event_id: EventId, *, request_options: typing.Optional[RequestOptions] = None - ) -> ListWebhookEventDeliveriesResponse: - """ - Retrieves all delivery attempts for a webhook event. Returns up to 100 deliveries. Each delivery includes a computed status field (Pending, In Progress, Delivered, or Failed). - - Parameters - ---------- - event_id : EventId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - ListWebhookEventDeliveriesResponse - - - Examples - -------- - import asyncio - - from vitable_connect import AsyncVitableConnect - - client = AsyncVitableConnect( - api_key="YOUR_API_KEY", - ) - - - async def main() -> None: - await client.webhook_events.list_deliveries( - event_id="event_id", - ) - - - asyncio.run(main()) - """ - _response = await self._raw_client.list_deliveries(event_id, request_options=request_options) - return _response.data diff --git a/src/vitable_connect/webhook_events/raw_client.py b/src/vitable_connect/webhook_events/raw_client.py deleted file mode 100644 index 551018d..0000000 --- a/src/vitable_connect/webhook_events/raw_client.py +++ /dev/null @@ -1,951 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -import typing -from json.decoder import JSONDecodeError - -from ..core.api_error import ApiError -from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper -from ..core.datetime_utils import serialize_datetime -from ..core.http_response import AsyncHttpResponse, HttpResponse -from ..core.jsonable_encoder import encode_path_param -from ..core.pagination import AsyncPager, SyncPager -from ..core.parse_error import ParsingError -from ..core.pydantic_utilities import parse_obj_as -from ..core.request_options import RequestOptions -from ..errors.bad_gateway_error import BadGatewayError -from ..errors.bad_request_error import BadRequestError -from ..errors.conflict_error import ConflictError -from ..errors.forbidden_error import ForbiddenError -from ..errors.internal_server_error import InternalServerError -from ..errors.not_found_error import NotFoundError -from ..errors.too_many_requests_error import TooManyRequestsError -from ..errors.unauthorized_error import UnauthorizedError -from ..types.created_after import CreatedAfter -from ..types.created_before import CreatedBefore -from ..types.error_response import ErrorResponse -from ..types.event_id import EventId -from ..types.event_name import EventName -from ..types.limit import Limit -from ..types.list_webhook_event_deliveries_response import ListWebhookEventDeliveriesResponse -from ..types.page import Page -from ..types.resource_id import ResourceId -from ..types.resource_type import ResourceType -from ..types.webhook_event import WebhookEvent -from ..types.webhook_event_list_response import WebhookEventListResponse -from ..types.webhook_event_response import WebhookEventResponse -from pydantic import ValidationError - - -class RawWebhookEventsClient: - def __init__(self, *, client_wrapper: SyncClientWrapper): - self._client_wrapper = client_wrapper - - def list( - self, - *, - created_after: typing.Optional[CreatedAfter] = None, - created_before: typing.Optional[CreatedBefore] = None, - event_name: typing.Optional[EventName] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - resource_id: typing.Optional[ResourceId] = None, - resource_type: typing.Optional[ResourceType] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> SyncPager[WebhookEvent, WebhookEventListResponse]: - """ - Retrieves a paginated list of webhook events for the authenticated organization. Supports filtering by event name, resource type, resource ID, and date range. - - Parameters - ---------- - created_after : typing.Optional[CreatedAfter] - - created_before : typing.Optional[CreatedBefore] - - event_name : typing.Optional[EventName] - * `enrollment.accepted` - Enrollment Accepted - * `enrollment.terminated` - Enrollment Terminated - * `enrollment.termination_rescheduled` - Enrollment Termination Rescheduled - * `enrollment.elected` - Enrollment Elected - * `enrollment.granted` - Enrollment Granted - * `enrollment.waived` - Enrollment Waived - * `enrollment.started` - Enrollment Started - * `employee.eligibility_granted` - Employee Eligibility Granted - * `employee.eligibility_terminated` - Employee Eligibility Terminated - * `employee.deactivated` - Employee Deactivated - * `employee.deduction_created` - Employee Deduction Created - - limit : typing.Optional[Limit] - - page : typing.Optional[Page] - - resource_id : typing.Optional[ResourceId] - - resource_type : typing.Optional[ResourceType] - * `enrollment` - Enrollment - * `employee` - Employee - * `employer` - Employer - * `dependent` - Dependent - * `plan_year` - Plan Year - * `payroll_deduction` - Payroll Deduction - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - SyncPager[WebhookEvent, WebhookEventListResponse] - - """ - page = page if page is not None else 1 - - _response = self._client_wrapper.httpx_client.request( - "v1/webhook-events", - method="GET", - params={ - "created_after": serialize_datetime(created_after) if created_after is not None else None, - "created_before": serialize_datetime(created_before) if created_before is not None else None, - "event_name": event_name, - "limit": limit, - "page": page, - "resource_id": resource_id, - "resource_type": resource_type, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - WebhookEventListResponse, - parse_obj_as( - type_=WebhookEventListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - _get_next = lambda: self.list( - created_after=created_after, - created_before=created_before, - event_name=event_name, - limit=limit, - page=page + 1, - resource_id=resource_id, - resource_type=resource_type, - request_options=request_options, - ) - return SyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def get( - self, event_id: EventId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[WebhookEventResponse]: - """ - Retrieves a single webhook event by its prefixed ID. Returns 404 if the event does not exist or belongs to a different organization. - - Parameters - ---------- - event_id : EventId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[WebhookEventResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/webhook-events/{encode_path_param(event_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - WebhookEventResponse, - parse_obj_as( - type_=WebhookEventResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - def list_deliveries( - self, event_id: EventId, *, request_options: typing.Optional[RequestOptions] = None - ) -> HttpResponse[ListWebhookEventDeliveriesResponse]: - """ - Retrieves all delivery attempts for a webhook event. Returns up to 100 deliveries. Each delivery includes a computed status field (Pending, In Progress, Delivered, or Failed). - - Parameters - ---------- - event_id : EventId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - HttpResponse[ListWebhookEventDeliveriesResponse] - - """ - _response = self._client_wrapper.httpx_client.request( - f"v1/webhook-events/{encode_path_param(event_id)}/deliveries", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ListWebhookEventDeliveriesResponse, - parse_obj_as( - type_=ListWebhookEventDeliveriesResponse, # type: ignore - object_=_response.json(), - ), - ) - return HttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - -class AsyncRawWebhookEventsClient: - def __init__(self, *, client_wrapper: AsyncClientWrapper): - self._client_wrapper = client_wrapper - - async def list( - self, - *, - created_after: typing.Optional[CreatedAfter] = None, - created_before: typing.Optional[CreatedBefore] = None, - event_name: typing.Optional[EventName] = None, - limit: typing.Optional[Limit] = None, - page: typing.Optional[Page] = None, - resource_id: typing.Optional[ResourceId] = None, - resource_type: typing.Optional[ResourceType] = None, - request_options: typing.Optional[RequestOptions] = None, - ) -> AsyncPager[WebhookEvent, WebhookEventListResponse]: - """ - Retrieves a paginated list of webhook events for the authenticated organization. Supports filtering by event name, resource type, resource ID, and date range. - - Parameters - ---------- - created_after : typing.Optional[CreatedAfter] - - created_before : typing.Optional[CreatedBefore] - - event_name : typing.Optional[EventName] - * `enrollment.accepted` - Enrollment Accepted - * `enrollment.terminated` - Enrollment Terminated - * `enrollment.termination_rescheduled` - Enrollment Termination Rescheduled - * `enrollment.elected` - Enrollment Elected - * `enrollment.granted` - Enrollment Granted - * `enrollment.waived` - Enrollment Waived - * `enrollment.started` - Enrollment Started - * `employee.eligibility_granted` - Employee Eligibility Granted - * `employee.eligibility_terminated` - Employee Eligibility Terminated - * `employee.deactivated` - Employee Deactivated - * `employee.deduction_created` - Employee Deduction Created - - limit : typing.Optional[Limit] - - page : typing.Optional[Page] - - resource_id : typing.Optional[ResourceId] - - resource_type : typing.Optional[ResourceType] - * `enrollment` - Enrollment - * `employee` - Employee - * `employer` - Employer - * `dependent` - Dependent - * `plan_year` - Plan Year - * `payroll_deduction` - Payroll Deduction - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncPager[WebhookEvent, WebhookEventListResponse] - - """ - page = page if page is not None else 1 - - _response = await self._client_wrapper.httpx_client.request( - "v1/webhook-events", - method="GET", - params={ - "created_after": serialize_datetime(created_after) if created_after is not None else None, - "created_before": serialize_datetime(created_before) if created_before is not None else None, - "event_name": event_name, - "limit": limit, - "page": page, - "resource_id": resource_id, - "resource_type": resource_type, - }, - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _parsed_response = typing.cast( - WebhookEventListResponse, - parse_obj_as( - type_=WebhookEventListResponse, # type: ignore - object_=_response.json(), - ), - ) - _items = _parsed_response.data - _has_next = len(_items or []) > 0 - - async def _get_next(): - return await self.list( - created_after=created_after, - created_before=created_before, - event_name=event_name, - limit=limit, - page=page + 1, - resource_id=resource_id, - resource_type=resource_type, - request_options=request_options, - ) - - return AsyncPager(has_next=_has_next, items=_items, get_next=_get_next, response=_parsed_response) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def get( - self, event_id: EventId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[WebhookEventResponse]: - """ - Retrieves a single webhook event by its prefixed ID. Returns 404 if the event does not exist or belongs to a different organization. - - Parameters - ---------- - event_id : EventId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[WebhookEventResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/webhook-events/{encode_path_param(event_id)}", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - WebhookEventResponse, - parse_obj_as( - type_=WebhookEventResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) - - async def list_deliveries( - self, event_id: EventId, *, request_options: typing.Optional[RequestOptions] = None - ) -> AsyncHttpResponse[ListWebhookEventDeliveriesResponse]: - """ - Retrieves all delivery attempts for a webhook event. Returns up to 100 deliveries. Each delivery includes a computed status field (Pending, In Progress, Delivered, or Failed). - - Parameters - ---------- - event_id : EventId - - request_options : typing.Optional[RequestOptions] - Request-specific configuration. - - Returns - ------- - AsyncHttpResponse[ListWebhookEventDeliveriesResponse] - - """ - _response = await self._client_wrapper.httpx_client.request( - f"v1/webhook-events/{encode_path_param(event_id)}/deliveries", - method="GET", - request_options=request_options, - ) - try: - if 200 <= _response.status_code < 300: - _data = typing.cast( - ListWebhookEventDeliveriesResponse, - parse_obj_as( - type_=ListWebhookEventDeliveriesResponse, # type: ignore - object_=_response.json(), - ), - ) - return AsyncHttpResponse(response=_response, data=_data) - if _response.status_code == 400: - raise BadRequestError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 401: - raise UnauthorizedError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 403: - raise ForbiddenError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 404: - raise NotFoundError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 409: - raise ConflictError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 429: - raise TooManyRequestsError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 500: - raise InternalServerError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - if _response.status_code == 502: - raise BadGatewayError( - headers=dict(_response.headers), - body=typing.cast( - ErrorResponse, - parse_obj_as( - type_=ErrorResponse, # type: ignore - object_=_response.json(), - ), - ), - ) - _response_json = _response.json() - except JSONDecodeError: - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) - except ValidationError as e: - raise ParsingError( - status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e - ) - raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/vitable_connect_api/lib/.keep b/src/vitable_connect_api/lib/.keep new file mode 100644 index 0000000..5e2c99f --- /dev/null +++ b/src/vitable_connect_api/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/src/vitable_partner_api/lib/.keep b/src/vitable_partner_api/lib/.keep new file mode 100644 index 0000000..5e2c99f --- /dev/null +++ b/src/vitable_partner_api/lib/.keep @@ -0,0 +1,4 @@ +File generated from our OpenAPI spec by Stainless. + +This directory can be used to store custom files to expand the SDK. +It is ignored by Stainless code generation and its content (other than this keep file) won't be touched. \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/__init__.py b/tests/api_resources/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/groups/__init__.py b/tests/api_resources/groups/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/groups/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/groups/members/__init__.py b/tests/api_resources/groups/members/__init__.py new file mode 100644 index 0000000..fd8019a --- /dev/null +++ b/tests/api_resources/groups/members/__init__.py @@ -0,0 +1 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. diff --git a/tests/api_resources/groups/members/test_sync.py b/tests/api_resources/groups/members/test_sync.py new file mode 100644 index 0000000..feb4f4d --- /dev/null +++ b/tests/api_resources/groups/members/test_sync.py @@ -0,0 +1,333 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect._utils import parse_date +from vitable_connect.types.groups.members import SyncSubmitResponse, SyncRetrieveResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestSync: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: VitableConnect) -> None: + sync = client.groups.members.sync.retrieve( + request_id="request_id", + group_id="grp_abc123def456", + ) + assert_matches_type(SyncRetrieveResponse, sync, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: VitableConnect) -> None: + response = client.groups.members.sync.with_raw_response.retrieve( + request_id="request_id", + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + sync = response.parse() + assert_matches_type(SyncRetrieveResponse, sync, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: VitableConnect) -> None: + with client.groups.members.sync.with_streaming_response.retrieve( + request_id="request_id", + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + sync = response.parse() + assert_matches_type(SyncRetrieveResponse, sync, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.members.sync.with_raw_response.retrieve( + request_id="request_id", + group_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `request_id` but received ''"): + client.groups.members.sync.with_raw_response.retrieve( + request_id="", + group_id="grp_abc123def456", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_submit(self, client: VitableConnect) -> None: + sync = client.groups.members.sync.submit( + group_id="grp_abc123def456", + members=[ + { + "address": { + "address_line_1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zipcode": "94102", + }, + "date_of_birth": parse_date("1990-05-15"), + "first_name": "Jane", + "last_name": "Doe", + "plan_id": "pln_abc123def456", + "reference_id": "EMP-001", + } + ], + ) + assert_matches_type(SyncSubmitResponse, sync, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_submit(self, client: VitableConnect) -> None: + response = client.groups.members.sync.with_raw_response.submit( + group_id="grp_abc123def456", + members=[ + { + "address": { + "address_line_1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zipcode": "94102", + }, + "date_of_birth": parse_date("1990-05-15"), + "first_name": "Jane", + "last_name": "Doe", + "plan_id": "pln_abc123def456", + "reference_id": "EMP-001", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + sync = response.parse() + assert_matches_type(SyncSubmitResponse, sync, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_submit(self, client: VitableConnect) -> None: + with client.groups.members.sync.with_streaming_response.submit( + group_id="grp_abc123def456", + members=[ + { + "address": { + "address_line_1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zipcode": "94102", + }, + "date_of_birth": parse_date("1990-05-15"), + "first_name": "Jane", + "last_name": "Doe", + "plan_id": "pln_abc123def456", + "reference_id": "EMP-001", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + sync = response.parse() + assert_matches_type(SyncSubmitResponse, sync, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_submit(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.members.sync.with_raw_response.submit( + group_id="", + members=[ + { + "address": { + "address_line_1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zipcode": "94102", + }, + "date_of_birth": parse_date("1990-05-15"), + "first_name": "Jane", + "last_name": "Doe", + "plan_id": "pln_abc123def456", + "reference_id": "EMP-001", + } + ], + ) + + +class TestAsyncSync: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncVitableConnect) -> None: + sync = await async_client.groups.members.sync.retrieve( + request_id="request_id", + group_id="grp_abc123def456", + ) + assert_matches_type(SyncRetrieveResponse, sync, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.groups.members.sync.with_raw_response.retrieve( + request_id="request_id", + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + sync = await response.parse() + assert_matches_type(SyncRetrieveResponse, sync, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + async with async_client.groups.members.sync.with_streaming_response.retrieve( + request_id="request_id", + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + sync = await response.parse() + assert_matches_type(SyncRetrieveResponse, sync, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.members.sync.with_raw_response.retrieve( + request_id="request_id", + group_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `request_id` but received ''"): + await async_client.groups.members.sync.with_raw_response.retrieve( + request_id="", + group_id="grp_abc123def456", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_submit(self, async_client: AsyncVitableConnect) -> None: + sync = await async_client.groups.members.sync.submit( + group_id="grp_abc123def456", + members=[ + { + "address": { + "address_line_1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zipcode": "94102", + }, + "date_of_birth": parse_date("1990-05-15"), + "first_name": "Jane", + "last_name": "Doe", + "plan_id": "pln_abc123def456", + "reference_id": "EMP-001", + } + ], + ) + assert_matches_type(SyncSubmitResponse, sync, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_submit(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.groups.members.sync.with_raw_response.submit( + group_id="grp_abc123def456", + members=[ + { + "address": { + "address_line_1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zipcode": "94102", + }, + "date_of_birth": parse_date("1990-05-15"), + "first_name": "Jane", + "last_name": "Doe", + "plan_id": "pln_abc123def456", + "reference_id": "EMP-001", + } + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + sync = await response.parse() + assert_matches_type(SyncSubmitResponse, sync, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_submit(self, async_client: AsyncVitableConnect) -> None: + async with async_client.groups.members.sync.with_streaming_response.submit( + group_id="grp_abc123def456", + members=[ + { + "address": { + "address_line_1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zipcode": "94102", + }, + "date_of_birth": parse_date("1990-05-15"), + "first_name": "Jane", + "last_name": "Doe", + "plan_id": "pln_abc123def456", + "reference_id": "EMP-001", + } + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + sync = await response.parse() + assert_matches_type(SyncSubmitResponse, sync, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_submit(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.members.sync.with_raw_response.submit( + group_id="", + members=[ + { + "address": { + "address_line_1": "123 Main Street", + "city": "San Francisco", + "state": "CA", + "zipcode": "94102", + }, + "date_of_birth": parse_date("1990-05-15"), + "first_name": "Jane", + "last_name": "Doe", + "plan_id": "pln_abc123def456", + "reference_id": "EMP-001", + } + ], + ) diff --git a/tests/api_resources/test_auth.py b/tests/api_resources/test_auth.py new file mode 100644 index 0000000..5e9e3f9 --- /dev/null +++ b/tests/api_resources/test_auth.py @@ -0,0 +1,116 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import AuthIssueAccessTokenResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestAuth: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_issue_access_token(self, client: VitableConnect) -> None: + auth = client.auth.issue_access_token( + grant_type="client_credentials", + ) + assert_matches_type(AuthIssueAccessTokenResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_issue_access_token_with_all_params(self, client: VitableConnect) -> None: + auth = client.auth.issue_access_token( + grant_type="client_credentials", + bound_entity={ + "id": "id", + "type": "employer", + }, + ) + assert_matches_type(AuthIssueAccessTokenResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_issue_access_token(self, client: VitableConnect) -> None: + response = client.auth.with_raw_response.issue_access_token( + grant_type="client_credentials", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = response.parse() + assert_matches_type(AuthIssueAccessTokenResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_issue_access_token(self, client: VitableConnect) -> None: + with client.auth.with_streaming_response.issue_access_token( + grant_type="client_credentials", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = response.parse() + assert_matches_type(AuthIssueAccessTokenResponse, auth, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncAuth: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_issue_access_token(self, async_client: AsyncVitableConnect) -> None: + auth = await async_client.auth.issue_access_token( + grant_type="client_credentials", + ) + assert_matches_type(AuthIssueAccessTokenResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_issue_access_token_with_all_params(self, async_client: AsyncVitableConnect) -> None: + auth = await async_client.auth.issue_access_token( + grant_type="client_credentials", + bound_entity={ + "id": "id", + "type": "employer", + }, + ) + assert_matches_type(AuthIssueAccessTokenResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_issue_access_token(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.auth.with_raw_response.issue_access_token( + grant_type="client_credentials", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + auth = await response.parse() + assert_matches_type(AuthIssueAccessTokenResponse, auth, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_issue_access_token(self, async_client: AsyncVitableConnect) -> None: + async with async_client.auth.with_streaming_response.issue_access_token( + grant_type="client_credentials", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + auth = await response.parse() + assert_matches_type(AuthIssueAccessTokenResponse, auth, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_employees.py b/tests/api_resources/test_employees.py new file mode 100644 index 0000000..c60b7df --- /dev/null +++ b/tests/api_resources/test_employees.py @@ -0,0 +1,356 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import ( + Enrollment, + EmployeeUpdateResponse, + EmployeeRetrieveResponse, +) +from vitable_connect._utils import parse_date +from vitable_connect.pagination import SyncPageNumberPage, AsyncPageNumberPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestEmployees: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: VitableConnect) -> None: + employee = client.employees.retrieve( + "empl_abc123def456", + ) + assert_matches_type(EmployeeRetrieveResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: VitableConnect) -> None: + response = client.employees.with_raw_response.retrieve( + "empl_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employee = response.parse() + assert_matches_type(EmployeeRetrieveResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: VitableConnect) -> None: + with client.employees.with_streaming_response.retrieve( + "empl_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employee = response.parse() + assert_matches_type(EmployeeRetrieveResponse, employee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employee_id` but received ''"): + client.employees.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: VitableConnect) -> None: + employee = client.employees.update( + employee_id="empl_abc123def456", + effective_date=parse_date("2023-03-01"), + ) + assert_matches_type(EmployeeUpdateResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: VitableConnect) -> None: + employee = client.employees.update( + employee_id="empl_abc123def456", + effective_date=parse_date("2023-03-01"), + address={ + "city": "city", + "state": "xx", + "street_1": "street_1", + "zip_code": "zip_code", + "country": "country", + "street_2": "street_2", + }, + compensation_type="Salary", + email="dev@stainless.com", + employee_class="Full Time", + gender="Male", + phone="phone", + start_date=parse_date("2023-01-15"), + ) + assert_matches_type(EmployeeUpdateResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: VitableConnect) -> None: + response = client.employees.with_raw_response.update( + employee_id="empl_abc123def456", + effective_date=parse_date("2023-03-01"), + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employee = response.parse() + assert_matches_type(EmployeeUpdateResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: VitableConnect) -> None: + with client.employees.with_streaming_response.update( + employee_id="empl_abc123def456", + effective_date=parse_date("2023-03-01"), + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employee = response.parse() + assert_matches_type(EmployeeUpdateResponse, employee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employee_id` but received ''"): + client.employees.with_raw_response.update( + employee_id="", + effective_date=parse_date("2023-03-01"), + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_enrollments(self, client: VitableConnect) -> None: + employee = client.employees.list_enrollments( + employee_id="empl_abc123def456", + ) + assert_matches_type(SyncPageNumberPage[Enrollment], employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_enrollments_with_all_params(self, client: VitableConnect) -> None: + employee = client.employees.list_enrollments( + employee_id="empl_abc123def456", + limit=20, + page=1, + ) + assert_matches_type(SyncPageNumberPage[Enrollment], employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_enrollments(self, client: VitableConnect) -> None: + response = client.employees.with_raw_response.list_enrollments( + employee_id="empl_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employee = response.parse() + assert_matches_type(SyncPageNumberPage[Enrollment], employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_enrollments(self, client: VitableConnect) -> None: + with client.employees.with_streaming_response.list_enrollments( + employee_id="empl_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employee = response.parse() + assert_matches_type(SyncPageNumberPage[Enrollment], employee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_enrollments(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employee_id` but received ''"): + client.employees.with_raw_response.list_enrollments( + employee_id="", + ) + + +class TestAsyncEmployees: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncVitableConnect) -> None: + employee = await async_client.employees.retrieve( + "empl_abc123def456", + ) + assert_matches_type(EmployeeRetrieveResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employees.with_raw_response.retrieve( + "empl_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employee = await response.parse() + assert_matches_type(EmployeeRetrieveResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employees.with_streaming_response.retrieve( + "empl_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employee = await response.parse() + assert_matches_type(EmployeeRetrieveResponse, employee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employee_id` but received ''"): + await async_client.employees.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncVitableConnect) -> None: + employee = await async_client.employees.update( + employee_id="empl_abc123def456", + effective_date=parse_date("2023-03-01"), + ) + assert_matches_type(EmployeeUpdateResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncVitableConnect) -> None: + employee = await async_client.employees.update( + employee_id="empl_abc123def456", + effective_date=parse_date("2023-03-01"), + address={ + "city": "city", + "state": "xx", + "street_1": "street_1", + "zip_code": "zip_code", + "country": "country", + "street_2": "street_2", + }, + compensation_type="Salary", + email="dev@stainless.com", + employee_class="Full Time", + gender="Male", + phone="phone", + start_date=parse_date("2023-01-15"), + ) + assert_matches_type(EmployeeUpdateResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employees.with_raw_response.update( + employee_id="empl_abc123def456", + effective_date=parse_date("2023-03-01"), + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employee = await response.parse() + assert_matches_type(EmployeeUpdateResponse, employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employees.with_streaming_response.update( + employee_id="empl_abc123def456", + effective_date=parse_date("2023-03-01"), + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employee = await response.parse() + assert_matches_type(EmployeeUpdateResponse, employee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employee_id` but received ''"): + await async_client.employees.with_raw_response.update( + employee_id="", + effective_date=parse_date("2023-03-01"), + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_enrollments(self, async_client: AsyncVitableConnect) -> None: + employee = await async_client.employees.list_enrollments( + employee_id="empl_abc123def456", + ) + assert_matches_type(AsyncPageNumberPage[Enrollment], employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_enrollments_with_all_params(self, async_client: AsyncVitableConnect) -> None: + employee = await async_client.employees.list_enrollments( + employee_id="empl_abc123def456", + limit=20, + page=1, + ) + assert_matches_type(AsyncPageNumberPage[Enrollment], employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_enrollments(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employees.with_raw_response.list_enrollments( + employee_id="empl_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employee = await response.parse() + assert_matches_type(AsyncPageNumberPage[Enrollment], employee, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_enrollments(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employees.with_streaming_response.list_enrollments( + employee_id="empl_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employee = await response.parse() + assert_matches_type(AsyncPageNumberPage[Enrollment], employee, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_enrollments(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employee_id` but received ''"): + await async_client.employees.with_raw_response.list_enrollments( + employee_id="", + ) diff --git a/tests/api_resources/test_employers.py b/tests/api_resources/test_employers.py new file mode 100644 index 0000000..ff24aa2 --- /dev/null +++ b/tests/api_resources/test_employers.py @@ -0,0 +1,2071 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import ( + Employee, + EmployerResponse, + EmployerListResponse, + EmployerListInvoicesResponse, + EmployerRetrieveHRISResponse, + EmployerUpdateSettingsResponse, + EmployerSubmitCensusSyncResponse, + EmployerListHRISProvidersResponse, + EmployerRetrieveInvoicePdfResponse, + EmployerListBenefitPlanYearsResponse, + EmployerRetrieveBenefitPlanYearResponse, + EmployerSubmitPayrollAccessSetupResponse, + EmployerRetrievePayrollAccessSetupResponse, + EmployerEnsurePayrollIntegrationEmailResponse, + EmployerListBenefitPlanYearEnrollmentsResponse, + EmployerListPayrollDeductionStatementsResponse, +) +from vitable_connect._utils import parse_date +from vitable_connect.pagination import SyncPageNumberPage, AsyncPageNumberPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestEmployers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: VitableConnect) -> None: + employer = client.employers.create( + address={ + "address_line_1": "789 Business Blvd", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + }, + ein="12-3456789", + email="hr@newco.com", + legal_name="NewCo Industries LLC", + name="NewCo Industries", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: VitableConnect) -> None: + employer = client.employers.create( + address={ + "address_line_1": "789 Business Blvd", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + "address_line_2": "Floor 5", + }, + ein="12-3456789", + email="hr@newco.com", + legal_name="NewCo Industries LLC", + name="NewCo Industries", + phone_number="2065550100", + reference_id="partner-emp-001", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.create( + address={ + "address_line_1": "789 Business Blvd", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + }, + ein="12-3456789", + email="hr@newco.com", + legal_name="NewCo Industries LLC", + name="NewCo Industries", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.create( + address={ + "address_line_1": "789 Business Blvd", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + }, + ein="12-3456789", + email="hr@newco.com", + legal_name="NewCo Industries LLC", + name="NewCo Industries", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: VitableConnect) -> None: + employer = client.employers.retrieve( + "empr_abc123def456", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.retrieve( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.retrieve( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: VitableConnect) -> None: + employer = client.employers.update( + employer_id="empr_abc123def456", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: VitableConnect) -> None: + employer = client.employers.update( + employer_id="empr_abc123def456", + active=True, + address={ + "address_line_1": "address_line_1", + "city": "city", + "state": "xx", + "zipcode": "zipcode", + "address_line_2": "address_line_2", + }, + legal_name="x", + name="x", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.update( + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.update( + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.update( + employer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: VitableConnect) -> None: + employer = client.employers.list() + assert_matches_type(SyncPageNumberPage[EmployerListResponse], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: VitableConnect) -> None: + employer = client.employers.list( + benefit_family=["mec"], + benefit_lifecycle_stage=["open_enrollment"], + hris_provider=["string"], + hris_status=["Pending"], + include_cancelled=True, + limit=20, + page=1, + search="x", + ) + assert_matches_type(SyncPageNumberPage[EmployerListResponse], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(SyncPageNumberPage[EmployerListResponse], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(SyncPageNumberPage[EmployerListResponse], employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_ensure_payroll_integration_email(self, client: VitableConnect) -> None: + employer = client.employers.ensure_payroll_integration_email( + "empr_abc123def456", + ) + assert_matches_type(EmployerEnsurePayrollIntegrationEmailResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_ensure_payroll_integration_email(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.ensure_payroll_integration_email( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerEnsurePayrollIntegrationEmailResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_ensure_payroll_integration_email(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.ensure_payroll_integration_email( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerEnsurePayrollIntegrationEmailResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_ensure_payroll_integration_email(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.ensure_payroll_integration_email( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_benefit_plan_year_enrollments(self, client: VitableConnect) -> None: + employer = client.employers.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) + assert_matches_type( + SyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_benefit_plan_year_enrollments_with_all_params(self, client: VitableConnect) -> None: + employer = client.employers.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + election_status=["Enrolled"], + limit=20, + page=1, + search="search", + ) + assert_matches_type( + SyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_benefit_plan_year_enrollments(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type( + SyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_benefit_plan_year_enrollments(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type( + SyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], employer, path=["response"] + ) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_benefit_plan_year_enrollments(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `benefit_plan_year_id` but received ''"): + client.employers.with_raw_response.list_benefit_plan_year_enrollments( + benefit_plan_year_id="", + employer_id="empr_abc123def456", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_benefit_plan_years(self, client: VitableConnect) -> None: + employer = client.employers.list_benefit_plan_years( + "empr_abc123def456", + ) + assert_matches_type(EmployerListBenefitPlanYearsResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_benefit_plan_years(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.list_benefit_plan_years( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerListBenefitPlanYearsResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_benefit_plan_years(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.list_benefit_plan_years( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerListBenefitPlanYearsResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_benefit_plan_years(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.list_benefit_plan_years( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_employees(self, client: VitableConnect) -> None: + employer = client.employers.list_employees( + employer_id="empr_abc123def456", + ) + assert_matches_type(SyncPageNumberPage[Employee], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_employees_with_all_params(self, client: VitableConnect) -> None: + employer = client.employers.list_employees( + employer_id="empr_abc123def456", + employment_status="active", + limit=20, + page=1, + search="jane", + ) + assert_matches_type(SyncPageNumberPage[Employee], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_employees(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.list_employees( + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(SyncPageNumberPage[Employee], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_employees(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.list_employees( + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(SyncPageNumberPage[Employee], employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_employees(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.list_employees( + employer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_hris_providers(self, client: VitableConnect) -> None: + employer = client.employers.list_hris_providers() + assert_matches_type(EmployerListHRISProvidersResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_hris_providers(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.list_hris_providers() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerListHRISProvidersResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_hris_providers(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.list_hris_providers() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerListHRISProvidersResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_invoices(self, client: VitableConnect) -> None: + employer = client.employers.list_invoices( + employer_id="empr_abc123def456", + ) + assert_matches_type(EmployerListInvoicesResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_invoices_with_all_params(self, client: VitableConnect) -> None: + employer = client.employers.list_invoices( + employer_id="empr_abc123def456", + limit=20, + offset="x", + ) + assert_matches_type(EmployerListInvoicesResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_invoices(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.list_invoices( + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerListInvoicesResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_invoices(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.list_invoices( + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerListInvoicesResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_invoices(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.list_invoices( + employer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_payroll_deduction_statements(self, client: VitableConnect) -> None: + employer = client.employers.list_payroll_deduction_statements( + employer_id="empr_abc123def456", + ) + assert_matches_type( + SyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_payroll_deduction_statements_with_all_params(self, client: VitableConnect) -> None: + employer = client.employers.list_payroll_deduction_statements( + employer_id="empr_abc123def456", + limit=20, + page=1, + ) + assert_matches_type( + SyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_payroll_deduction_statements(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.list_payroll_deduction_statements( + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type( + SyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_payroll_deduction_statements(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.list_payroll_deduction_statements( + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type( + SyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], employer, path=["response"] + ) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_payroll_deduction_statements(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.list_payroll_deduction_statements( + employer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_benefit_plan_year(self, client: VitableConnect) -> None: + employer = client.employers.retrieve_benefit_plan_year( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) + assert_matches_type(EmployerRetrieveBenefitPlanYearResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve_benefit_plan_year(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.retrieve_benefit_plan_year( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerRetrieveBenefitPlanYearResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve_benefit_plan_year(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.retrieve_benefit_plan_year( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerRetrieveBenefitPlanYearResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve_benefit_plan_year(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.retrieve_benefit_plan_year( + benefit_plan_year_id="plyr_abc123def456", + employer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `benefit_plan_year_id` but received ''"): + client.employers.with_raw_response.retrieve_benefit_plan_year( + benefit_plan_year_id="", + employer_id="empr_abc123def456", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_hris(self, client: VitableConnect) -> None: + employer = client.employers.retrieve_hris( + "empr_abc123def456", + ) + assert_matches_type(EmployerRetrieveHRISResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve_hris(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.retrieve_hris( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerRetrieveHRISResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve_hris(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.retrieve_hris( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerRetrieveHRISResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve_hris(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.retrieve_hris( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_invoice_pdf(self, client: VitableConnect) -> None: + employer = client.employers.retrieve_invoice_pdf( + invoice_id="INV-00042", + employer_id="empr_abc123def456", + ) + assert_matches_type(EmployerRetrieveInvoicePdfResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve_invoice_pdf(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.retrieve_invoice_pdf( + invoice_id="INV-00042", + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerRetrieveInvoicePdfResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve_invoice_pdf(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.retrieve_invoice_pdf( + invoice_id="INV-00042", + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerRetrieveInvoicePdfResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve_invoice_pdf(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.retrieve_invoice_pdf( + invoice_id="INV-00042", + employer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invoice_id` but received ''"): + client.employers.with_raw_response.retrieve_invoice_pdf( + invoice_id="", + employer_id="empr_abc123def456", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_payroll_access_setup(self, client: VitableConnect) -> None: + employer = client.employers.retrieve_payroll_access_setup( + "empr_abc123def456", + ) + assert_matches_type(EmployerRetrievePayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve_payroll_access_setup(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.retrieve_payroll_access_setup( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerRetrievePayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve_payroll_access_setup(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.retrieve_payroll_access_setup( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerRetrievePayrollAccessSetupResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve_payroll_access_setup(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.retrieve_payroll_access_setup( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_submit_census_sync(self, client: VitableConnect) -> None: + employer = client.employers.submit_census_sync( + employer_id="empr_abc123def456", + employees=[ + { + "date_of_birth": parse_date("1990-05-15"), + "email": "jane.doe@acme.com", + "first_name": "Jane", + "last_name": "Doe", + }, + { + "date_of_birth": parse_date("1985-11-20"), + "email": "john.smith@acme.com", + "first_name": "John", + "last_name": "Smith", + }, + ], + ) + assert_matches_type(EmployerSubmitCensusSyncResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_submit_census_sync(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.submit_census_sync( + employer_id="empr_abc123def456", + employees=[ + { + "date_of_birth": parse_date("1990-05-15"), + "email": "jane.doe@acme.com", + "first_name": "Jane", + "last_name": "Doe", + }, + { + "date_of_birth": parse_date("1985-11-20"), + "email": "john.smith@acme.com", + "first_name": "John", + "last_name": "Smith", + }, + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerSubmitCensusSyncResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_submit_census_sync(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.submit_census_sync( + employer_id="empr_abc123def456", + employees=[ + { + "date_of_birth": parse_date("1990-05-15"), + "email": "jane.doe@acme.com", + "first_name": "Jane", + "last_name": "Doe", + }, + { + "date_of_birth": parse_date("1985-11-20"), + "email": "john.smith@acme.com", + "first_name": "John", + "last_name": "Smith", + }, + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerSubmitCensusSyncResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_submit_census_sync(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.submit_census_sync( + employer_id="", + employees=[ + { + "date_of_birth": parse_date("1990-05-15"), + "email": "jane.doe@acme.com", + "first_name": "Jane", + "last_name": "Doe", + }, + { + "date_of_birth": parse_date("1985-11-20"), + "email": "john.smith@acme.com", + "first_name": "John", + "last_name": "Smith", + }, + ], + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_submit_payroll_access_setup(self, client: VitableConnect) -> None: + employer = client.employers.submit_payroll_access_setup( + employer_id="empr_abc123def456", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + ) + assert_matches_type(EmployerSubmitPayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_submit_payroll_access_setup_with_all_params(self, client: VitableConnect) -> None: + employer = client.employers.submit_payroll_access_setup( + employer_id="empr_abc123def456", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + additional_access_method="SELF_SETUP", + additional_integration_confirmed=True, + additional_login_url="additional_login_url", + additional_password="additional_password", + additional_phone="additional_phone", + additional_username="additional_username", + classification_correction_source="ENTER_NAMES", + integration_confirmed=True, + login_url="login_url", + misclassified_employee_names=["string"], + missing_employee_resolution="EMAIL_CENSUS", + password="password", + phone="phone", + remaining_employee_action="VITABLE_UPDATE", + same_payroll_covers_other_eins=True, + username="username", + ) + assert_matches_type(EmployerSubmitPayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_submit_payroll_access_setup(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.submit_payroll_access_setup( + employer_id="empr_abc123def456", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerSubmitPayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_submit_payroll_access_setup(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.submit_payroll_access_setup( + employer_id="empr_abc123def456", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerSubmitPayrollAccessSetupResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_submit_payroll_access_setup(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.submit_payroll_access_setup( + employer_id="", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_settings(self, client: VitableConnect) -> None: + employer = client.employers.update_settings( + employer_id="empr_abc123def456", + pay_frequency="bi_weekly", + ) + assert_matches_type(EmployerUpdateSettingsResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update_settings(self, client: VitableConnect) -> None: + response = client.employers.with_raw_response.update_settings( + employer_id="empr_abc123def456", + pay_frequency="bi_weekly", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = response.parse() + assert_matches_type(EmployerUpdateSettingsResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update_settings(self, client: VitableConnect) -> None: + with client.employers.with_streaming_response.update_settings( + employer_id="empr_abc123def456", + pay_frequency="bi_weekly", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = response.parse() + assert_matches_type(EmployerUpdateSettingsResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update_settings(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + client.employers.with_raw_response.update_settings( + employer_id="", + pay_frequency="bi_weekly", + ) + + +class TestAsyncEmployers: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.create( + address={ + "address_line_1": "789 Business Blvd", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + }, + ein="12-3456789", + email="hr@newco.com", + legal_name="NewCo Industries LLC", + name="NewCo Industries", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.create( + address={ + "address_line_1": "789 Business Blvd", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + "address_line_2": "Floor 5", + }, + ein="12-3456789", + email="hr@newco.com", + legal_name="NewCo Industries LLC", + name="NewCo Industries", + phone_number="2065550100", + reference_id="partner-emp-001", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.create( + address={ + "address_line_1": "789 Business Blvd", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + }, + ein="12-3456789", + email="hr@newco.com", + legal_name="NewCo Industries LLC", + name="NewCo Industries", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.create( + address={ + "address_line_1": "789 Business Blvd", + "city": "Seattle", + "state": "WA", + "zipcode": "98101", + }, + ein="12-3456789", + email="hr@newco.com", + legal_name="NewCo Industries LLC", + name="NewCo Industries", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.retrieve( + "empr_abc123def456", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.retrieve( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.retrieve( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.update( + employer_id="empr_abc123def456", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.update( + employer_id="empr_abc123def456", + active=True, + address={ + "address_line_1": "address_line_1", + "city": "city", + "state": "xx", + "zipcode": "zipcode", + "address_line_2": "address_line_2", + }, + legal_name="x", + name="x", + ) + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.update( + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.update( + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.update( + employer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list() + assert_matches_type(AsyncPageNumberPage[EmployerListResponse], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list( + benefit_family=["mec"], + benefit_lifecycle_stage=["open_enrollment"], + hris_provider=["string"], + hris_status=["Pending"], + include_cancelled=True, + limit=20, + page=1, + search="x", + ) + assert_matches_type(AsyncPageNumberPage[EmployerListResponse], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(AsyncPageNumberPage[EmployerListResponse], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(AsyncPageNumberPage[EmployerListResponse], employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_ensure_payroll_integration_email(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.ensure_payroll_integration_email( + "empr_abc123def456", + ) + assert_matches_type(EmployerEnsurePayrollIntegrationEmailResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_ensure_payroll_integration_email(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.ensure_payroll_integration_email( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerEnsurePayrollIntegrationEmailResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_ensure_payroll_integration_email(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.ensure_payroll_integration_email( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerEnsurePayrollIntegrationEmailResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_ensure_payroll_integration_email(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.ensure_payroll_integration_email( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_benefit_plan_year_enrollments(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) + assert_matches_type( + AsyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_benefit_plan_year_enrollments_with_all_params( + self, async_client: AsyncVitableConnect + ) -> None: + employer = await async_client.employers.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + election_status=["Enrolled"], + limit=20, + page=1, + search="search", + ) + assert_matches_type( + AsyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_benefit_plan_year_enrollments(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type( + AsyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_benefit_plan_year_enrollments( + self, async_client: AsyncVitableConnect + ) -> None: + async with async_client.employers.with_streaming_response.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type( + AsyncPageNumberPage[EmployerListBenefitPlanYearEnrollmentsResponse], employer, path=["response"] + ) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_benefit_plan_year_enrollments(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.list_benefit_plan_year_enrollments( + benefit_plan_year_id="plyr_abc123def456", + employer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `benefit_plan_year_id` but received ''"): + await async_client.employers.with_raw_response.list_benefit_plan_year_enrollments( + benefit_plan_year_id="", + employer_id="empr_abc123def456", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_benefit_plan_years(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list_benefit_plan_years( + "empr_abc123def456", + ) + assert_matches_type(EmployerListBenefitPlanYearsResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_benefit_plan_years(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.list_benefit_plan_years( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerListBenefitPlanYearsResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_benefit_plan_years(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.list_benefit_plan_years( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerListBenefitPlanYearsResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_benefit_plan_years(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.list_benefit_plan_years( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_employees(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list_employees( + employer_id="empr_abc123def456", + ) + assert_matches_type(AsyncPageNumberPage[Employee], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_employees_with_all_params(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list_employees( + employer_id="empr_abc123def456", + employment_status="active", + limit=20, + page=1, + search="jane", + ) + assert_matches_type(AsyncPageNumberPage[Employee], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_employees(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.list_employees( + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(AsyncPageNumberPage[Employee], employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_employees(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.list_employees( + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(AsyncPageNumberPage[Employee], employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_employees(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.list_employees( + employer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_hris_providers(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list_hris_providers() + assert_matches_type(EmployerListHRISProvidersResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_hris_providers(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.list_hris_providers() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerListHRISProvidersResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_hris_providers(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.list_hris_providers() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerListHRISProvidersResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_invoices(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list_invoices( + employer_id="empr_abc123def456", + ) + assert_matches_type(EmployerListInvoicesResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_invoices_with_all_params(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list_invoices( + employer_id="empr_abc123def456", + limit=20, + offset="x", + ) + assert_matches_type(EmployerListInvoicesResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_invoices(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.list_invoices( + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerListInvoicesResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_invoices(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.list_invoices( + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerListInvoicesResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_invoices(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.list_invoices( + employer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_payroll_deduction_statements(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.list_payroll_deduction_statements( + employer_id="empr_abc123def456", + ) + assert_matches_type( + AsyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_payroll_deduction_statements_with_all_params( + self, async_client: AsyncVitableConnect + ) -> None: + employer = await async_client.employers.list_payroll_deduction_statements( + employer_id="empr_abc123def456", + limit=20, + page=1, + ) + assert_matches_type( + AsyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_payroll_deduction_statements(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.list_payroll_deduction_statements( + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type( + AsyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], employer, path=["response"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_payroll_deduction_statements( + self, async_client: AsyncVitableConnect + ) -> None: + async with async_client.employers.with_streaming_response.list_payroll_deduction_statements( + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type( + AsyncPageNumberPage[EmployerListPayrollDeductionStatementsResponse], employer, path=["response"] + ) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_payroll_deduction_statements(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.list_payroll_deduction_statements( + employer_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_benefit_plan_year(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.retrieve_benefit_plan_year( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) + assert_matches_type(EmployerRetrieveBenefitPlanYearResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve_benefit_plan_year(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.retrieve_benefit_plan_year( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerRetrieveBenefitPlanYearResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve_benefit_plan_year(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.retrieve_benefit_plan_year( + benefit_plan_year_id="plyr_abc123def456", + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerRetrieveBenefitPlanYearResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve_benefit_plan_year(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.retrieve_benefit_plan_year( + benefit_plan_year_id="plyr_abc123def456", + employer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `benefit_plan_year_id` but received ''"): + await async_client.employers.with_raw_response.retrieve_benefit_plan_year( + benefit_plan_year_id="", + employer_id="empr_abc123def456", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_hris(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.retrieve_hris( + "empr_abc123def456", + ) + assert_matches_type(EmployerRetrieveHRISResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve_hris(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.retrieve_hris( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerRetrieveHRISResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve_hris(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.retrieve_hris( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerRetrieveHRISResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve_hris(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.retrieve_hris( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_invoice_pdf(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.retrieve_invoice_pdf( + invoice_id="INV-00042", + employer_id="empr_abc123def456", + ) + assert_matches_type(EmployerRetrieveInvoicePdfResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve_invoice_pdf(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.retrieve_invoice_pdf( + invoice_id="INV-00042", + employer_id="empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerRetrieveInvoicePdfResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve_invoice_pdf(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.retrieve_invoice_pdf( + invoice_id="INV-00042", + employer_id="empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerRetrieveInvoicePdfResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve_invoice_pdf(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.retrieve_invoice_pdf( + invoice_id="INV-00042", + employer_id="", + ) + + with pytest.raises(ValueError, match=r"Expected a non-empty value for `invoice_id` but received ''"): + await async_client.employers.with_raw_response.retrieve_invoice_pdf( + invoice_id="", + employer_id="empr_abc123def456", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_payroll_access_setup(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.retrieve_payroll_access_setup( + "empr_abc123def456", + ) + assert_matches_type(EmployerRetrievePayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve_payroll_access_setup(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.retrieve_payroll_access_setup( + "empr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerRetrievePayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve_payroll_access_setup(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.retrieve_payroll_access_setup( + "empr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerRetrievePayrollAccessSetupResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve_payroll_access_setup(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.retrieve_payroll_access_setup( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_submit_census_sync(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.submit_census_sync( + employer_id="empr_abc123def456", + employees=[ + { + "date_of_birth": parse_date("1990-05-15"), + "email": "jane.doe@acme.com", + "first_name": "Jane", + "last_name": "Doe", + }, + { + "date_of_birth": parse_date("1985-11-20"), + "email": "john.smith@acme.com", + "first_name": "John", + "last_name": "Smith", + }, + ], + ) + assert_matches_type(EmployerSubmitCensusSyncResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_submit_census_sync(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.submit_census_sync( + employer_id="empr_abc123def456", + employees=[ + { + "date_of_birth": parse_date("1990-05-15"), + "email": "jane.doe@acme.com", + "first_name": "Jane", + "last_name": "Doe", + }, + { + "date_of_birth": parse_date("1985-11-20"), + "email": "john.smith@acme.com", + "first_name": "John", + "last_name": "Smith", + }, + ], + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerSubmitCensusSyncResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_submit_census_sync(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.submit_census_sync( + employer_id="empr_abc123def456", + employees=[ + { + "date_of_birth": parse_date("1990-05-15"), + "email": "jane.doe@acme.com", + "first_name": "Jane", + "last_name": "Doe", + }, + { + "date_of_birth": parse_date("1985-11-20"), + "email": "john.smith@acme.com", + "first_name": "John", + "last_name": "Smith", + }, + ], + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerSubmitCensusSyncResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_submit_census_sync(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.submit_census_sync( + employer_id="", + employees=[ + { + "date_of_birth": parse_date("1990-05-15"), + "email": "jane.doe@acme.com", + "first_name": "Jane", + "last_name": "Doe", + }, + { + "date_of_birth": parse_date("1985-11-20"), + "email": "john.smith@acme.com", + "first_name": "John", + "last_name": "Smith", + }, + ], + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_submit_payroll_access_setup(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.submit_payroll_access_setup( + employer_id="empr_abc123def456", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + ) + assert_matches_type(EmployerSubmitPayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_submit_payroll_access_setup_with_all_params(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.submit_payroll_access_setup( + employer_id="empr_abc123def456", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + additional_access_method="SELF_SETUP", + additional_integration_confirmed=True, + additional_login_url="additional_login_url", + additional_password="additional_password", + additional_phone="additional_phone", + additional_username="additional_username", + classification_correction_source="ENTER_NAMES", + integration_confirmed=True, + login_url="login_url", + misclassified_employee_names=["string"], + missing_employee_resolution="EMAIL_CENSUS", + password="password", + phone="phone", + remaining_employee_action="VITABLE_UPDATE", + same_payroll_covers_other_eins=True, + username="username", + ) + assert_matches_type(EmployerSubmitPayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_submit_payroll_access_setup(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.submit_payroll_access_setup( + employer_id="empr_abc123def456", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerSubmitPayrollAccessSetupResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_submit_payroll_access_setup(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.submit_payroll_access_setup( + employer_id="empr_abc123def456", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerSubmitPayrollAccessSetupResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_submit_payroll_access_setup(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.submit_payroll_access_setup( + employer_id="", + access_method="SELF_SETUP", + all_benefit_eligible_employees_present=True, + classifications_accurate=True, + employees_in_payroll_acknowledged=True, + has_additional_payroll_system=True, + is_controlled_group=True, + payroll_data_impacts_eligibility_acknowledged=True, + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_settings(self, async_client: AsyncVitableConnect) -> None: + employer = await async_client.employers.update_settings( + employer_id="empr_abc123def456", + pay_frequency="bi_weekly", + ) + assert_matches_type(EmployerUpdateSettingsResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update_settings(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.employers.with_raw_response.update_settings( + employer_id="empr_abc123def456", + pay_frequency="bi_weekly", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + employer = await response.parse() + assert_matches_type(EmployerUpdateSettingsResponse, employer, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update_settings(self, async_client: AsyncVitableConnect) -> None: + async with async_client.employers.with_streaming_response.update_settings( + employer_id="empr_abc123def456", + pay_frequency="bi_weekly", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + employer = await response.parse() + assert_matches_type(EmployerUpdateSettingsResponse, employer, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update_settings(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `employer_id` but received ''"): + await async_client.employers.with_raw_response.update_settings( + employer_id="", + pay_frequency="bi_weekly", + ) diff --git a/tests/api_resources/test_enrollments.py b/tests/api_resources/test_enrollments.py new file mode 100644 index 0000000..68ab81d --- /dev/null +++ b/tests/api_resources/test_enrollments.py @@ -0,0 +1,323 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import ( + EnrollmentReissueResponse, + EnrollmentRetrieveResponse, +) + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestEnrollments: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: VitableConnect) -> None: + enrollment = client.enrollments.retrieve( + "enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + assert_matches_type(EnrollmentRetrieveResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: VitableConnect) -> None: + response = client.enrollments.with_raw_response.retrieve( + "enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + enrollment = response.parse() + assert_matches_type(EnrollmentRetrieveResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: VitableConnect) -> None: + with client.enrollments.with_streaming_response.retrieve( + "enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + enrollment = response.parse() + assert_matches_type(EnrollmentRetrieveResponse, enrollment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `enrollment_id` but received ''"): + client.enrollments.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_reissue(self, client: VitableConnect) -> None: + enrollment = client.enrollments.reissue( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + assert_matches_type(EnrollmentReissueResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_reissue_with_all_params(self, client: VitableConnect) -> None: + enrollment = client.enrollments.reissue( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", + reason="Member needs a new election after a qualifying event.", + ticket_number="BPT-1234", + ) + assert_matches_type(EnrollmentReissueResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_reissue(self, client: VitableConnect) -> None: + response = client.enrollments.with_raw_response.reissue( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + enrollment = response.parse() + assert_matches_type(EnrollmentReissueResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_reissue(self, client: VitableConnect) -> None: + with client.enrollments.with_streaming_response.reissue( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + enrollment = response.parse() + assert_matches_type(EnrollmentReissueResponse, enrollment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_reissue(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `enrollment_id` but received ''"): + client.enrollments.with_raw_response.reissue( + enrollment_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_terminate(self, client: VitableConnect) -> None: + enrollment = client.enrollments.terminate( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + assert enrollment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_terminate_with_all_params(self, client: VitableConnect) -> None: + enrollment = client.enrollments.terminate( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", + reason="Member requested coverage termination after a qualifying event.", + ticket_number="BPT-1234", + ) + assert enrollment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_terminate(self, client: VitableConnect) -> None: + response = client.enrollments.with_raw_response.terminate( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + enrollment = response.parse() + assert enrollment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_terminate(self, client: VitableConnect) -> None: + with client.enrollments.with_streaming_response.terminate( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + enrollment = response.parse() + assert enrollment is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_terminate(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `enrollment_id` but received ''"): + client.enrollments.with_raw_response.terminate( + enrollment_id="", + ) + + +class TestAsyncEnrollments: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncVitableConnect) -> None: + enrollment = await async_client.enrollments.retrieve( + "enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + assert_matches_type(EnrollmentRetrieveResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.enrollments.with_raw_response.retrieve( + "enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + enrollment = await response.parse() + assert_matches_type(EnrollmentRetrieveResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + async with async_client.enrollments.with_streaming_response.retrieve( + "enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + enrollment = await response.parse() + assert_matches_type(EnrollmentRetrieveResponse, enrollment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `enrollment_id` but received ''"): + await async_client.enrollments.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_reissue(self, async_client: AsyncVitableConnect) -> None: + enrollment = await async_client.enrollments.reissue( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + assert_matches_type(EnrollmentReissueResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_reissue_with_all_params(self, async_client: AsyncVitableConnect) -> None: + enrollment = await async_client.enrollments.reissue( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", + reason="Member needs a new election after a qualifying event.", + ticket_number="BPT-1234", + ) + assert_matches_type(EnrollmentReissueResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_reissue(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.enrollments.with_raw_response.reissue( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + enrollment = await response.parse() + assert_matches_type(EnrollmentReissueResponse, enrollment, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_reissue(self, async_client: AsyncVitableConnect) -> None: + async with async_client.enrollments.with_streaming_response.reissue( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + enrollment = await response.parse() + assert_matches_type(EnrollmentReissueResponse, enrollment, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_reissue(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `enrollment_id` but received ''"): + await async_client.enrollments.with_raw_response.reissue( + enrollment_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_terminate(self, async_client: AsyncVitableConnect) -> None: + enrollment = await async_client.enrollments.terminate( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + assert enrollment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_terminate_with_all_params(self, async_client: AsyncVitableConnect) -> None: + enrollment = await async_client.enrollments.terminate( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + qualifying_life_event_id="qle_AAAAAAAAAAAAAAAAAAAAAQ", + reason="Member requested coverage termination after a qualifying event.", + ticket_number="BPT-1234", + ) + assert enrollment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_terminate(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.enrollments.with_raw_response.terminate( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + enrollment = await response.parse() + assert enrollment is None + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_terminate(self, async_client: AsyncVitableConnect) -> None: + async with async_client.enrollments.with_streaming_response.terminate( + enrollment_id="enrl_AAAAAAAAAAAAAAAAAAAAAQ", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + enrollment = await response.parse() + assert enrollment is None + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_terminate(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `enrollment_id` but received ''"): + await async_client.enrollments.with_raw_response.terminate( + enrollment_id="", + ) diff --git a/tests/api_resources/test_groups.py b/tests/api_resources/test_groups.py new file mode 100644 index 0000000..c2d2a88 --- /dev/null +++ b/tests/api_resources/test_groups.py @@ -0,0 +1,361 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import Group, GroupResponse +from vitable_connect.pagination import SyncPageNumberPage, AsyncPageNumberPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestGroups: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: VitableConnect) -> None: + group = client.groups.create( + external_reference_id="mol_seg_001", + name="Tier 1", + ) + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: VitableConnect) -> None: + response = client.groups.with_raw_response.create( + external_reference_id="mol_seg_001", + name="Tier 1", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: VitableConnect) -> None: + with client.groups.with_streaming_response.create( + external_reference_id="mol_seg_001", + name="Tier 1", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: VitableConnect) -> None: + group = client.groups.retrieve( + "grp_abc123def456", + ) + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: VitableConnect) -> None: + response = client.groups.with_raw_response.retrieve( + "grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: VitableConnect) -> None: + with client.groups.with_streaming_response.retrieve( + "grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update(self, client: VitableConnect) -> None: + group = client.groups.update( + group_id="grp_abc123def456", + ) + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_update_with_all_params(self, client: VitableConnect) -> None: + group = client.groups.update( + group_id="grp_abc123def456", + external_reference_id="mol_seg_001_v2", + name="Tier 1 (renamed)", + ) + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_update(self, client: VitableConnect) -> None: + response = client.groups.with_raw_response.update( + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_update(self, client: VitableConnect) -> None: + with client.groups.with_streaming_response.update( + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_update(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + client.groups.with_raw_response.update( + group_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: VitableConnect) -> None: + group = client.groups.list() + assert_matches_type(SyncPageNumberPage[Group], group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: VitableConnect) -> None: + group = client.groups.list( + limit=20, + page=1, + ) + assert_matches_type(SyncPageNumberPage[Group], group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: VitableConnect) -> None: + response = client.groups.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = response.parse() + assert_matches_type(SyncPageNumberPage[Group], group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: VitableConnect) -> None: + with client.groups.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = response.parse() + assert_matches_type(SyncPageNumberPage[Group], group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncGroups: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncVitableConnect) -> None: + group = await async_client.groups.create( + external_reference_id="mol_seg_001", + name="Tier 1", + ) + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.groups.with_raw_response.create( + external_reference_id="mol_seg_001", + name="Tier 1", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncVitableConnect) -> None: + async with async_client.groups.with_streaming_response.create( + external_reference_id="mol_seg_001", + name="Tier 1", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncVitableConnect) -> None: + group = await async_client.groups.retrieve( + "grp_abc123def456", + ) + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.groups.with_raw_response.retrieve( + "grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + async with async_client.groups.with_streaming_response.retrieve( + "grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update(self, async_client: AsyncVitableConnect) -> None: + group = await async_client.groups.update( + group_id="grp_abc123def456", + ) + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_update_with_all_params(self, async_client: AsyncVitableConnect) -> None: + group = await async_client.groups.update( + group_id="grp_abc123def456", + external_reference_id="mol_seg_001_v2", + name="Tier 1 (renamed)", + ) + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_update(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.groups.with_raw_response.update( + group_id="grp_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_update(self, async_client: AsyncVitableConnect) -> None: + async with async_client.groups.with_streaming_response.update( + group_id="grp_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(GroupResponse, group, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_update(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `group_id` but received ''"): + await async_client.groups.with_raw_response.update( + group_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncVitableConnect) -> None: + group = await async_client.groups.list() + assert_matches_type(AsyncPageNumberPage[Group], group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncVitableConnect) -> None: + group = await async_client.groups.list( + limit=20, + page=1, + ) + assert_matches_type(AsyncPageNumberPage[Group], group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.groups.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + group = await response.parse() + assert_matches_type(AsyncPageNumberPage[Group], group, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncVitableConnect) -> None: + async with async_client.groups.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + group = await response.parse() + assert_matches_type(AsyncPageNumberPage[Group], group, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_members.py b/tests/api_resources/test_members.py new file mode 100644 index 0000000..4c81407 --- /dev/null +++ b/tests/api_resources/test_members.py @@ -0,0 +1,720 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import ( + MemberListResponse, + MemberRetrieveResponse, + MemberListIDCardsResponse, + MemberListDependentsResponse, + MemberListEmploymentsResponse, + MemberListEnrollmentsResponse, + MemberRetrieveHouseholdResponse, + MemberListQualifyingLifeEventsResponse, +) +from vitable_connect.pagination import SyncPageNumberPage, AsyncPageNumberPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestMembers: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: VitableConnect) -> None: + member = client.members.retrieve( + "mbr_abc123def456", + ) + assert_matches_type(MemberRetrieveResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: VitableConnect) -> None: + response = client.members.with_raw_response.retrieve( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberRetrieveResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: VitableConnect) -> None: + with client.members.with_streaming_response.retrieve( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberRetrieveResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + client.members.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: VitableConnect) -> None: + member = client.members.list() + assert_matches_type(SyncPageNumberPage[MemberListResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: VitableConnect) -> None: + member = client.members.list( + limit=20, + page=1, + search="search", + ) + assert_matches_type(SyncPageNumberPage[MemberListResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: VitableConnect) -> None: + response = client.members.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(SyncPageNumberPage[MemberListResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: VitableConnect) -> None: + with client.members.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(SyncPageNumberPage[MemberListResponse], member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_dependents(self, client: VitableConnect) -> None: + member = client.members.list_dependents( + "mbr_abc123def456", + ) + assert_matches_type(MemberListDependentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_dependents(self, client: VitableConnect) -> None: + response = client.members.with_raw_response.list_dependents( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberListDependentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_dependents(self, client: VitableConnect) -> None: + with client.members.with_streaming_response.list_dependents( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberListDependentsResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_dependents(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + client.members.with_raw_response.list_dependents( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_employments(self, client: VitableConnect) -> None: + member = client.members.list_employments( + "mbr_abc123def456", + ) + assert_matches_type(MemberListEmploymentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_employments(self, client: VitableConnect) -> None: + response = client.members.with_raw_response.list_employments( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberListEmploymentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_employments(self, client: VitableConnect) -> None: + with client.members.with_streaming_response.list_employments( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberListEmploymentsResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_employments(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + client.members.with_raw_response.list_employments( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_enrollments(self, client: VitableConnect) -> None: + member = client.members.list_enrollments( + "mbr_abc123def456", + ) + assert_matches_type(MemberListEnrollmentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_enrollments(self, client: VitableConnect) -> None: + response = client.members.with_raw_response.list_enrollments( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberListEnrollmentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_enrollments(self, client: VitableConnect) -> None: + with client.members.with_streaming_response.list_enrollments( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberListEnrollmentsResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_enrollments(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + client.members.with_raw_response.list_enrollments( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_id_cards(self, client: VitableConnect) -> None: + member = client.members.list_id_cards( + "mbr_abc123def456", + ) + assert_matches_type(MemberListIDCardsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_id_cards(self, client: VitableConnect) -> None: + response = client.members.with_raw_response.list_id_cards( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberListIDCardsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_id_cards(self, client: VitableConnect) -> None: + with client.members.with_streaming_response.list_id_cards( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberListIDCardsResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_id_cards(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + client.members.with_raw_response.list_id_cards( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_qualifying_life_events(self, client: VitableConnect) -> None: + member = client.members.list_qualifying_life_events( + member_id="mbr_abc123def456", + ) + assert_matches_type(SyncPageNumberPage[MemberListQualifyingLifeEventsResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_qualifying_life_events_with_all_params(self, client: VitableConnect) -> None: + member = client.members.list_qualifying_life_events( + member_id="mbr_abc123def456", + limit=20, + page=1, + status="approved", + ) + assert_matches_type(SyncPageNumberPage[MemberListQualifyingLifeEventsResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_qualifying_life_events(self, client: VitableConnect) -> None: + response = client.members.with_raw_response.list_qualifying_life_events( + member_id="mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(SyncPageNumberPage[MemberListQualifyingLifeEventsResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_qualifying_life_events(self, client: VitableConnect) -> None: + with client.members.with_streaming_response.list_qualifying_life_events( + member_id="mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(SyncPageNumberPage[MemberListQualifyingLifeEventsResponse], member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_qualifying_life_events(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + client.members.with_raw_response.list_qualifying_life_events( + member_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve_household(self, client: VitableConnect) -> None: + member = client.members.retrieve_household( + "mbr_abc123def456", + ) + assert_matches_type(MemberRetrieveHouseholdResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve_household(self, client: VitableConnect) -> None: + response = client.members.with_raw_response.retrieve_household( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = response.parse() + assert_matches_type(MemberRetrieveHouseholdResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve_household(self, client: VitableConnect) -> None: + with client.members.with_streaming_response.retrieve_household( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = response.parse() + assert_matches_type(MemberRetrieveHouseholdResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve_household(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + client.members.with_raw_response.retrieve_household( + "", + ) + + +class TestAsyncMembers: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.retrieve( + "mbr_abc123def456", + ) + assert_matches_type(MemberRetrieveResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.members.with_raw_response.retrieve( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberRetrieveResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + async with async_client.members.with_streaming_response.retrieve( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberRetrieveResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + await async_client.members.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.list() + assert_matches_type(AsyncPageNumberPage[MemberListResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.list( + limit=20, + page=1, + search="search", + ) + assert_matches_type(AsyncPageNumberPage[MemberListResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.members.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(AsyncPageNumberPage[MemberListResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncVitableConnect) -> None: + async with async_client.members.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(AsyncPageNumberPage[MemberListResponse], member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_dependents(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.list_dependents( + "mbr_abc123def456", + ) + assert_matches_type(MemberListDependentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_dependents(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.members.with_raw_response.list_dependents( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberListDependentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_dependents(self, async_client: AsyncVitableConnect) -> None: + async with async_client.members.with_streaming_response.list_dependents( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberListDependentsResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_dependents(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + await async_client.members.with_raw_response.list_dependents( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_employments(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.list_employments( + "mbr_abc123def456", + ) + assert_matches_type(MemberListEmploymentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_employments(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.members.with_raw_response.list_employments( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberListEmploymentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_employments(self, async_client: AsyncVitableConnect) -> None: + async with async_client.members.with_streaming_response.list_employments( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberListEmploymentsResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_employments(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + await async_client.members.with_raw_response.list_employments( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_enrollments(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.list_enrollments( + "mbr_abc123def456", + ) + assert_matches_type(MemberListEnrollmentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_enrollments(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.members.with_raw_response.list_enrollments( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberListEnrollmentsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_enrollments(self, async_client: AsyncVitableConnect) -> None: + async with async_client.members.with_streaming_response.list_enrollments( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberListEnrollmentsResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_enrollments(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + await async_client.members.with_raw_response.list_enrollments( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_id_cards(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.list_id_cards( + "mbr_abc123def456", + ) + assert_matches_type(MemberListIDCardsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_id_cards(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.members.with_raw_response.list_id_cards( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberListIDCardsResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_id_cards(self, async_client: AsyncVitableConnect) -> None: + async with async_client.members.with_streaming_response.list_id_cards( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberListIDCardsResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_id_cards(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + await async_client.members.with_raw_response.list_id_cards( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_qualifying_life_events(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.list_qualifying_life_events( + member_id="mbr_abc123def456", + ) + assert_matches_type(AsyncPageNumberPage[MemberListQualifyingLifeEventsResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_qualifying_life_events_with_all_params(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.list_qualifying_life_events( + member_id="mbr_abc123def456", + limit=20, + page=1, + status="approved", + ) + assert_matches_type(AsyncPageNumberPage[MemberListQualifyingLifeEventsResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_qualifying_life_events(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.members.with_raw_response.list_qualifying_life_events( + member_id="mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(AsyncPageNumberPage[MemberListQualifyingLifeEventsResponse], member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_qualifying_life_events(self, async_client: AsyncVitableConnect) -> None: + async with async_client.members.with_streaming_response.list_qualifying_life_events( + member_id="mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(AsyncPageNumberPage[MemberListQualifyingLifeEventsResponse], member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_qualifying_life_events(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + await async_client.members.with_raw_response.list_qualifying_life_events( + member_id="", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve_household(self, async_client: AsyncVitableConnect) -> None: + member = await async_client.members.retrieve_household( + "mbr_abc123def456", + ) + assert_matches_type(MemberRetrieveHouseholdResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve_household(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.members.with_raw_response.retrieve_household( + "mbr_abc123def456", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + member = await response.parse() + assert_matches_type(MemberRetrieveHouseholdResponse, member, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve_household(self, async_client: AsyncVitableConnect) -> None: + async with async_client.members.with_streaming_response.retrieve_household( + "mbr_abc123def456", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + member = await response.parse() + assert_matches_type(MemberRetrieveHouseholdResponse, member, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve_household(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `member_id` but received ''"): + await async_client.members.with_raw_response.retrieve_household( + "", + ) diff --git a/tests/api_resources/test_organizations.py b/tests/api_resources/test_organizations.py new file mode 100644 index 0000000..61387f5 --- /dev/null +++ b/tests/api_resources/test_organizations.py @@ -0,0 +1,166 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import OrganizationListResponse, OrganizationCreateResponse + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestOrganizations: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create(self, client: VitableConnect) -> None: + organization = client.organizations.create( + name="Acme Brokerage", + ) + assert_matches_type(OrganizationCreateResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_create_with_all_params(self, client: VitableConnect) -> None: + organization = client.organizations.create( + name="Acme Brokerage", + type="BROKERAGE", + ) + assert_matches_type(OrganizationCreateResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_create(self, client: VitableConnect) -> None: + response = client.organizations.with_raw_response.create( + name="Acme Brokerage", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + organization = response.parse() + assert_matches_type(OrganizationCreateResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_create(self, client: VitableConnect) -> None: + with client.organizations.with_streaming_response.create( + name="Acme Brokerage", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + organization = response.parse() + assert_matches_type(OrganizationCreateResponse, organization, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: VitableConnect) -> None: + organization = client.organizations.list() + assert_matches_type(OrganizationListResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: VitableConnect) -> None: + response = client.organizations.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + organization = response.parse() + assert_matches_type(OrganizationListResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: VitableConnect) -> None: + with client.organizations.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + organization = response.parse() + assert_matches_type(OrganizationListResponse, organization, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncOrganizations: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create(self, async_client: AsyncVitableConnect) -> None: + organization = await async_client.organizations.create( + name="Acme Brokerage", + ) + assert_matches_type(OrganizationCreateResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_create_with_all_params(self, async_client: AsyncVitableConnect) -> None: + organization = await async_client.organizations.create( + name="Acme Brokerage", + type="BROKERAGE", + ) + assert_matches_type(OrganizationCreateResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_create(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.organizations.with_raw_response.create( + name="Acme Brokerage", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + organization = await response.parse() + assert_matches_type(OrganizationCreateResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_create(self, async_client: AsyncVitableConnect) -> None: + async with async_client.organizations.with_streaming_response.create( + name="Acme Brokerage", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + organization = await response.parse() + assert_matches_type(OrganizationCreateResponse, organization, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncVitableConnect) -> None: + organization = await async_client.organizations.list() + assert_matches_type(OrganizationListResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.organizations.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + organization = await response.parse() + assert_matches_type(OrganizationListResponse, organization, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncVitableConnect) -> None: + async with async_client.organizations.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + organization = await response.parse() + assert_matches_type(OrganizationListResponse, organization, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_plans.py b/tests/api_resources/test_plans.py new file mode 100644 index 0000000..b9c8b6e --- /dev/null +++ b/tests/api_resources/test_plans.py @@ -0,0 +1,99 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import PlanListResponse +from vitable_connect.pagination import SyncPageNumberPage, AsyncPageNumberPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestPlans: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: VitableConnect) -> None: + plan = client.plans.list() + assert_matches_type(SyncPageNumberPage[PlanListResponse], plan, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: VitableConnect) -> None: + plan = client.plans.list( + limit=20, + page=1, + ) + assert_matches_type(SyncPageNumberPage[PlanListResponse], plan, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: VitableConnect) -> None: + response = client.plans.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + plan = response.parse() + assert_matches_type(SyncPageNumberPage[PlanListResponse], plan, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: VitableConnect) -> None: + with client.plans.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + plan = response.parse() + assert_matches_type(SyncPageNumberPage[PlanListResponse], plan, path=["response"]) + + assert cast(Any, response.is_closed) is True + + +class TestAsyncPlans: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncVitableConnect) -> None: + plan = await async_client.plans.list() + assert_matches_type(AsyncPageNumberPage[PlanListResponse], plan, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncVitableConnect) -> None: + plan = await async_client.plans.list( + limit=20, + page=1, + ) + assert_matches_type(AsyncPageNumberPage[PlanListResponse], plan, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.plans.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + plan = await response.parse() + assert_matches_type(AsyncPageNumberPage[PlanListResponse], plan, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncVitableConnect) -> None: + async with async_client.plans.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + plan = await response.parse() + assert_matches_type(AsyncPageNumberPage[PlanListResponse], plan, path=["response"]) + + assert cast(Any, response.is_closed) is True diff --git a/tests/api_resources/test_webhook_events.py b/tests/api_resources/test_webhook_events.py new file mode 100644 index 0000000..93f98f8 --- /dev/null +++ b/tests/api_resources/test_webhook_events.py @@ -0,0 +1,282 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +from typing import Any, cast + +import pytest + +from tests.utils import assert_matches_type +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect.types import ( + WebhookEvent, + WebhookEventRetrieveResponse, + WebhookEventListDeliveriesResponse, +) +from vitable_connect._utils import parse_datetime +from vitable_connect.pagination import SyncPageNumberPage, AsyncPageNumberPage + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + + +class TestWebhookEvents: + parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_retrieve(self, client: VitableConnect) -> None: + webhook_event = client.webhook_events.retrieve( + "event_id", + ) + assert_matches_type(WebhookEventRetrieveResponse, webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_retrieve(self, client: VitableConnect) -> None: + response = client.webhook_events.with_raw_response.retrieve( + "event_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook_event = response.parse() + assert_matches_type(WebhookEventRetrieveResponse, webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_retrieve(self, client: VitableConnect) -> None: + with client.webhook_events.with_streaming_response.retrieve( + "event_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook_event = response.parse() + assert_matches_type(WebhookEventRetrieveResponse, webhook_event, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_retrieve(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `event_id` but received ''"): + client.webhook_events.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list(self, client: VitableConnect) -> None: + webhook_event = client.webhook_events.list() + assert_matches_type(SyncPageNumberPage[WebhookEvent], webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_with_all_params(self, client: VitableConnect) -> None: + webhook_event = client.webhook_events.list( + created_after=parse_datetime("2019-12-27T18:11:19.117Z"), + created_before=parse_datetime("2019-12-27T18:11:19.117Z"), + event_name="enrollment.accepted", + limit=20, + page=1, + resource_id="x", + resource_type="enrollment", + ) + assert_matches_type(SyncPageNumberPage[WebhookEvent], webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list(self, client: VitableConnect) -> None: + response = client.webhook_events.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook_event = response.parse() + assert_matches_type(SyncPageNumberPage[WebhookEvent], webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list(self, client: VitableConnect) -> None: + with client.webhook_events.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook_event = response.parse() + assert_matches_type(SyncPageNumberPage[WebhookEvent], webhook_event, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_method_list_deliveries(self, client: VitableConnect) -> None: + webhook_event = client.webhook_events.list_deliveries( + "event_id", + ) + assert_matches_type(WebhookEventListDeliveriesResponse, webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_raw_response_list_deliveries(self, client: VitableConnect) -> None: + response = client.webhook_events.with_raw_response.list_deliveries( + "event_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook_event = response.parse() + assert_matches_type(WebhookEventListDeliveriesResponse, webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_streaming_response_list_deliveries(self, client: VitableConnect) -> None: + with client.webhook_events.with_streaming_response.list_deliveries( + "event_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook_event = response.parse() + assert_matches_type(WebhookEventListDeliveriesResponse, webhook_event, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + def test_path_params_list_deliveries(self, client: VitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `event_id` but received ''"): + client.webhook_events.with_raw_response.list_deliveries( + "", + ) + + +class TestAsyncWebhookEvents: + parametrize = pytest.mark.parametrize( + "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"] + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_retrieve(self, async_client: AsyncVitableConnect) -> None: + webhook_event = await async_client.webhook_events.retrieve( + "event_id", + ) + assert_matches_type(WebhookEventRetrieveResponse, webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.webhook_events.with_raw_response.retrieve( + "event_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook_event = await response.parse() + assert_matches_type(WebhookEventRetrieveResponse, webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_retrieve(self, async_client: AsyncVitableConnect) -> None: + async with async_client.webhook_events.with_streaming_response.retrieve( + "event_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook_event = await response.parse() + assert_matches_type(WebhookEventRetrieveResponse, webhook_event, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_retrieve(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `event_id` but received ''"): + await async_client.webhook_events.with_raw_response.retrieve( + "", + ) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list(self, async_client: AsyncVitableConnect) -> None: + webhook_event = await async_client.webhook_events.list() + assert_matches_type(AsyncPageNumberPage[WebhookEvent], webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_with_all_params(self, async_client: AsyncVitableConnect) -> None: + webhook_event = await async_client.webhook_events.list( + created_after=parse_datetime("2019-12-27T18:11:19.117Z"), + created_before=parse_datetime("2019-12-27T18:11:19.117Z"), + event_name="enrollment.accepted", + limit=20, + page=1, + resource_id="x", + resource_type="enrollment", + ) + assert_matches_type(AsyncPageNumberPage[WebhookEvent], webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.webhook_events.with_raw_response.list() + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook_event = await response.parse() + assert_matches_type(AsyncPageNumberPage[WebhookEvent], webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list(self, async_client: AsyncVitableConnect) -> None: + async with async_client.webhook_events.with_streaming_response.list() as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook_event = await response.parse() + assert_matches_type(AsyncPageNumberPage[WebhookEvent], webhook_event, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_method_list_deliveries(self, async_client: AsyncVitableConnect) -> None: + webhook_event = await async_client.webhook_events.list_deliveries( + "event_id", + ) + assert_matches_type(WebhookEventListDeliveriesResponse, webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_raw_response_list_deliveries(self, async_client: AsyncVitableConnect) -> None: + response = await async_client.webhook_events.with_raw_response.list_deliveries( + "event_id", + ) + + assert response.is_closed is True + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + webhook_event = await response.parse() + assert_matches_type(WebhookEventListDeliveriesResponse, webhook_event, path=["response"]) + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_streaming_response_list_deliveries(self, async_client: AsyncVitableConnect) -> None: + async with async_client.webhook_events.with_streaming_response.list_deliveries( + "event_id", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + webhook_event = await response.parse() + assert_matches_type(WebhookEventListDeliveriesResponse, webhook_event, path=["response"]) + + assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Mock server tests are disabled") + @parametrize + async def test_path_params_list_deliveries(self, async_client: AsyncVitableConnect) -> None: + with pytest.raises(ValueError, match=r"Expected a non-empty value for `event_id` but received ''"): + await async_client.webhook_events.with_raw_response.list_deliveries( + "", + ) diff --git a/tests/conftest.py b/tests/conftest.py index 25710db..f330a79 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,21 +1,84 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import os +import logging +from typing import TYPE_CHECKING, Iterator, AsyncIterator + +import httpx import pytest +from pytest_asyncio import is_async_test + +from vitable_connect import VitableConnect, AsyncVitableConnect, DefaultAioHttpClient +from vitable_connect._utils import is_dict + +if TYPE_CHECKING: + from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage] +pytest.register_assert_rewrite("tests.utils") -def _has_httpx_aiohttp() -> bool: - """Check if httpx_aiohttp is importable.""" - try: - import httpx_aiohttp # type: ignore[import-not-found] # noqa: F401 +logging.getLogger("vitable_connect").setLevel(logging.DEBUG) - return True - except ImportError: - return False +# automatically add `pytest.mark.asyncio()` to all of our async tests +# so we don't have to add that boilerplate everywhere +def pytest_collection_modifyitems(items: list[pytest.Function]) -> None: + pytest_asyncio_tests = (item for item in items if is_async_test(item)) + session_scope_marker = pytest.mark.asyncio(loop_scope="session") + for async_test in pytest_asyncio_tests: + async_test.add_marker(session_scope_marker, append=False) -def pytest_collection_modifyitems(config: pytest.Config, items: list) -> None: - """Auto-skip @pytest.mark.aiohttp tests when httpx_aiohttp is not installed.""" - if _has_httpx_aiohttp(): - return - skip_aiohttp = pytest.mark.skip(reason="httpx_aiohttp not installed") + # We skip tests that use both the aiohttp client and respx_mock as respx_mock + # doesn't support custom transports. for item in items: - if "aiohttp" in item.keywords: - item.add_marker(skip_aiohttp) + if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames: + continue + + if not hasattr(item, "callspec"): + continue + + async_client_param = item.callspec.params.get("async_client") + if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp": + item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock")) + + +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") + +api_key = "My API Key" + + +@pytest.fixture(scope="session") +def client(request: FixtureRequest) -> Iterator[VitableConnect]: + strict = getattr(request, "param", True) + if not isinstance(strict, bool): + raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}") + + with VitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client: + yield client + + +@pytest.fixture(scope="session") +async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncVitableConnect]: + param = getattr(request, "param", True) + + # defaults + strict = True + http_client: None | httpx.AsyncClient = None + + if isinstance(param, bool): + strict = param + elif is_dict(param): + strict = param.get("strict", True) + assert isinstance(strict, bool) + + http_client_type = param.get("http_client", "httpx") + if http_client_type == "aiohttp": + http_client = DefaultAioHttpClient() + else: + raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict") + + async with AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client + ) as client: + yield client diff --git a/tests/custom/test_client.py b/tests/custom/test_client.py deleted file mode 100644 index ab04ce6..0000000 --- a/tests/custom/test_client.py +++ /dev/null @@ -1,7 +0,0 @@ -import pytest - - -# Get started with writing tests with pytest at https://docs.pytest.org -@pytest.mark.skip(reason="Unimplemented") -def test_client() -> None: - assert True diff --git a/tests/sample_file.txt b/tests/sample_file.txt new file mode 100644 index 0000000..af5626b --- /dev/null +++ b/tests/sample_file.txt @@ -0,0 +1 @@ +Hello, world! diff --git a/tests/test_aiohttp_autodetect.py b/tests/test_aiohttp_autodetect.py deleted file mode 100644 index b4a602a..0000000 --- a/tests/test_aiohttp_autodetect.py +++ /dev/null @@ -1,116 +0,0 @@ -import importlib -import sys -import unittest -from unittest import mock - -import httpx -import pytest - - -class TestMakeDefaultAsyncClientWithoutAiohttp(unittest.TestCase): - """Tests for _make_default_async_client when httpx_aiohttp is NOT installed.""" - - def test_returns_httpx_async_client(self) -> None: - """When httpx_aiohttp is not installed, returns plain httpx.AsyncClient.""" - with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): - from vitable_connect.client import _make_default_async_client - - client = _make_default_async_client(timeout=60, follow_redirects=True) - self.assertIsInstance(client, httpx.AsyncClient) - self.assertEqual(client.timeout.read, 60) - self.assertTrue(client.follow_redirects) - - def test_follow_redirects_none(self) -> None: - """When follow_redirects is None, omits it from httpx.AsyncClient.""" - with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): - from vitable_connect.client import _make_default_async_client - - client = _make_default_async_client(timeout=60, follow_redirects=None) - self.assertIsInstance(client, httpx.AsyncClient) - self.assertFalse(client.follow_redirects) - - def test_explicit_httpx_client_bypasses_autodetect(self) -> None: - """When user passes httpx_client explicitly, _make_default_async_client is not called.""" - - explicit_client = httpx.AsyncClient(timeout=120) - with mock.patch("vitable_connect.client._make_default_async_client") as mock_make: - # Replicate the generated conditional: httpx_client if httpx_client is not None else _make_default_async_client(...) - result = explicit_client if explicit_client is not None else mock_make(timeout=60, follow_redirects=True) - mock_make.assert_not_called() - self.assertIs(result, explicit_client) - - -@pytest.mark.aiohttp -class TestMakeDefaultAsyncClientWithAiohttp(unittest.TestCase): - """Tests for _make_default_async_client when httpx_aiohttp IS installed.""" - - def test_returns_aiohttp_client(self) -> None: - """When httpx_aiohttp is installed, returns HttpxAiohttpClient.""" - import httpx_aiohttp # type: ignore[import-not-found] - - from vitable_connect.client import _make_default_async_client - - client = _make_default_async_client(timeout=60, follow_redirects=True) - self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) - self.assertEqual(client.timeout.read, 60) - self.assertTrue(client.follow_redirects) - - def test_follow_redirects_none(self) -> None: - """When httpx_aiohttp is installed and follow_redirects is None, omits it.""" - import httpx_aiohttp # type: ignore[import-not-found] - - from vitable_connect.client import _make_default_async_client - - client = _make_default_async_client(timeout=60, follow_redirects=None) - self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) - self.assertFalse(client.follow_redirects) - - -class TestDefaultClientsWithoutAiohttp(unittest.TestCase): - """Tests for _default_clients.py convenience classes (no aiohttp).""" - - def test_default_async_httpx_client_defaults(self) -> None: - """DefaultAsyncHttpxClient applies SDK defaults.""" - from vitable_connect._default_clients import SDK_DEFAULT_TIMEOUT, DefaultAsyncHttpxClient - - client = DefaultAsyncHttpxClient() - self.assertIsInstance(client, httpx.AsyncClient) - self.assertEqual(client.timeout.read, SDK_DEFAULT_TIMEOUT) - self.assertTrue(client.follow_redirects) - - def test_default_async_httpx_client_overrides(self) -> None: - """DefaultAsyncHttpxClient allows overriding defaults.""" - from vitable_connect._default_clients import DefaultAsyncHttpxClient - - client = DefaultAsyncHttpxClient(timeout=30, follow_redirects=False) - self.assertEqual(client.timeout.read, 30) - self.assertFalse(client.follow_redirects) - - def test_default_aiohttp_client_raises_without_package(self) -> None: - """DefaultAioHttpClient raises RuntimeError when httpx_aiohttp not installed.""" - import vitable_connect._default_clients - - with mock.patch.dict(sys.modules, {"httpx_aiohttp": None}): - importlib.reload(vitable_connect._default_clients) - - with self.assertRaises(RuntimeError) as ctx: - vitable_connect._default_clients.DefaultAioHttpClient() - self.assertIn("pip install vitable-connect[aiohttp]", str(ctx.exception)) - - importlib.reload(vitable_connect._default_clients) - - -@pytest.mark.aiohttp -class TestDefaultClientsWithAiohttp(unittest.TestCase): - """Tests for _default_clients.py when httpx_aiohttp IS installed.""" - - def test_default_aiohttp_client_defaults(self) -> None: - """DefaultAioHttpClient works when httpx_aiohttp is installed.""" - import httpx_aiohttp # type: ignore[import-not-found] - - from vitable_connect._default_clients import SDK_DEFAULT_TIMEOUT, DefaultAioHttpClient - - client = DefaultAioHttpClient() - self.assertIsInstance(client, httpx_aiohttp.HttpxAiohttpClient) - self.assertEqual(client.timeout.read, SDK_DEFAULT_TIMEOUT) - self.assertTrue(client.follow_redirects) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..2d04b70 --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,2018 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +import gc +import os +import sys +import json +import asyncio +import inspect +import dataclasses +import tracemalloc +from typing import Any, Union, TypeVar, Callable, Iterable, Iterator, Optional, Coroutine, cast +from unittest import mock +from typing_extensions import Literal, AsyncIterator, override + +import httpx +import pytest +from respx import MockRouter +from pydantic import ValidationError + +from vitable_connect import VitableConnect, AsyncVitableConnect, APIResponseValidationError +from vitable_connect._types import Omit +from vitable_connect._utils import asyncify +from vitable_connect._models import BaseModel, FinalRequestOptions +from vitable_connect._exceptions import APIStatusError, APITimeoutError, VitableConnectError, APIResponseValidationError +from vitable_connect._base_client import ( + DEFAULT_TIMEOUT, + HTTPX_DEFAULT_TIMEOUT, + BaseClient, + OtherPlatform, + DefaultHttpxClient, + DefaultAsyncHttpxClient, + get_platform, + make_request_options, +) + +from .utils import update_env + +T = TypeVar("T") +base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") +api_key = "My API Key" + + +def _get_params(client: BaseClient[Any, Any]) -> dict[str, str]: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + return dict(url.params) + + +def _low_retry_timeout(*_args: Any, **_kwargs: Any) -> float: + return 0.1 + + +def mirror_request_content(request: httpx.Request) -> httpx.Response: + return httpx.Response(200, content=request.content) + + +# note: we can't use the httpx.MockTransport class as it consumes the request +# body itself, which means we can't test that the body is read lazily +class MockTransport(httpx.BaseTransport, httpx.AsyncBaseTransport): + def __init__( + self, + handler: Callable[[httpx.Request], httpx.Response] + | Callable[[httpx.Request], Coroutine[Any, Any, httpx.Response]], + ) -> None: + self.handler = handler + + @override + def handle_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert not inspect.iscoroutinefunction(self.handler), "handler must not be a coroutine function" + assert inspect.isfunction(self.handler), "handler must be a function" + return self.handler(request) + + @override + async def handle_async_request( + self, + request: httpx.Request, + ) -> httpx.Response: + assert inspect.iscoroutinefunction(self.handler), "handler must be a coroutine function" + return await self.handler(request) + + +@dataclasses.dataclass +class Counter: + value: int = 0 + + +def _make_sync_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> Iterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +async def _make_async_iterator(iterable: Iterable[T], counter: Optional[Counter] = None) -> AsyncIterator[T]: + for item in iterable: + if counter: + counter.value += 1 + yield item + + +def _get_open_connections(client: VitableConnect | AsyncVitableConnect) -> int: + transport = client._client._transport + assert isinstance(transport, httpx.HTTPTransport) or isinstance(transport, httpx.AsyncHTTPTransport) + + pool = transport._pool + return len(pool._requests) + + +class TestVitableConnect: + @pytest.mark.respx(base_url=base_url) + def test_raw_response(self, respx_mock: MockRouter, client: VitableConnect) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + def test_raw_response_for_binary(self, respx_mock: MockRouter, client: VitableConnect) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self, client: VitableConnect) -> None: + copied = client.copy() + assert id(copied) != id(client) + + copied = client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert client.api_key == "My API Key" + + def test_copy_default_options(self, client: VitableConnect) -> None: + # options that have a default are overridden correctly + copied = client.copy(max_retries=7) + assert copied.max_retries == 7 + assert client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(client.timeout, httpx.Timeout) + copied = client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(client.timeout, httpx.Timeout) + + def test_copy_default_headers(self) -> None: + client = VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + client.close() + + def test_copy_default_query(self) -> None: + client = VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + client.close() + + def test_copy_signature(self, client: VitableConnect) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") + def test_copy_build_request(self, client: VitableConnect) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client_copy = client.copy() + client_copy._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "vitable_connect/_legacy_response.py", + "vitable_connect/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "vitable_connect/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + def test_request_timeout(self, client: VitableConnect) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = client._build_request(FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0))) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + def test_client_timeout_option(self) -> None: + client = VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + client.close() + + def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + with httpx.Client(timeout=None) as http_client: + client = VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + client.close() + + # no timeout given to the httpx client should not use the httpx default + with httpx.Client() as http_client: + client = VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + client.close() + + # explicitly passing the default timeout currently results in it being ignored + with httpx.Client(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + client.close() + + async def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + async with httpx.AsyncClient() as http_client: + VitableConnect( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + def test_default_headers_option(self) -> None: + test_client = VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + test_client2 = VitableConnect( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + test_client.close() + test_client2.close() + + def test_validate_headers(self) -> None: + client = VitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with pytest.raises(VitableConnectError): + with update_env(**{"VITABLE_CONNECT_API_KEY": Omit()}): + client2 = VitableConnect(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + def test_default_query_option(self) -> None: + client = VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overridden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} + + client.close() + + def test_hardcoded_query_params_in_url(self, client: VitableConnect) -> None: + request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + + def test_request_extra_json(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions.construct( + method="post", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload(self, respx_mock: MockRouter, client: VitableConnect) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + def test_binary_content_upload_with_iterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_sync_iterator([file_content], counter=counter) + + def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=request.read()) + + with VitableConnect( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(transport=MockTransport(handler=mock_handler)), + ) as client: + response = client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, client: VitableConnect + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + @pytest.mark.respx(base_url=base_url) + def test_basic_union_response(self, respx_mock: MockRouter, client: VitableConnect) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + def test_union_response_different_types(self, respx_mock: MockRouter, client: VitableConnect) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, client: VitableConnect + ) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + def test_base_url_setter(self) -> None: + client = VitableConnect( + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True + ) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + client.close() + + def test_base_url_env(self) -> None: + with update_env(VITABLE_CONNECT_BASE_URL="http://localhost:5000/from/env"): + client = VitableConnect(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + # explicit environment arg requires explicitness + with update_env(VITABLE_CONNECT_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + VitableConnect(api_key=api_key, _strict_response_validation=True, environment="production") + + client = VitableConnect( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://api.vitablehealth.com") + + client.close() + + @pytest.mark.parametrize( + "client", + [ + VitableConnect( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + VitableConnect( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_trailing_slash(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + client.close() + + @pytest.mark.parametrize( + "client", + [ + VitableConnect( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + VitableConnect( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_base_url_no_trailing_slash(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + client.close() + + @pytest.mark.parametrize( + "client", + [ + VitableConnect( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + VitableConnect( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.Client(), + ), + ], + ids=["standard", "custom http client"], + ) + def test_absolute_request_url(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + client.close() + + def test_copied_client_does_not_close_http(self) -> None: + test_client = VitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() + + copied = test_client.copy() + assert copied is not test_client + + del copied + + assert not test_client.is_closed() + + def test_client_context_manager(self) -> None: + test_client = VitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=True) + with test_client as c2: + assert c2 is test_client + assert not c2.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() + + @pytest.mark.respx(base_url=base_url) + def test_client_response_validation_error(self, respx_mock: MockRouter, client: VitableConnect) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + VitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + ) + + @pytest.mark.respx(base_url=base_url) + def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = VitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + strict_client.get("/foo", cast_to=Model) + + non_strict_client = VitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = non_strict_client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + strict_client.close() + non_strict_client.close() + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + [-1100, "", 8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, client: VitableConnect + ) -> None: + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_timeout_errors_doesnt_leak(self, respx_mock: MockRouter, client: VitableConnect) -> None: + respx_mock.post("/v1/auth/access-tokens").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + client.auth.with_streaming_response.issue_access_token(grant_type="client_credentials").__enter__() + + assert _get_open_connections(client) == 0 + + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_retrying_status_errors_doesnt_leak(self, respx_mock: MockRouter, client: VitableConnect) -> None: + respx_mock.post("/v1/auth/access-tokens").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + client.auth.with_streaming_response.issue_access_token(grant_type="client_credentials").__enter__() + assert _get_open_connections(client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + def test_retries_taken( + self, + client: VitableConnect, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/auth/access-tokens").mock(side_effect=retry_handler) + + response = client.auth.with_raw_response.issue_access_token(grant_type="client_credentials") + + assert response.retries_taken == failures_before_success + assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_omit_retry_count_header( + self, client: VitableConnect, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/auth/access-tokens").mock(side_effect=retry_handler) + + response = client.auth.with_raw_response.issue_access_token( + grant_type="client_credentials", extra_headers={"x-stainless-retry-count": Omit()} + ) + + assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + def test_overwrite_retry_count_header( + self, client: VitableConnect, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/auth/access-tokens").mock(side_effect=retry_handler) + + response = client.auth.with_raw_response.issue_access_token( + grant_type="client_credentials", extra_headers={"x-stainless-retry-count": "42"} + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has any proxy env vars set + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) + + client = DefaultHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects(self, respx_mock: MockRouter, client: VitableConnect) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + def test_follow_redirects_disabled(self, respx_mock: MockRouter, client: VitableConnect) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + client.post("/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" + + +class TestAsyncVitableConnect: + @pytest.mark.respx(base_url=base_url) + async def test_raw_response(self, respx_mock: MockRouter, async_client: AsyncVitableConnect) -> None: + respx_mock.post("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + @pytest.mark.respx(base_url=base_url) + async def test_raw_response_for_binary(self, respx_mock: MockRouter, async_client: AsyncVitableConnect) -> None: + respx_mock.post("/foo").mock( + return_value=httpx.Response(200, headers={"Content-Type": "application/binary"}, content='{"foo": "bar"}') + ) + + response = await async_client.post("/foo", cast_to=httpx.Response) + assert response.status_code == 200 + assert isinstance(response, httpx.Response) + assert response.json() == {"foo": "bar"} + + def test_copy(self, async_client: AsyncVitableConnect) -> None: + copied = async_client.copy() + assert id(copied) != id(async_client) + + copied = async_client.copy(api_key="another My API Key") + assert copied.api_key == "another My API Key" + assert async_client.api_key == "My API Key" + + def test_copy_default_options(self, async_client: AsyncVitableConnect) -> None: + # options that have a default are overridden correctly + copied = async_client.copy(max_retries=7) + assert copied.max_retries == 7 + assert async_client.max_retries == 2 + + copied2 = copied.copy(max_retries=6) + assert copied2.max_retries == 6 + assert copied.max_retries == 7 + + # timeout + assert isinstance(async_client.timeout, httpx.Timeout) + copied = async_client.copy(timeout=None) + assert copied.timeout is None + assert isinstance(async_client.timeout, httpx.Timeout) + + async def test_copy_default_headers(self) -> None: + client = AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + assert client.default_headers["X-Foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert copied.default_headers["X-Foo"] == "bar" + + # merges already given headers + copied = client.copy(default_headers={"X-Bar": "stainless"}) + assert copied.default_headers["X-Foo"] == "bar" + assert copied.default_headers["X-Bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_headers={"X-Foo": "stainless"}) + assert copied.default_headers["X-Foo"] == "stainless" + + # set_default_headers + + # completely overrides already set values + copied = client.copy(set_default_headers={}) + assert copied.default_headers.get("X-Foo") is None + + copied = client.copy(set_default_headers={"X-Bar": "Robert"}) + assert copied.default_headers["X-Bar"] == "Robert" + + with pytest.raises( + ValueError, + match="`default_headers` and `set_default_headers` arguments are mutually exclusive", + ): + client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + await client.close() + + async def test_copy_default_query(self) -> None: + client = AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"foo": "bar"} + ) + assert _get_params(client)["foo"] == "bar" + + # does not override the already given value when not specified + copied = client.copy() + assert _get_params(copied)["foo"] == "bar" + + # merges already given params + copied = client.copy(default_query={"bar": "stainless"}) + params = _get_params(copied) + assert params["foo"] == "bar" + assert params["bar"] == "stainless" + + # uses new values for any already given headers + copied = client.copy(default_query={"foo": "stainless"}) + assert _get_params(copied)["foo"] == "stainless" + + # set_default_query + + # completely overrides already set values + copied = client.copy(set_default_query={}) + assert _get_params(copied) == {} + + copied = client.copy(set_default_query={"bar": "Robert"}) + assert _get_params(copied)["bar"] == "Robert" + + with pytest.raises( + ValueError, + # TODO: update + match="`default_query` and `set_default_query` arguments are mutually exclusive", + ): + client.copy(set_default_query={}, default_query={"foo": "Bar"}) + + await client.close() + + def test_copy_signature(self, async_client: AsyncVitableConnect) -> None: + # ensure the same parameters that can be passed to the client are defined in the `.copy()` method + init_signature = inspect.signature( + # mypy doesn't like that we access the `__init__` property. + async_client.__init__, # type: ignore[misc] + ) + copy_signature = inspect.signature(async_client.copy) + exclude_params = {"transport", "proxies", "_strict_response_validation"} + + for name in init_signature.parameters.keys(): + if name in exclude_params: + continue + + copy_param = copy_signature.parameters.get(name) + assert copy_param is not None, f"copy() signature is missing the {name} param" + + @pytest.mark.skipif(sys.version_info >= (3, 10), reason="fails because of a memory leak that started from 3.12") + def test_copy_build_request(self, async_client: AsyncVitableConnect) -> None: + options = FinalRequestOptions(method="get", url="/foo") + + def build_request(options: FinalRequestOptions) -> None: + client_copy = async_client.copy() + client_copy._build_request(options) + + # ensure that the machinery is warmed up before tracing starts. + build_request(options) + gc.collect() + + tracemalloc.start(1000) + + snapshot_before = tracemalloc.take_snapshot() + + ITERATIONS = 10 + for _ in range(ITERATIONS): + build_request(options) + + gc.collect() + snapshot_after = tracemalloc.take_snapshot() + + tracemalloc.stop() + + def add_leak(leaks: list[tracemalloc.StatisticDiff], diff: tracemalloc.StatisticDiff) -> None: + if diff.count == 0: + # Avoid false positives by considering only leaks (i.e. allocations that persist). + return + + if diff.count % ITERATIONS != 0: + # Avoid false positives by considering only leaks that appear per iteration. + return + + for frame in diff.traceback: + if any( + frame.filename.endswith(fragment) + for fragment in [ + # to_raw_response_wrapper leaks through the @functools.wraps() decorator. + # + # removing the decorator fixes the leak for reasons we don't understand. + "vitable_connect/_legacy_response.py", + "vitable_connect/_response.py", + # pydantic.BaseModel.model_dump || pydantic.BaseModel.dict leak memory for some reason. + "vitable_connect/_compat.py", + # Standard library leaks we don't care about. + "/logging/__init__.py", + ] + ): + return + + leaks.append(diff) + + leaks: list[tracemalloc.StatisticDiff] = [] + for diff in snapshot_after.compare_to(snapshot_before, "traceback"): + add_leak(leaks, diff) + if leaks: + for leak in leaks: + print("MEMORY LEAK:", leak) + for frame in leak.traceback: + print(frame) + raise AssertionError() + + async def test_request_timeout(self, async_client: AsyncVitableConnect) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + request = async_client._build_request( + FinalRequestOptions(method="get", url="/foo", timeout=httpx.Timeout(100.0)) + ) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(100.0) + + async def test_client_timeout_option(self) -> None: + client = AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, timeout=httpx.Timeout(0) + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(0) + + await client.close() + + async def test_http_client_timeout_option(self) -> None: + # custom timeout given to the httpx client should be used + async with httpx.AsyncClient(timeout=None) as http_client: + client = AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == httpx.Timeout(None) + + await client.close() + + # no timeout given to the httpx client should not use the httpx default + async with httpx.AsyncClient() as http_client: + client = AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT + + await client.close() + + # explicitly passing the default timeout currently results in it being ignored + async with httpx.AsyncClient(timeout=HTTPX_DEFAULT_TIMEOUT) as http_client: + client = AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, http_client=http_client + ) + + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + timeout = httpx.Timeout(**request.extensions["timeout"]) # type: ignore + assert timeout == DEFAULT_TIMEOUT # our default + + await client.close() + + def test_invalid_http_client(self) -> None: + with pytest.raises(TypeError, match="Invalid `http_client` arg"): + with httpx.Client() as http_client: + AsyncVitableConnect( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=cast(Any, http_client), + ) + + async def test_default_headers_option(self) -> None: + test_client = AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_headers={"X-Foo": "bar"} + ) + request = test_client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "bar" + assert request.headers.get("x-stainless-lang") == "python" + + test_client2 = AsyncVitableConnect( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + default_headers={ + "X-Foo": "stainless", + "X-Stainless-Lang": "my-overriding-header", + }, + ) + request = test_client2._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("x-foo") == "stainless" + assert request.headers.get("x-stainless-lang") == "my-overriding-header" + + await test_client.close() + await test_client2.close() + + def test_validate_headers(self) -> None: + client = AsyncVitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=True) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + assert request.headers.get("Authorization") == f"Bearer {api_key}" + + with pytest.raises(VitableConnectError): + with update_env(**{"VITABLE_CONNECT_API_KEY": Omit()}): + client2 = AsyncVitableConnect(base_url=base_url, api_key=None, _strict_response_validation=True) + _ = client2 + + async def test_default_query_option(self) -> None: + client = AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} + ) + request = client._build_request(FinalRequestOptions(method="get", url="/foo")) + url = httpx.URL(request.url) + assert dict(url.params) == {"query_param": "bar"} + + request = client._build_request( + FinalRequestOptions( + method="get", + url="/foo", + params={"foo": "baz", "query_param": "overridden"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"foo": "baz", "query_param": "overridden"} + + await client.close() + + async def test_hardcoded_query_params_in_url(self, async_client: AsyncVitableConnect) -> None: + request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true")) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/foo?beta=true", + params={"limit": "10", "page": "abc"}, + ) + ) + url = httpx.URL(request.url) + assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"} + + request = async_client._build_request( + FinalRequestOptions( + method="get", + url="/files/a%2Fb?beta=true", + params={"limit": "10"}, + ) + ) + assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10" + + def test_request_extra_json(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": False} + + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + extra_json={"baz": False}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"baz": False} + + # `extra_json` takes priority over `json_data` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar", "baz": True}, + extra_json={"baz": None}, + ), + ) + data = json.loads(request.content.decode("utf-8")) + assert data == {"foo": "bar", "baz": None} + + def test_request_extra_headers(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"X-Foo": "Foo"}), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + + # `extra_headers` takes priority over `default_headers` when keys clash + request = client.with_options(default_headers={"X-Bar": "true"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_headers={"X-Bar": "false"}, + ), + ), + ) + assert request.headers.get("X-Bar") == "false" + + def test_request_extra_query(self, client: VitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + extra_query={"my_query_param": "Foo"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"my_query_param": "Foo"} + + # if both `query` and `extra_query` are given, they are merged + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"bar": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"bar": "1", "foo": "2"} + + # `extra_query` takes priority over `query` when keys clash + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options( + query={"foo": "1"}, + extra_query={"foo": "2"}, + ), + ), + ) + params = dict(request.url.params) + assert params == {"foo": "2"} + + def test_multipart_repeating_array(self, async_client: AsyncVitableConnect) -> None: + request = async_client._build_request( + FinalRequestOptions.construct( + method="post", + url="/foo", + headers={"Content-Type": "multipart/form-data; boundary=6b7ba517decee4a450543ea6ae821c82"}, + json_data={"array": ["foo", "bar"]}, + files=[("foo.txt", b"hello world")], + ) + ) + + assert request.read().split(b"\r\n") == [ + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"foo", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="array[]"', + b"", + b"bar", + b"--6b7ba517decee4a450543ea6ae821c82", + b'Content-Disposition: form-data; name="foo.txt"; filename="upload"', + b"Content-Type: application/octet-stream", + b"", + b"hello world", + b"--6b7ba517decee4a450543ea6ae821c82--", + b"", + ] + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload(self, respx_mock: MockRouter, async_client: AsyncVitableConnect) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + response = await async_client.post( + "/upload", + content=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + async def test_binary_content_upload_with_asynciterator(self) -> None: + file_content = b"Hello, this is a test file." + counter = Counter() + iterator = _make_async_iterator([file_content], counter=counter) + + async def mock_handler(request: httpx.Request) -> httpx.Response: + assert counter.value == 0, "the request body should not have been read" + return httpx.Response(200, content=await request.aread()) + + async with AsyncVitableConnect( + base_url=base_url, + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(transport=MockTransport(handler=mock_handler)), + ) as client: + response = await client.post( + "/upload", + content=iterator, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + assert counter.value == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_binary_content_upload_with_body_is_deprecated( + self, respx_mock: MockRouter, async_client: AsyncVitableConnect + ) -> None: + respx_mock.post("/upload").mock(side_effect=mirror_request_content) + + file_content = b"Hello, this is a test file." + + with pytest.deprecated_call( + match="Passing raw bytes as `body` is deprecated and will be removed in a future version. Please pass raw bytes via the `content` parameter instead." + ): + response = await async_client.post( + "/upload", + body=file_content, + cast_to=httpx.Response, + options={"headers": {"Content-Type": "application/octet-stream"}}, + ) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == "application/octet-stream" + assert response.content == file_content + + @pytest.mark.respx(base_url=base_url) + async def test_basic_union_response(self, respx_mock: MockRouter, async_client: AsyncVitableConnect) -> None: + class Model1(BaseModel): + name: str + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + @pytest.mark.respx(base_url=base_url) + async def test_union_response_different_types( + self, respx_mock: MockRouter, async_client: AsyncVitableConnect + ) -> None: + """Union of objects with the same field name using a different type""" + + class Model1(BaseModel): + foo: int + + class Model2(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": "bar"})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model2) + assert response.foo == "bar" + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": 1})) + + response = await async_client.get("/foo", cast_to=cast(Any, Union[Model1, Model2])) + assert isinstance(response, Model1) + assert response.foo == 1 + + @pytest.mark.respx(base_url=base_url) + async def test_non_application_json_content_type_for_json_data( + self, respx_mock: MockRouter, async_client: AsyncVitableConnect + ) -> None: + """ + Response that sets Content-Type to something other than application/json but returns json data + """ + + class Model(BaseModel): + foo: int + + respx_mock.get("/foo").mock( + return_value=httpx.Response( + 200, + content=json.dumps({"foo": 2}), + headers={"Content-Type": "application/text"}, + ) + ) + + response = await async_client.get("/foo", cast_to=Model) + assert isinstance(response, Model) + assert response.foo == 2 + + async def test_base_url_setter(self) -> None: + client = AsyncVitableConnect( + base_url="https://example.com/from_init", api_key=api_key, _strict_response_validation=True + ) + assert client.base_url == "https://example.com/from_init/" + + client.base_url = "https://example.com/from_setter" # type: ignore[assignment] + + assert client.base_url == "https://example.com/from_setter/" + + await client.close() + + async def test_base_url_env(self) -> None: + with update_env(VITABLE_CONNECT_BASE_URL="http://localhost:5000/from/env"): + client = AsyncVitableConnect(api_key=api_key, _strict_response_validation=True) + assert client.base_url == "http://localhost:5000/from/env/" + + # explicit environment arg requires explicitness + with update_env(VITABLE_CONNECT_BASE_URL="http://localhost:5000/from/env"): + with pytest.raises(ValueError, match=r"you must pass base_url=None"): + AsyncVitableConnect(api_key=api_key, _strict_response_validation=True, environment="production") + + client = AsyncVitableConnect( + base_url=None, api_key=api_key, _strict_response_validation=True, environment="production" + ) + assert str(client.base_url).startswith("https://api.vitablehealth.com") + + await client.close() + + @pytest.mark.parametrize( + "client", + [ + AsyncVitableConnect( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncVitableConnect( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_base_url_trailing_slash(self, client: AsyncVitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() + + @pytest.mark.parametrize( + "client", + [ + AsyncVitableConnect( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncVitableConnect( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_base_url_no_trailing_slash(self, client: AsyncVitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "http://localhost:5000/custom/path/foo" + await client.close() + + @pytest.mark.parametrize( + "client", + [ + AsyncVitableConnect( + base_url="http://localhost:5000/custom/path/", api_key=api_key, _strict_response_validation=True + ), + AsyncVitableConnect( + base_url="http://localhost:5000/custom/path/", + api_key=api_key, + _strict_response_validation=True, + http_client=httpx.AsyncClient(), + ), + ], + ids=["standard", "custom http client"], + ) + async def test_absolute_request_url(self, client: AsyncVitableConnect) -> None: + request = client._build_request( + FinalRequestOptions( + method="post", + url="https://myapi.com/foo", + json_data={"foo": "bar"}, + ), + ) + assert request.url == "https://myapi.com/foo" + await client.close() + + async def test_copied_client_does_not_close_http(self) -> None: + test_client = AsyncVitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=True) + assert not test_client.is_closed() + + copied = test_client.copy() + assert copied is not test_client + + del copied + + await asyncio.sleep(0.2) + assert not test_client.is_closed() + + async def test_client_context_manager(self) -> None: + test_client = AsyncVitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=True) + async with test_client as c2: + assert c2 is test_client + assert not c2.is_closed() + assert not test_client.is_closed() + assert test_client.is_closed() + + @pytest.mark.respx(base_url=base_url) + async def test_client_response_validation_error( + self, respx_mock: MockRouter, async_client: AsyncVitableConnect + ) -> None: + class Model(BaseModel): + foo: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, json={"foo": {"invalid": True}})) + + with pytest.raises(APIResponseValidationError) as exc: + await async_client.get("/foo", cast_to=Model) + + assert isinstance(exc.value.__cause__, ValidationError) + + async def test_client_max_retries_validation(self) -> None: + with pytest.raises(TypeError, match=r"max_retries cannot be None"): + AsyncVitableConnect( + base_url=base_url, api_key=api_key, _strict_response_validation=True, max_retries=cast(Any, None) + ) + + @pytest.mark.respx(base_url=base_url) + async def test_received_text_for_expected_json(self, respx_mock: MockRouter) -> None: + class Model(BaseModel): + name: str + + respx_mock.get("/foo").mock(return_value=httpx.Response(200, text="my-custom-format")) + + strict_client = AsyncVitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=True) + + with pytest.raises(APIResponseValidationError): + await strict_client.get("/foo", cast_to=Model) + + non_strict_client = AsyncVitableConnect(base_url=base_url, api_key=api_key, _strict_response_validation=False) + + response = await non_strict_client.get("/foo", cast_to=Model) + assert isinstance(response, str) # type: ignore[unreachable] + + await strict_client.close() + await non_strict_client.close() + + @pytest.mark.parametrize( + "remaining_retries,retry_after,timeout", + [ + [3, "20", 20], + [3, "0", 0.5], + [3, "-10", 0.5], + [3, "60", 60], + [3, "61", 0.5], + [3, "Fri, 29 Sep 2023 16:26:57 GMT", 20], + [3, "Fri, 29 Sep 2023 16:26:37 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "Fri, 29 Sep 2023 16:27:37 GMT", 60], + [3, "Fri, 29 Sep 2023 16:27:38 GMT", 0.5], + [3, "99999999999999999999999999999999999", 0.5], + [3, "Zun, 29 Sep 2023 16:26:27 GMT", 0.5], + [3, "", 0.5], + [2, "", 0.5 * 2.0], + [1, "", 0.5 * 4.0], + [-1100, "", 8], # test large number potentially overflowing + ], + ) + @mock.patch("time.time", mock.MagicMock(return_value=1696004797)) + async def test_parse_retry_after_header( + self, remaining_retries: int, retry_after: str, timeout: float, async_client: AsyncVitableConnect + ) -> None: + headers = httpx.Headers({"retry-after": retry_after}) + options = FinalRequestOptions(method="get", url="/foo", max_retries=3) + calculated = async_client._calculate_retry_timeout(remaining_retries, options, headers) + assert calculated == pytest.approx(timeout, 0.5 * 0.875) # pyright: ignore[reportUnknownMemberType] + + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_timeout_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncVitableConnect + ) -> None: + respx_mock.post("/v1/auth/access-tokens").mock(side_effect=httpx.TimeoutException("Test timeout error")) + + with pytest.raises(APITimeoutError): + await async_client.auth.with_streaming_response.issue_access_token( + grant_type="client_credentials" + ).__aenter__() + + assert _get_open_connections(async_client) == 0 + + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_retrying_status_errors_doesnt_leak( + self, respx_mock: MockRouter, async_client: AsyncVitableConnect + ) -> None: + respx_mock.post("/v1/auth/access-tokens").mock(return_value=httpx.Response(500)) + + with pytest.raises(APIStatusError): + await async_client.auth.with_streaming_response.issue_access_token( + grant_type="client_credentials" + ).__aenter__() + assert _get_open_connections(async_client) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + @pytest.mark.parametrize("failure_mode", ["status", "exception"]) + async def test_retries_taken( + self, + async_client: AsyncVitableConnect, + failures_before_success: int, + failure_mode: Literal["status", "exception"], + respx_mock: MockRouter, + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + if failure_mode == "exception": + raise RuntimeError("oops") + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/auth/access-tokens").mock(side_effect=retry_handler) + + response = await client.auth.with_raw_response.issue_access_token(grant_type="client_credentials") + + assert response.retries_taken == failures_before_success + assert int(response.http_request.headers.get("x-stainless-retry-count")) == failures_before_success + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_omit_retry_count_header( + self, async_client: AsyncVitableConnect, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/auth/access-tokens").mock(side_effect=retry_handler) + + response = await client.auth.with_raw_response.issue_access_token( + grant_type="client_credentials", extra_headers={"x-stainless-retry-count": Omit()} + ) + + assert len(response.http_request.headers.get_list("x-stainless-retry-count")) == 0 + + @pytest.mark.parametrize("failures_before_success", [0, 2, 4]) + @mock.patch("vitable_connect._base_client.BaseClient._calculate_retry_timeout", _low_retry_timeout) + @pytest.mark.respx(base_url=base_url) + async def test_overwrite_retry_count_header( + self, async_client: AsyncVitableConnect, failures_before_success: int, respx_mock: MockRouter + ) -> None: + client = async_client.with_options(max_retries=4) + + nb_retries = 0 + + def retry_handler(_request: httpx.Request) -> httpx.Response: + nonlocal nb_retries + if nb_retries < failures_before_success: + nb_retries += 1 + return httpx.Response(500) + return httpx.Response(200) + + respx_mock.post("/v1/auth/access-tokens").mock(side_effect=retry_handler) + + response = await client.auth.with_raw_response.issue_access_token( + grant_type="client_credentials", extra_headers={"x-stainless-retry-count": "42"} + ) + + assert response.http_request.headers.get("x-stainless-retry-count") == "42" + + async def test_get_platform(self) -> None: + platform = await asyncify(get_platform)() + assert isinstance(platform, (str, OtherPlatform)) + + async def test_proxy_environment_variables(self, monkeypatch: pytest.MonkeyPatch) -> None: + # Test that the proxy environment variables are set correctly + monkeypatch.setenv("HTTPS_PROXY", "https://example.org") + # Delete in case our environment has any proxy env vars set + monkeypatch.delenv("HTTP_PROXY", raising=False) + monkeypatch.delenv("ALL_PROXY", raising=False) + monkeypatch.delenv("NO_PROXY", raising=False) + monkeypatch.delenv("http_proxy", raising=False) + monkeypatch.delenv("https_proxy", raising=False) + monkeypatch.delenv("all_proxy", raising=False) + monkeypatch.delenv("no_proxy", raising=False) + + client = DefaultAsyncHttpxClient() + + mounts = tuple(client._mounts.items()) + assert len(mounts) == 1 + assert mounts[0][0].pattern == "https://" + + @pytest.mark.filterwarnings("ignore:.*deprecated.*:DeprecationWarning") + async def test_default_client_creation(self) -> None: + # Ensure that the client can be initialized without any exceptions + DefaultAsyncHttpxClient( + verify=True, + cert=None, + trust_env=True, + http1=True, + http2=False, + limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), + ) + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects(self, respx_mock: MockRouter, async_client: AsyncVitableConnect) -> None: + # Test that the default follow_redirects=True allows following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + respx_mock.get("/redirected").mock(return_value=httpx.Response(200, json={"status": "ok"})) + + response = await async_client.post("/redirect", body={"key": "value"}, cast_to=httpx.Response) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + @pytest.mark.respx(base_url=base_url) + async def test_follow_redirects_disabled(self, respx_mock: MockRouter, async_client: AsyncVitableConnect) -> None: + # Test that follow_redirects=False prevents following redirects + respx_mock.post("/redirect").mock( + return_value=httpx.Response(302, headers={"Location": f"{base_url}/redirected"}) + ) + + with pytest.raises(APIStatusError) as exc_info: + await async_client.post( + "/redirect", body={"key": "value"}, options={"follow_redirects": False}, cast_to=httpx.Response + ) + + assert exc_info.value.response.status_code == 302 + assert exc_info.value.response.headers["Location"] == f"{base_url}/redirected" diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py new file mode 100644 index 0000000..5f894ca --- /dev/null +++ b/tests/test_extract_files.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +from typing import Sequence + +import pytest + +from vitable_connect._types import FileTypes, ArrayFormat +from vitable_connect._utils import extract_files + + +def test_removes_files_from_input() -> None: + query = {"foo": "bar"} + assert extract_files(query, paths=[]) == [] + assert query == {"foo": "bar"} + + query2 = {"foo": b"Bar", "hello": "world"} + assert extract_files(query2, paths=[["foo"]]) == [("foo", b"Bar")] + assert query2 == {"hello": "world"} + + query3 = {"foo": {"foo": {"bar": b"Bar"}}, "hello": "world"} + assert extract_files(query3, paths=[["foo", "foo", "bar"]]) == [("foo[foo][bar]", b"Bar")] + assert query3 == {"foo": {"foo": {}}, "hello": "world"} + + query4 = {"foo": {"bar": b"Bar", "baz": "foo"}, "hello": "world"} + assert extract_files(query4, paths=[["foo", "bar"]]) == [("foo[bar]", b"Bar")] + assert query4 == {"hello": "world", "foo": {"baz": "foo"}} + + +def test_multiple_files() -> None: + query = {"documents": [{"file": b"My first file"}, {"file": b"My second file"}]} + assert extract_files(query, paths=[["documents", "", "file"]]) == [ + ("documents[][file]", b"My first file"), + ("documents[][file]", b"My second file"), + ] + assert query == {"documents": [{}, {}]} + + +def test_top_level_file_array() -> None: + query = {"files": [b"file one", b"file two"], "title": "hello"} + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] + assert query == {"title": "hello"} + + +@pytest.mark.parametrize( + "query,paths,expected", + [ + [ + {"foo": {"bar": "baz"}}, + [["foo", "", "bar"]], + [], + ], + [ + {"foo": ["bar", "baz"]}, + [["foo", "bar"]], + [], + ], + [ + {"foo": {"bar": "baz"}}, + [["foo", "foo"]], + [], + ], + ], + ids=["dict expecting array", "array expecting dict", "unknown keys"], +) +def test_ignores_incorrect_paths( + query: dict[str, object], + paths: Sequence[Sequence[str]], + expected: list[tuple[str, FileTypes]], +) -> None: + assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 0000000..c1810dc --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,148 @@ +from pathlib import Path + +import anyio +import pytest +from dirty_equals import IsDict, IsList, IsBytes, IsTuple + +from vitable_connect._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files +from vitable_connect._utils import extract_files + +readme_path = Path(__file__).parent.parent.joinpath("README.md") + + +def test_pathlib_includes_file_name() -> None: + result = to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +def test_tuple_input() -> None: + result = to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +@pytest.mark.asyncio +async def test_async_pathlib_includes_file_name() -> None: + result = await async_to_httpx_files({"file": readme_path}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_supports_anyio_path() -> None: + result = await async_to_httpx_files({"file": anyio.Path(readme_path)}) + print(result) + assert result == IsDict({"file": IsTuple("README.md", IsBytes())}) + + +@pytest.mark.asyncio +async def test_async_tuple_input() -> None: + result = await async_to_httpx_files([("file", readme_path)]) + print(result) + assert result == IsList(IsTuple("file", IsTuple("README.md", IsBytes()))) + + +def test_string_not_allowed() -> None: + with pytest.raises(TypeError, match="Expected file types input to be a FileContent type or to be a tuple"): + to_httpx_files( + { + "file": "foo", # type: ignore + } + ) + + +def assert_different_identities(obj1: object, obj2: object) -> None: + assert obj1 == obj2 + assert obj1 is not obj2 + + +class TestDeepcopyWithPaths: + def test_copies_top_level_dict(self) -> None: + original = {"file": b"data", "other": "value"} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + + def test_file_value_is_same_reference(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes} + result = deepcopy_with_paths(original, [["file"]]) + assert_different_identities(result, original) + assert result["file"] is file_bytes + + def test_list_popped_wholesale(self) -> None: + files = [b"f1", b"f2"] + original = {"files": files, "title": "t"} + result = deepcopy_with_paths(original, [["files", ""]]) + assert_different_identities(result, original) + result_files = result["files"] + assert isinstance(result_files, list) + assert_different_identities(result_files, files) + + def test_nested_array_path_copies_list_and_elements(self) -> None: + elem1 = {"file": b"f1", "extra": 1} + elem2 = {"file": b"f2", "extra": 2} + original = {"items": [elem1, elem2]} + result = deepcopy_with_paths(original, [["items", "", "file"]]) + assert_different_identities(result, original) + result_items = result["items"] + assert isinstance(result_items, list) + assert_different_identities(result_items, original["items"]) + assert_different_identities(result_items[0], elem1) + assert_different_identities(result_items[1], elem2) + + def test_empty_paths_returns_same_object(self) -> None: + original = {"foo": "bar"} + result = deepcopy_with_paths(original, []) + assert result is original + + def test_multiple_paths(self) -> None: + f1 = b"file1" + f2 = b"file2" + original = {"a": f1, "b": f2, "c": "unchanged"} + result = deepcopy_with_paths(original, [["a"], ["b"]]) + assert_different_identities(result, original) + assert result["a"] is f1 + assert result["b"] is f2 + assert result["c"] is original["c"] + + def test_extract_files_does_not_mutate_original_top_level(self) -> None: + file_bytes = b"contents" + original = {"file": file_bytes, "other": "value"} + + copied = deepcopy_with_paths(original, [["file"]]) + extracted = extract_files(copied, paths=[["file"]]) + + assert extracted == [("file", file_bytes)] + assert original == {"file": file_bytes, "other": "value"} + assert copied == {"other": "value"} + + def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: + file1 = b"f1" + file2 = b"f2" + original = { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + + copied = deepcopy_with_paths(original, [["items", "", "file"]]) + extracted = extract_files(copied, paths=[["items", "", "file"]]) + + assert [entry for _, entry in extracted] == [file1, file2] + assert original == { + "items": [ + {"file": file1, "extra": 1}, + {"file": file2, "extra": 2}, + ], + "title": "example", + } + assert copied == { + "items": [ + {"extra": 1}, + {"extra": 2}, + ], + "title": "example", + } diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..75e36cf --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,1017 @@ +import json +from typing import TYPE_CHECKING, Any, Dict, List, Union, Iterable, Optional, cast +from datetime import datetime, timezone +from collections import deque +from typing_extensions import Literal, Annotated, TypedDict, TypeAliasType + +import pytest +import pydantic +from pydantic import Field + +from vitable_connect._utils import PropertyInfo +from vitable_connect._compat import PYDANTIC_V1, parse_obj, model_dump, model_json +from vitable_connect._models import DISCRIMINATOR_CACHE, BaseModel, EagerIterable, construct_type + + +class BasicModel(BaseModel): + foo: str + + +@pytest.mark.parametrize("value", ["hello", 1], ids=["correct type", "mismatched"]) +def test_basic(value: object) -> None: + m = BasicModel.construct(foo=value) + assert m.foo == value + + +def test_directly_nested_model() -> None: + class NestedModel(BaseModel): + nested: BasicModel + + m = NestedModel.construct(nested={"foo": "Foo!"}) + assert m.nested.foo == "Foo!" + + # mismatched types + m = NestedModel.construct(nested="hello!") + assert cast(Any, m.nested) == "hello!" + + +def test_optional_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[BasicModel] + + m1 = NestedModel.construct(nested=None) + assert m1.nested is None + + m2 = NestedModel.construct(nested={"foo": "bar"}) + assert m2.nested is not None + assert m2.nested.foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested={"foo"}) + assert isinstance(cast(Any, m3.nested), set) + assert cast(Any, m3.nested) == {"foo"} + + +def test_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[BasicModel] + + m = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0].foo == "bar" + assert m.nested[1].foo == "2" + + # mismatched types + m = NestedModel.construct(nested=True) + assert cast(Any, m.nested) is True + + m = NestedModel.construct(nested=[False]) + assert cast(Any, m.nested) == [False] + + +def test_optional_list_nested_model() -> None: + class NestedModel(BaseModel): + nested: Optional[List[BasicModel]] + + m1 = NestedModel.construct(nested=[{"foo": "bar"}, {"foo": "2"}]) + assert m1.nested is not None + assert isinstance(m1.nested, list) + assert len(m1.nested) == 2 + assert m1.nested[0].foo == "bar" + assert m1.nested[1].foo == "2" + + m2 = NestedModel.construct(nested=None) + assert m2.nested is None + + # mismatched types + m3 = NestedModel.construct(nested={1}) + assert cast(Any, m3.nested) == {1} + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_optional_items_nested_model() -> None: + class NestedModel(BaseModel): + nested: List[Optional[BasicModel]] + + m = NestedModel.construct(nested=[None, {"foo": "bar"}]) + assert m.nested is not None + assert isinstance(m.nested, list) + assert len(m.nested) == 2 + assert m.nested[0] is None + assert m.nested[1] is not None + assert m.nested[1].foo == "bar" + + # mismatched types + m3 = NestedModel.construct(nested="foo") + assert cast(Any, m3.nested) == "foo" + + m4 = NestedModel.construct(nested=[False]) + assert cast(Any, m4.nested) == [False] + + +def test_list_mismatched_type() -> None: + class NestedModel(BaseModel): + nested: List[str] + + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_raw_dictionary() -> None: + class NestedModel(BaseModel): + nested: Dict[str, str] + + m = NestedModel.construct(nested={"hello": "world"}) + assert m.nested == {"hello": "world"} + + # mismatched types + m = NestedModel.construct(nested=False) + assert cast(Any, m.nested) is False + + +def test_nested_dictionary_model() -> None: + class NestedModel(BaseModel): + nested: Dict[str, BasicModel] + + m = NestedModel.construct(nested={"hello": {"foo": "bar"}}) + assert isinstance(m.nested, dict) + assert m.nested["hello"].foo == "bar" + + # mismatched types + m = NestedModel.construct(nested={"hello": False}) + assert cast(Any, m.nested["hello"]) is False + + +def test_unknown_fields() -> None: + m1 = BasicModel.construct(foo="foo", unknown=1) + assert m1.foo == "foo" + assert cast(Any, m1).unknown == 1 + + m2 = BasicModel.construct(foo="foo", unknown={"foo_bar": True}) + assert m2.foo == "foo" + assert cast(Any, m2).unknown == {"foo_bar": True} + + assert model_dump(m2) == {"foo": "foo", "unknown": {"foo_bar": True}} + + +def test_strict_validation_unknown_fields() -> None: + class Model(BaseModel): + foo: str + + model = parse_obj(Model, dict(foo="hello!", user="Robert")) + assert model.foo == "hello!" + assert cast(Any, model).user == "Robert" + + assert model_dump(model) == {"foo": "hello!", "user": "Robert"} + + +def test_aliases() -> None: + class Model(BaseModel): + my_field: int = Field(alias="myField") + + m = Model.construct(myField=1) + assert m.my_field == 1 + + # mismatched types + m = Model.construct(myField={"hello": False}) + assert cast(Any, m.my_field) == {"hello": False} + + +def test_repr() -> None: + model = BasicModel(foo="bar") + assert str(model) == "BasicModel(foo='bar')" + assert repr(model) == "BasicModel(foo='bar')" + + +def test_repr_nested_model() -> None: + class Child(BaseModel): + name: str + age: int + + class Parent(BaseModel): + name: str + child: Child + + model = Parent(name="Robert", child=Child(name="Foo", age=5)) + assert str(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + assert repr(model) == "Parent(name='Robert', child=Child(name='Foo', age=5))" + + +def test_optional_list() -> None: + class Submodel(BaseModel): + name: str + + class Model(BaseModel): + items: Optional[List[Submodel]] + + m = Model.construct(items=None) + assert m.items is None + + m = Model.construct(items=[]) + assert m.items == [] + + m = Model.construct(items=[{"name": "Robert"}]) + assert m.items is not None + assert len(m.items) == 1 + assert m.items[0].name == "Robert" + + +def test_nested_union_of_models() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + +def test_nested_union_of_mixed_types() -> None: + class Submodel1(BaseModel): + bar: bool + + class Model(BaseModel): + foo: Union[Submodel1, Literal[True], Literal["CARD_HOLDER"]] + + m = Model.construct(foo=True) + assert m.foo is True + + m = Model.construct(foo="CARD_HOLDER") + assert m.foo == "CARD_HOLDER" + + m = Model.construct(foo={"bar": False}) + assert isinstance(m.foo, Submodel1) + assert m.foo.bar is False + + +def test_nested_union_multiple_variants() -> None: + class Submodel1(BaseModel): + bar: bool + + class Submodel2(BaseModel): + thing: str + + class Submodel3(BaseModel): + foo: int + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2, None, Submodel3] + + m = Model.construct(foo={"thing": "hello"}) + assert isinstance(m.foo, Submodel2) + assert m.foo.thing == "hello" + + m = Model.construct(foo=None) + assert m.foo is None + + m = Model.construct() + assert m.foo is None + + m = Model.construct(foo={"foo": "1"}) + assert isinstance(m.foo, Submodel3) + assert m.foo.foo == 1 + + +def test_nested_union_invalid_data() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + foo: Union[Submodel1, Submodel2] + + m = Model.construct(foo=True) + assert cast(bool, m.foo) is True + + m = Model.construct(foo={"name": 3}) + if PYDANTIC_V1: + assert isinstance(m.foo, Submodel2) + assert m.foo.name == "3" + else: + assert isinstance(m.foo, Submodel1) + assert m.foo.name == 3 # type: ignore + + +def test_list_of_unions() -> None: + class Submodel1(BaseModel): + level: int + + class Submodel2(BaseModel): + name: str + + class Model(BaseModel): + items: List[Union[Submodel1, Submodel2]] + + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], Submodel2) + assert m.items[1].name == "Robert" + + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], Submodel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_union_of_lists() -> None: + class SubModel1(BaseModel): + level: int + + class SubModel2(BaseModel): + name: str + + class Model(BaseModel): + items: Union[List[SubModel1], List[SubModel2]] + + # with one valid entry + m = Model.construct(items=[{"name": "Robert"}]) + assert len(m.items) == 1 + assert isinstance(m.items[0], SubModel2) + assert m.items[0].name == "Robert" + + # with two entries pointing to different types + m = Model.construct(items=[{"level": 1}, {"name": "Robert"}]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == 1 + assert isinstance(m.items[1], SubModel1) + assert cast(Any, m.items[1]).name == "Robert" + + # with two entries pointing to *completely* different types + m = Model.construct(items=[{"level": -1}, 156]) + assert len(m.items) == 2 + assert isinstance(m.items[0], SubModel1) + assert m.items[0].level == -1 + assert cast(Any, m.items[1]) == 156 + + +def test_dict_of_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Dict[str, Union[SubModel1, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel2) + assert m.data["foo"].foo == "bar" + + # TODO: test mismatched type + + +def test_double_nested_union() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + bar: str + + class Model(BaseModel): + data: Dict[str, List[Union[SubModel1, SubModel2]]] + + m = Model.construct(data={"foo": [{"bar": "baz"}, {"name": "Robert"}]}) + assert len(m.data["foo"]) == 2 + + entry1 = m.data["foo"][0] + assert isinstance(entry1, SubModel2) + assert entry1.bar == "baz" + + entry2 = m.data["foo"][1] + assert isinstance(entry2, SubModel1) + assert entry2.name == "Robert" + + # TODO: test mismatched type + + +def test_union_of_dict() -> None: + class SubModel1(BaseModel): + name: str + + class SubModel2(BaseModel): + foo: str + + class Model(BaseModel): + data: Union[Dict[str, SubModel1], Dict[str, SubModel2]] + + m = Model.construct(data={"hello": {"name": "there"}, "foo": {"foo": "bar"}}) + assert len(list(m.data.keys())) == 2 + assert isinstance(m.data["hello"], SubModel1) + assert m.data["hello"].name == "there" + assert isinstance(m.data["foo"], SubModel1) + assert cast(Any, m.data["foo"]).foo == "bar" + + +def test_iso8601_datetime() -> None: + class Model(BaseModel): + created_at: datetime + + expected = datetime(2019, 12, 27, 18, 11, 19, 117000, tzinfo=timezone.utc) + + if PYDANTIC_V1: + expected_json = '{"created_at": "2019-12-27T18:11:19.117000+00:00"}' + else: + expected_json = '{"created_at":"2019-12-27T18:11:19.117000Z"}' + + model = Model.construct(created_at="2019-12-27T18:11:19.117Z") + assert model.created_at == expected + assert model_json(model) == expected_json + + model = parse_obj(Model, dict(created_at="2019-12-27T18:11:19.117Z")) + assert model.created_at == expected + assert model_json(model) == expected_json + + +def test_does_not_coerce_int() -> None: + class Model(BaseModel): + bar: int + + assert Model.construct(bar=1).bar == 1 + assert Model.construct(bar=10.9).bar == 10.9 + assert Model.construct(bar="19").bar == "19" # type: ignore[comparison-overlap] + assert Model.construct(bar=False).bar is False + + +def test_int_to_float_safe_conversion() -> None: + class Model(BaseModel): + float_field: float + + m = Model.construct(float_field=10) + assert m.float_field == 10.0 + assert isinstance(m.float_field, float) + + m = Model.construct(float_field=10.12) + assert m.float_field == 10.12 + assert isinstance(m.float_field, float) + + # number too big + m = Model.construct(float_field=2**53 + 1) + assert m.float_field == 2**53 + 1 + assert isinstance(m.float_field, int) + + +def test_deprecated_alias() -> None: + class Model(BaseModel): + resource_id: str = Field(alias="model_id") + + @property + def model_id(self) -> str: + return self.resource_id + + m = Model.construct(model_id="id") + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + m = parse_obj(Model, {"model_id": "id"}) + assert m.model_id == "id" + assert m.resource_id == "id" + assert m.resource_id is m.model_id + + +def test_omitted_fields() -> None: + class Model(BaseModel): + resource_id: Optional[str] = None + + m = Model.construct() + assert m.resource_id is None + assert "resource_id" not in m.model_fields_set + + m = Model.construct(resource_id=None) + assert m.resource_id is None + assert "resource_id" in m.model_fields_set + + m = Model.construct(resource_id="foo") + assert m.resource_id == "foo" + assert "resource_id" in m.model_fields_set + + +def test_to_dict() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.to_dict() == {"FOO": "hello"} + assert m.to_dict(use_api_names=False) == {"foo": "hello"} + + m2 = Model() + assert m2.to_dict() == {} + assert m2.to_dict(exclude_unset=False) == {"FOO": None} + assert m2.to_dict(exclude_unset=False, exclude_none=True) == {} + assert m2.to_dict(exclude_unset=False, exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.to_dict() == {"FOO": None} + assert m3.to_dict(exclude_none=True) == {} + assert m3.to_dict(exclude_defaults=True) == {} + + class Model2(BaseModel): + created_at: datetime + + time_str = "2024-03-21T11:39:01.275859" + m4 = Model2.construct(created_at=time_str) + assert m4.to_dict(mode="python") == {"created_at": datetime.fromisoformat(time_str)} + assert m4.to_dict(mode="json") == {"created_at": time_str} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_dict(warnings=False) + + +def test_forwards_compat_model_dump_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert m.model_dump() == {"foo": "hello"} + assert m.model_dump(include={"bar"}) == {} + assert m.model_dump(exclude={"foo"}) == {} + assert m.model_dump(by_alias=True) == {"FOO": "hello"} + + m2 = Model() + assert m2.model_dump() == {"foo": None} + assert m2.model_dump(exclude_unset=True) == {} + assert m2.model_dump(exclude_none=True) == {} + assert m2.model_dump(exclude_defaults=True) == {} + + m3 = Model(FOO=None) + assert m3.model_dump() == {"foo": None} + assert m3.model_dump(exclude_none=True) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump(warnings=False) + + +def test_compat_method_no_error_for_warnings() -> None: + class Model(BaseModel): + foo: Optional[str] + + m = Model(foo="hello") + assert isinstance(model_dump(m, warnings=False), dict) + + +def test_to_json() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.to_json()) == {"FOO": "hello"} + assert json.loads(m.to_json(use_api_names=False)) == {"foo": "hello"} + + if PYDANTIC_V1: + assert m.to_json(indent=None) == '{"FOO": "hello"}' + else: + assert m.to_json(indent=None) == '{"FOO":"hello"}' + + m2 = Model() + assert json.loads(m2.to_json()) == {} + assert json.loads(m2.to_json(exclude_unset=False)) == {"FOO": None} + assert json.loads(m2.to_json(exclude_unset=False, exclude_none=True)) == {} + assert json.loads(m2.to_json(exclude_unset=False, exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.to_json()) == {"FOO": None} + assert json.loads(m3.to_json(exclude_none=True)) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.to_json(warnings=False) + + +def test_forwards_compat_model_dump_json_method() -> None: + class Model(BaseModel): + foo: Optional[str] = Field(alias="FOO", default=None) + + m = Model(FOO="hello") + assert json.loads(m.model_dump_json()) == {"foo": "hello"} + assert json.loads(m.model_dump_json(include={"bar"})) == {} + assert json.loads(m.model_dump_json(include={"foo"})) == {"foo": "hello"} + assert json.loads(m.model_dump_json(by_alias=True)) == {"FOO": "hello"} + + assert m.model_dump_json(indent=2) == '{\n "foo": "hello"\n}' + + m2 = Model() + assert json.loads(m2.model_dump_json()) == {"foo": None} + assert json.loads(m2.model_dump_json(exclude_unset=True)) == {} + assert json.loads(m2.model_dump_json(exclude_none=True)) == {} + assert json.loads(m2.model_dump_json(exclude_defaults=True)) == {} + + m3 = Model(FOO=None) + assert json.loads(m3.model_dump_json()) == {"foo": None} + assert json.loads(m3.model_dump_json(exclude_none=True)) == {} + + if PYDANTIC_V1: + with pytest.raises(ValueError, match="round_trip is only supported in Pydantic v2"): + m.model_dump_json(round_trip=True) + + with pytest.raises(ValueError, match="warnings is only supported in Pydantic v2"): + m.model_dump_json(warnings=False) + + +def test_type_compat() -> None: + # our model type can be assigned to Pydantic's model type + + def takes_pydantic(model: pydantic.BaseModel) -> None: # noqa: ARG001 + ... + + class OurModel(BaseModel): + foo: Optional[str] = None + + takes_pydantic(OurModel()) + + +def test_annotated_types() -> None: + class Model(BaseModel): + value: str + + m = construct_type( + value={"value": "foo"}, + type_=cast(Any, Annotated[Model, "random metadata"]), + ) + assert isinstance(m, Model) + assert m.value == "foo" + + +def test_discriminated_unions_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, A) + assert m.type == "a" + if PYDANTIC_V1: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] + + +def test_discriminated_unions_unknown_variant() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + m = construct_type( + value={"type": "c", "data": None, "new_thing": "bar"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + + # just chooses the first variant + assert isinstance(m, A) + assert m.type == "c" # type: ignore[comparison-overlap] + assert m.data == None # type: ignore[unreachable] + assert m.new_thing == "bar" + + +def test_discriminated_unions_invalid_data_nested_unions() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + class C(BaseModel): + type: Literal["c"] + + data: bool + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "c", "data": "foo"}, + type_=cast(Any, Annotated[Union[Union[A, B], C], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, C) + assert m.type == "c" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_with_aliases_invalid_data() -> None: + class A(BaseModel): + foo_type: Literal["a"] = Field(alias="type") + + data: str + + class B(BaseModel): + foo_type: Literal["b"] = Field(alias="type") + + data: int + + m = construct_type( + value={"type": "b", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, B) + assert m.foo_type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + m = construct_type( + value={"type": "a", "data": 100}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="foo_type")]), + ) + assert isinstance(m, A) + assert m.foo_type == "a" + if PYDANTIC_V1: + # pydantic v1 automatically converts inputs to strings + # if the expected type is a str + assert m.data == "100" + else: + assert m.data == 100 # type: ignore[comparison-overlap] + + +def test_discriminated_unions_overlapping_discriminators_invalid_data() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["a"] + + data: int + + m = construct_type( + value={"type": "a", "data": "foo"}, + type_=cast(Any, Annotated[Union[A, B], PropertyInfo(discriminator="type")]), + ) + assert isinstance(m, B) + assert m.type == "a" + assert m.data == "foo" # type: ignore[comparison-overlap] + + +def test_discriminated_unions_invalid_data_uses_cache() -> None: + class A(BaseModel): + type: Literal["a"] + + data: str + + class B(BaseModel): + type: Literal["b"] + + data: int + + UnionType = cast(Any, Union[A, B]) + + assert not DISCRIMINATOR_CACHE.get(UnionType) + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + discriminator = DISCRIMINATOR_CACHE.get(UnionType) + assert discriminator is not None + + m = construct_type( + value={"type": "b", "data": "foo"}, type_=cast(Any, Annotated[UnionType, PropertyInfo(discriminator="type")]) + ) + assert isinstance(m, B) + assert m.type == "b" + assert m.data == "foo" # type: ignore[comparison-overlap] + + # if the discriminator details object stays the same between invocations then + # we hit the cache + assert DISCRIMINATOR_CACHE.get(UnionType) is discriminator + + +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") +def test_type_alias_type() -> None: + Alias = TypeAliasType("Alias", str) # pyright: ignore + + class Model(BaseModel): + alias: Alias + union: Union[int, Alias] + + m = construct_type(value={"alias": "foo", "union": "bar"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.alias, str) + assert m.alias == "foo" + assert isinstance(m.union, str) + assert m.union == "bar" + + +@pytest.mark.skipif(PYDANTIC_V1, reason="TypeAliasType is not supported in Pydantic v1") +def test_field_named_cls() -> None: + class Model(BaseModel): + cls: str + + m = construct_type(value={"cls": "foo"}, type_=Model) + assert isinstance(m, Model) + assert isinstance(m.cls, str) + + +def test_discriminated_union_case() -> None: + class A(BaseModel): + type: Literal["a"] + + data: bool + + class B(BaseModel): + type: Literal["b"] + + data: List[Union[A, object]] + + class ModelA(BaseModel): + type: Literal["modelA"] + + data: int + + class ModelB(BaseModel): + type: Literal["modelB"] + + required: str + + data: Union[A, B] + + # when constructing ModelA | ModelB, value data doesn't match ModelB exactly - missing `required` + m = construct_type( + value={"type": "modelB", "data": {"type": "a", "data": True}}, + type_=cast(Any, Annotated[Union[ModelA, ModelB], PropertyInfo(discriminator="type")]), + ) + + assert isinstance(m, ModelB) + + +def test_nested_discriminated_union() -> None: + class InnerType1(BaseModel): + type: Literal["type_1"] + + class InnerModel(BaseModel): + inner_value: str + + class InnerType2(BaseModel): + type: Literal["type_2"] + some_inner_model: InnerModel + + class Type1(BaseModel): + base_type: Literal["base_type_1"] + value: Annotated[ + Union[ + InnerType1, + InnerType2, + ], + PropertyInfo(discriminator="type"), + ] + + class Type2(BaseModel): + base_type: Literal["base_type_2"] + + T = Annotated[ + Union[ + Type1, + Type2, + ], + PropertyInfo(discriminator="base_type"), + ] + + model = construct_type( + type_=T, + value={ + "base_type": "base_type_1", + "value": { + "type": "type_2", + }, + }, + ) + assert isinstance(model, Type1) + assert isinstance(model.value, InnerType2) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2 for now") +def test_extra_properties() -> None: + class Item(BaseModel): + prop: int + + class Model(BaseModel): + __pydantic_extra__: Dict[str, Item] = Field(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + other: str + + if TYPE_CHECKING: + + def __getattr__(self, attr: str) -> Item: ... + + model = construct_type( + type_=Model, + value={ + "a": {"prop": 1}, + "other": "foo", + }, + ) + assert isinstance(model, Model) + assert model.a.prop == 1 + assert isinstance(model.a, Item) + assert model.other == "foo" + + +# NOTE: Workaround for Pydantic Iterable behavior. +# Iterable fields are replaced with a ValidatorIterator and may be consumed +# during serialization, which can cause subsequent dumps to return empty data. +# See: https://github.com/pydantic/pydantic/issues/9541 +@pytest.mark.parametrize( + "data, expected_validated", + [ + ([1, 2, 3], [1, 2, 3]), + ((1, 2, 3), (1, 2, 3)), + (set([1, 2, 3]), set([1, 2, 3])), + (iter([1, 2, 3]), [1, 2, 3]), + ([], []), + ((x for x in [1, 2, 3]), [1, 2, 3]), + (map(lambda x: x, [1, 2, 3]), [1, 2, 3]), + (frozenset([1, 2, 3]), frozenset([1, 2, 3])), + (deque([1, 2, 3]), deque([1, 2, 3])), + ], + ids=["list", "tuple", "set", "iterator", "empty", "generator", "map", "frozenset", "deque"], +) +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction(data: Iterable[int], expected_validated: Iterable[int]) -> None: + class TypeWithIterable(TypedDict): + items: EagerIterable[int] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": data}}) + assert m.data["items"] == expected_validated + + # Verify repeated dumps don't lose data (the original bug) + assert m.model_dump()["data"]["items"] == list(expected_validated) + assert m.model_dump()["data"]["items"] == list(expected_validated) + + +@pytest.mark.skipif(PYDANTIC_V1, reason="this is only supported in pydantic v2") +def test_iterable_construction_str_falls_back_to_list() -> None: + # str is iterable (over chars), but str(list_of_chars) produces the list's repr + # rather than reconstructing a string from items. We special-case str to fall + # back to list instead of attempting reconstruction. + class TypeWithIterable(TypedDict): + items: EagerIterable[str] + + class Model(BaseModel): + data: TypeWithIterable + + m = Model.model_validate({"data": {"items": "hello"}}) + + # falls back to list of chars rather than calling str(["h", "e", "l", "l", "o"]) + assert m.data["items"] == ["h", "e", "l", "l", "o"] + assert m.model_dump()["data"]["items"] == ["h", "e", "l", "l", "o"] diff --git a/tests/test_qs.py b/tests/test_qs.py new file mode 100644 index 0000000..27581df --- /dev/null +++ b/tests/test_qs.py @@ -0,0 +1,78 @@ +from typing import Any, cast +from functools import partial +from urllib.parse import unquote + +import pytest + +from vitable_connect._qs import Querystring, stringify + + +def test_empty() -> None: + assert stringify({}) == "" + assert stringify({"a": {}}) == "" + assert stringify({"a": {"b": {"c": {}}}}) == "" + + +def test_basic() -> None: + assert stringify({"a": 1}) == "a=1" + assert stringify({"a": "b"}) == "a=b" + assert stringify({"a": True}) == "a=true" + assert stringify({"a": False}) == "a=false" + assert stringify({"a": 1.23456}) == "a=1.23456" + assert stringify({"a": None}) == "" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_nested_dotted(method: str) -> None: + if method == "class": + serialise = Querystring(nested_format="dots").stringify + else: + serialise = partial(stringify, nested_format="dots") + + assert unquote(serialise({"a": {"b": "c"}})) == "a.b=c" + assert unquote(serialise({"a": {"b": "c", "d": "e", "f": "g"}})) == "a.b=c&a.d=e&a.f=g" + assert unquote(serialise({"a": {"b": {"c": {"d": "e"}}}})) == "a.b.c.d=e" + assert unquote(serialise({"a": {"b": True}})) == "a.b=true" + + +def test_nested_brackets() -> None: + assert unquote(stringify({"a": {"b": "c"}})) == "a[b]=c" + assert unquote(stringify({"a": {"b": "c", "d": "e", "f": "g"}})) == "a[b]=c&a[d]=e&a[f]=g" + assert unquote(stringify({"a": {"b": {"c": {"d": "e"}}}})) == "a[b][c][d]=e" + assert unquote(stringify({"a": {"b": True}})) == "a[b]=true" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_comma(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="comma").stringify + else: + serialise = partial(stringify, array_format="comma") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in=foo,bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b]=true,false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b]=true,false,true" + + +def test_array_repeat() -> None: + assert unquote(stringify({"in": ["foo", "bar"]})) == "in=foo&in=bar" + assert unquote(stringify({"a": {"b": [True, False]}})) == "a[b]=true&a[b]=false" + assert unquote(stringify({"a": {"b": [True, False, None, True]}})) == "a[b]=true&a[b]=false&a[b]=true" + assert unquote(stringify({"in": ["foo", {"b": {"c": ["d", "e"]}}]})) == "in=foo&in[b][c]=d&in[b][c]=e" + + +@pytest.mark.parametrize("method", ["class", "function"]) +def test_array_brackets(method: str) -> None: + if method == "class": + serialise = Querystring(array_format="brackets").stringify + else: + serialise = partial(stringify, array_format="brackets") + + assert unquote(serialise({"in": ["foo", "bar"]})) == "in[]=foo&in[]=bar" + assert unquote(serialise({"a": {"b": [True, False]}})) == "a[b][]=true&a[b][]=false" + assert unquote(serialise({"a": {"b": [True, False, None, True]}})) == "a[b][]=true&a[b][]=false&a[b][]=true" + + +def test_unknown_array_format() -> None: + with pytest.raises(NotImplementedError, match="Unknown array_format value: foo, choose from comma, repeat"): + stringify({"a": ["foo", "bar"]}, array_format=cast(Any, "foo")) diff --git a/tests/test_required_args.py b/tests/test_required_args.py new file mode 100644 index 0000000..f868d61 --- /dev/null +++ b/tests/test_required_args.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import pytest + +from vitable_connect._utils import required_args + + +def test_too_many_positional_params() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + with pytest.raises(TypeError, match=r"foo\(\) takes 1 argument\(s\) but 2 were given"): + foo("a", "b") # type: ignore + + +def test_positional_param() -> None: + @required_args(["a"]) + def foo(a: str | None = None) -> str | None: + return a + + assert foo("a") == "a" + assert foo(None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_keyword_only_param() -> None: + @required_args(["a"]) + def foo(*, a: str | None = None) -> str | None: + return a + + assert foo(a="a") == "a" + assert foo(a=None) is None + assert foo(a="b") == "b" + + with pytest.raises(TypeError, match="Missing required argument: 'a'"): + foo() + + +def test_multiple_params() -> None: + @required_args(["a", "b", "c"]) + def foo(a: str = "", *, b: str = "", c: str = "") -> str | None: + return f"{a} {b} {c}" + + assert foo(a="a", b="b", c="c") == "a b c" + + error_message = r"Missing required arguments.*" + + with pytest.raises(TypeError, match=error_message): + foo() + + with pytest.raises(TypeError, match=error_message): + foo(a="a") + + with pytest.raises(TypeError, match=error_message): + foo(b="b") + + with pytest.raises(TypeError, match=error_message): + foo(c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'a'"): + foo(b="a", c="c") + + with pytest.raises(TypeError, match=r"Missing required argument: 'b'"): + foo("a", c="c") + + +def test_multiple_variants() -> None: + @required_args(["a"], ["b"]) + def foo(*, a: str | None = None, b: str | None = None) -> str | None: + return a if a is not None else b + + assert foo(a="foo") == "foo" + assert foo(b="bar") == "bar" + assert foo(a=None) is None + assert foo(b=None) is None + + # TODO: this error message could probably be improved + with pytest.raises( + TypeError, + match=r"Missing required arguments; Expected either \('a'\) or \('b'\) arguments to be given", + ): + foo() + + +def test_multiple_params_multiple_variants() -> None: + @required_args(["a", "b"], ["c"]) + def foo(*, a: str | None = None, b: str | None = None, c: str | None = None) -> str | None: + if a is not None: + return a + if b is not None: + return b + return c + + error_message = r"Missing required arguments; Expected either \('a' and 'b'\) or \('c'\) arguments to be given" + + with pytest.raises(TypeError, match=error_message): + foo(a="foo") + + with pytest.raises(TypeError, match=error_message): + foo(b="bar") + + with pytest.raises(TypeError, match=error_message): + foo() + + assert foo(a=None, b="bar") == "bar" + assert foo(c=None) is None + assert foo(c="foo") == "foo" diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 0000000..db60e23 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,277 @@ +import json +from typing import Any, List, Union, cast +from typing_extensions import Annotated + +import httpx +import pytest +import pydantic + +from vitable_connect import BaseModel, VitableConnect, AsyncVitableConnect +from vitable_connect._response import ( + APIResponse, + BaseAPIResponse, + AsyncAPIResponse, + BinaryAPIResponse, + AsyncBinaryAPIResponse, + extract_response_type, +) +from vitable_connect._streaming import Stream +from vitable_connect._base_client import FinalRequestOptions + + +class ConcreteBaseAPIResponse(APIResponse[bytes]): ... + + +class ConcreteAPIResponse(APIResponse[List[str]]): ... + + +class ConcreteAsyncAPIResponse(APIResponse[httpx.Response]): ... + + +def test_extract_response_type_direct_classes() -> None: + assert extract_response_type(BaseAPIResponse[str]) == str + assert extract_response_type(APIResponse[str]) == str + assert extract_response_type(AsyncAPIResponse[str]) == str + + +def test_extract_response_type_direct_class_missing_type_arg() -> None: + with pytest.raises( + RuntimeError, + match="Expected type to have a type argument at index 0 but it did not", + ): + extract_response_type(AsyncAPIResponse) + + +def test_extract_response_type_concrete_subclasses() -> None: + assert extract_response_type(ConcreteBaseAPIResponse) == bytes + assert extract_response_type(ConcreteAPIResponse) == List[str] + assert extract_response_type(ConcreteAsyncAPIResponse) == httpx.Response + + +def test_extract_response_type_binary_response() -> None: + assert extract_response_type(BinaryAPIResponse) == bytes + assert extract_response_type(AsyncBinaryAPIResponse) == bytes + + +class PydanticModel(pydantic.BaseModel): ... + + +def test_response_parse_mismatched_basemodel(client: VitableConnect) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo"), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + with pytest.raises( + TypeError, + match="Pydantic models must subclass our base model type, e.g. `from vitable_connect import BaseModel`", + ): + response.parse(to=PydanticModel) + + +@pytest.mark.asyncio +async def test_async_response_parse_mismatched_basemodel(async_client: AsyncVitableConnect) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo"), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + with pytest.raises( + TypeError, + match="Pydantic models must subclass our base model type, e.g. `from vitable_connect import BaseModel`", + ): + await response.parse(to=PydanticModel) + + +def test_response_parse_custom_stream(client: VitableConnect) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo"), + client=client, + stream=True, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + stream = response.parse(to=Stream[int]) + assert stream._cast_to == int + + +@pytest.mark.asyncio +async def test_async_response_parse_custom_stream(async_client: AsyncVitableConnect) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo"), + client=async_client, + stream=True, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + stream = await response.parse(to=Stream[int]) + assert stream._cast_to == int + + +class CustomModel(BaseModel): + foo: str + bar: int + + +def test_response_parse_custom_model(client: VitableConnect) -> None: + response = APIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse(to=CustomModel) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +@pytest.mark.asyncio +async def test_async_response_parse_custom_model(async_client: AsyncVitableConnect) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=CustomModel) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +def test_response_parse_annotated_type(client: VitableConnect) -> None: + response = APIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +async def test_async_response_parse_annotated_type(async_client: AsyncVitableConnect) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=json.dumps({"foo": "hello!", "bar": 2})), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse( + to=cast("type[CustomModel]", Annotated[CustomModel, "random metadata"]), + ) + assert obj.foo == "hello!" + assert obj.bar == 2 + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +def test_response_parse_bool(client: VitableConnect, content: str, expected: bool) -> None: + response = APIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = response.parse(to=bool) + assert result is expected + + +@pytest.mark.parametrize( + "content, expected", + [ + ("false", False), + ("true", True), + ("False", False), + ("True", True), + ("TrUe", True), + ("FalSe", False), + ], +) +async def test_async_response_parse_bool(client: AsyncVitableConnect, content: str, expected: bool) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=content), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + result = await response.parse(to=bool) + assert result is expected + + +class OtherModel(BaseModel): + a: str + + +@pytest.mark.parametrize("client", [False], indirect=True) # loose validation +def test_response_parse_expect_model_union_non_json_content(client: VitableConnect) -> None: + response = APIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("async_client", [False], indirect=True) # loose validation +async def test_async_response_parse_expect_model_union_non_json_content(async_client: AsyncVitableConnect) -> None: + response = AsyncAPIResponse( + raw=httpx.Response(200, content=b"foo", headers={"Content-Type": "application/text"}), + client=async_client, + stream=False, + stream_cls=None, + cast_to=str, + options=FinalRequestOptions.construct(method="get", url="/foo"), + ) + + obj = await response.parse(to=cast(Any, Union[CustomModel, OtherModel])) + assert isinstance(obj, str) + assert obj == "foo" diff --git a/tests/test_streaming.py b/tests/test_streaming.py new file mode 100644 index 0000000..fb9cbf4 --- /dev/null +++ b/tests/test_streaming.py @@ -0,0 +1,252 @@ +from __future__ import annotations + +from typing import Iterator, AsyncIterator + +import httpx +import pytest + +from vitable_connect import VitableConnect, AsyncVitableConnect +from vitable_connect._streaming import Stream, AsyncStream, ServerSentEvent + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_basic(sync: bool, client: VitableConnect, async_client: AsyncVitableConnect) -> None: + def body() -> Iterator[bytes]: + yield b"event: completion\n" + yield b'data: {"foo":true}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_data_missing_event(sync: bool, client: VitableConnect, async_client: AsyncVitableConnect) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"foo":true}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_event_missing_data(sync: bool, client: VitableConnect, async_client: AsyncVitableConnect) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.data == "" + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_events(sync: bool, client: VitableConnect, async_client: AsyncVitableConnect) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"\n" + yield b"event: completion\n" + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.data == "" + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.data == "" + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_events_with_data(sync: bool, client: VitableConnect, async_client: AsyncVitableConnect) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b'data: {"foo":true}\n' + yield b"\n" + yield b"event: completion\n" + yield b'data: {"bar":false}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + + sse = await iter_next(iterator) + assert sse.event == "completion" + assert sse.json() == {"bar": False} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_data_lines_with_empty_line( + sync: bool, client: VitableConnect, async_client: AsyncVitableConnect +) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"data: {\n" + yield b'data: "foo":\n' + yield b"data: \n" + yield b"data:\n" + yield b"data: true}\n" + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + assert sse.data == '{\n"foo":\n\n\ntrue}' + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_data_json_escaped_double_new_line( + sync: bool, client: VitableConnect, async_client: AsyncVitableConnect +) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b'data: {"foo": "my long\\n\\ncontent"}' + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": "my long\n\ncontent"} + + await assert_empty_iter(iterator) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multiple_data_lines(sync: bool, client: VitableConnect, async_client: AsyncVitableConnect) -> None: + def body() -> Iterator[bytes]: + yield b"event: ping\n" + yield b"data: {\n" + yield b'data: "foo":\n' + yield b"data: true}\n" + yield b"\n\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event == "ping" + assert sse.json() == {"foo": True} + + await assert_empty_iter(iterator) + + +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_special_new_line_character( + sync: bool, + client: VitableConnect, + async_client: AsyncVitableConnect, +) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"content":" culpa"}\n' + yield b"\n" + yield b'data: {"content":" \xe2\x80\xa8"}\n' + yield b"\n" + yield b'data: {"content":"foo"}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": " culpa"} + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": " 
"} + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": "foo"} + + await assert_empty_iter(iterator) + + +@pytest.mark.parametrize("sync", [True, False], ids=["sync", "async"]) +async def test_multi_byte_character_multiple_chunks( + sync: bool, + client: VitableConnect, + async_client: AsyncVitableConnect, +) -> None: + def body() -> Iterator[bytes]: + yield b'data: {"content":"' + # bytes taken from the string 'известни' and arbitrarily split + # so that some multi-byte characters span multiple chunks + yield b"\xd0" + yield b"\xb8\xd0\xb7\xd0" + yield b"\xb2\xd0\xb5\xd1\x81\xd1\x82\xd0\xbd\xd0\xb8" + yield b'"}\n' + yield b"\n" + + iterator = make_event_iterator(content=body(), sync=sync, client=client, async_client=async_client) + + sse = await iter_next(iterator) + assert sse.event is None + assert sse.json() == {"content": "известни"} + + +async def to_aiter(iter: Iterator[bytes]) -> AsyncIterator[bytes]: + for chunk in iter: + yield chunk + + +async def iter_next(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> ServerSentEvent: + if isinstance(iter, AsyncIterator): + return await iter.__anext__() + + return next(iter) + + +async def assert_empty_iter(iter: Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]) -> None: + with pytest.raises((StopAsyncIteration, RuntimeError)): + await iter_next(iter) + + +def make_event_iterator( + content: Iterator[bytes], + *, + sync: bool, + client: VitableConnect, + async_client: AsyncVitableConnect, +) -> Iterator[ServerSentEvent] | AsyncIterator[ServerSentEvent]: + if sync: + return Stream(cast_to=object, client=client, response=httpx.Response(200, content=content))._iter_events() + + return AsyncStream( + cast_to=object, client=async_client, response=httpx.Response(200, content=to_aiter(content)) + )._iter_events() diff --git a/tests/test_transform.py b/tests/test_transform.py new file mode 100644 index 0000000..470d7ff --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,460 @@ +from __future__ import annotations + +import io +import pathlib +from typing import Any, Dict, List, Union, TypeVar, Iterable, Optional, cast +from datetime import date, datetime +from typing_extensions import Required, Annotated, TypedDict + +import pytest + +from vitable_connect._types import Base64FileInput, omit, not_given +from vitable_connect._utils import ( + PropertyInfo, + transform as _transform, + parse_datetime, + async_transform as _async_transform, +) +from vitable_connect._compat import PYDANTIC_V1 +from vitable_connect._models import BaseModel + +_T = TypeVar("_T") + +SAMPLE_FILE_PATH = pathlib.Path(__file__).parent.joinpath("sample_file.txt") + + +async def transform( + data: _T, + expected_type: object, + use_async: bool, +) -> _T: + if use_async: + return await _async_transform(data, expected_type=expected_type) + + return _transform(data, expected_type=expected_type) + + +parametrize = pytest.mark.parametrize("use_async", [False, True], ids=["sync", "async"]) + + +class Foo1(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +@parametrize +@pytest.mark.asyncio +async def test_top_level_alias(use_async: bool) -> None: + assert await transform({"foo_bar": "hello"}, expected_type=Foo1, use_async=use_async) == {"fooBar": "hello"} + + +class Foo2(TypedDict): + bar: Bar2 + + +class Bar2(TypedDict): + this_thing: Annotated[int, PropertyInfo(alias="this__thing")] + baz: Annotated[Baz2, PropertyInfo(alias="Baz")] + + +class Baz2(TypedDict): + my_baz: Annotated[str, PropertyInfo(alias="myBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_recursive_typeddict(use_async: bool) -> None: + assert await transform({"bar": {"this_thing": 1}}, Foo2, use_async) == {"bar": {"this__thing": 1}} + assert await transform({"bar": {"baz": {"my_baz": "foo"}}}, Foo2, use_async) == {"bar": {"Baz": {"myBaz": "foo"}}} + + +class Foo3(TypedDict): + things: List[Bar3] + + +class Bar3(TypedDict): + my_field: Annotated[str, PropertyInfo(alias="myField")] + + +@parametrize +@pytest.mark.asyncio +async def test_list_of_typeddict(use_async: bool) -> None: + result = await transform({"things": [{"my_field": "foo"}, {"my_field": "foo2"}]}, Foo3, use_async) + assert result == {"things": [{"myField": "foo"}, {"myField": "foo2"}]} + + +class Foo4(TypedDict): + foo: Union[Bar4, Baz4] + + +class Bar4(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz4(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_typeddict(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo4, use_async) == {"foo": {"fooBar": "bar"}} + assert await transform({"foo": {"foo_baz": "baz"}}, Foo4, use_async) == {"foo": {"fooBaz": "baz"}} + assert await transform({"foo": {"foo_baz": "baz", "foo_bar": "bar"}}, Foo4, use_async) == { + "foo": {"fooBaz": "baz", "fooBar": "bar"} + } + + +class Foo5(TypedDict): + foo: Annotated[Union[Bar4, List[Baz4]], PropertyInfo(alias="FOO")] + + +class Bar5(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz5(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_union_of_list(use_async: bool) -> None: + assert await transform({"foo": {"foo_bar": "bar"}}, Foo5, use_async) == {"FOO": {"fooBar": "bar"}} + assert await transform( + { + "foo": [ + {"foo_baz": "baz"}, + {"foo_baz": "baz"}, + ] + }, + Foo5, + use_async, + ) == {"FOO": [{"fooBaz": "baz"}, {"fooBaz": "baz"}]} + + +class Foo6(TypedDict): + bar: Annotated[str, PropertyInfo(alias="Bar")] + + +@parametrize +@pytest.mark.asyncio +async def test_includes_unknown_keys(use_async: bool) -> None: + assert await transform({"bar": "bar", "baz_": {"FOO": 1}}, Foo6, use_async) == { + "Bar": "bar", + "baz_": {"FOO": 1}, + } + + +class Foo7(TypedDict): + bar: Annotated[List[Bar7], PropertyInfo(alias="bAr")] + foo: Bar7 + + +class Bar7(TypedDict): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_ignores_invalid_input(use_async: bool) -> None: + assert await transform({"bar": ""}, Foo7, use_async) == {"bAr": ""} + assert await transform({"foo": ""}, Foo7, use_async) == {"foo": ""} + + +class DatetimeDict(TypedDict, total=False): + foo: Annotated[datetime, PropertyInfo(format="iso8601")] + + bar: Annotated[Optional[datetime], PropertyInfo(format="iso8601")] + + required: Required[Annotated[Optional[datetime], PropertyInfo(format="iso8601")]] + + list_: Required[Annotated[Optional[List[datetime]], PropertyInfo(format="iso8601")]] + + union: Annotated[Union[int, datetime], PropertyInfo(format="iso8601")] + + +class DateDict(TypedDict, total=False): + foo: Annotated[date, PropertyInfo(format="iso8601")] + + +class DatetimeModel(BaseModel): + foo: datetime + + +class DateModel(BaseModel): + foo: Optional[date] + + +@parametrize +@pytest.mark.asyncio +async def test_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + tz = "+00:00" if PYDANTIC_V1 else "Z" + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692" + tz} # type: ignore[comparison-overlap] + + dt = dt.replace(tzinfo=None) + assert await transform({"foo": dt}, DatetimeDict, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + assert await transform(DatetimeModel(foo=dt), Any, use_async) == {"foo": "2023-02-23T14:16:36.337692"} # type: ignore[comparison-overlap] + + assert await transform({"foo": None}, DateDict, use_async) == {"foo": None} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=None), Any, use_async) == {"foo": None} # type: ignore + assert await transform({"foo": date.fromisoformat("2023-02-23")}, DateDict, use_async) == {"foo": "2023-02-23"} # type: ignore[comparison-overlap] + assert await transform(DateModel(foo=date.fromisoformat("2023-02-23")), DateDict, use_async) == { + "foo": "2023-02-23" + } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_optional_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"bar": dt}, DatetimeDict, use_async) == {"bar": "2023-02-23T14:16:36.337692+00:00"} # type: ignore[comparison-overlap] + + assert await transform({"bar": None}, DatetimeDict, use_async) == {"bar": None} + + +@parametrize +@pytest.mark.asyncio +async def test_required_iso8601_format(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"required": dt}, DatetimeDict, use_async) == { + "required": "2023-02-23T14:16:36.337692+00:00" + } # type: ignore[comparison-overlap] + + assert await transform({"required": None}, DatetimeDict, use_async) == {"required": None} + + +@parametrize +@pytest.mark.asyncio +async def test_union_datetime(use_async: bool) -> None: + dt = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + assert await transform({"union": dt}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "union": "2023-02-23T14:16:36.337692+00:00" + } + + assert await transform({"union": "foo"}, DatetimeDict, use_async) == {"union": "foo"} + + +@parametrize +@pytest.mark.asyncio +async def test_nested_list_iso6801_format(use_async: bool) -> None: + dt1 = datetime.fromisoformat("2023-02-23T14:16:36.337692+00:00") + dt2 = parse_datetime("2022-01-15T06:34:23Z") + assert await transform({"list_": [dt1, dt2]}, DatetimeDict, use_async) == { # type: ignore[comparison-overlap] + "list_": ["2023-02-23T14:16:36.337692+00:00", "2022-01-15T06:34:23+00:00"] + } + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_custom_format(use_async: bool) -> None: + dt = parse_datetime("2022-01-15T06:34:23Z") + + result = await transform(dt, Annotated[datetime, PropertyInfo(format="custom", format_template="%H")], use_async) + assert result == "06" # type: ignore[comparison-overlap] + + +class DateDictWithRequiredAlias(TypedDict, total=False): + required_prop: Required[Annotated[date, PropertyInfo(format="iso8601", alias="prop")]] + + +@parametrize +@pytest.mark.asyncio +async def test_datetime_with_alias(use_async: bool) -> None: + assert await transform({"required_prop": None}, DateDictWithRequiredAlias, use_async) == {"prop": None} # type: ignore[comparison-overlap] + assert await transform( + {"required_prop": date.fromisoformat("2023-02-23")}, DateDictWithRequiredAlias, use_async + ) == {"prop": "2023-02-23"} # type: ignore[comparison-overlap] + + +class MyModel(BaseModel): + foo: str + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_model_to_dictionary(use_async: bool) -> None: + assert cast(Any, await transform(MyModel(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + assert cast(Any, await transform(MyModel.construct(foo="hi!"), Any, use_async)) == {"foo": "hi!"} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_empty_model(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(), Any, use_async)) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_unknown_field(use_async: bool) -> None: + assert cast(Any, await transform(MyModel.construct(my_untyped_field=True), Any, use_async)) == { + "my_untyped_field": True + } + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_types(use_async: bool) -> None: + model = MyModel.construct(foo=True) + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": True} + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_mismatched_object_type(use_async: bool) -> None: + model = MyModel.construct(foo=MyModel.construct(hello="world")) + if PYDANTIC_V1: + params = await transform(model, Any, use_async) + else: + with pytest.warns(UserWarning): + params = await transform(model, Any, use_async) + assert cast(Any, params) == {"foo": {"hello": "world"}} + + +class ModelNestedObjects(BaseModel): + nested: MyModel + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_nested_objects(use_async: bool) -> None: + model = ModelNestedObjects.construct(nested={"foo": "stainless"}) + assert isinstance(model.nested, MyModel) + assert cast(Any, await transform(model, Any, use_async)) == {"nested": {"foo": "stainless"}} + + +class ModelWithDefaultField(BaseModel): + foo: str + with_none_default: Union[str, None] = None + with_str_default: str = "foo" + + +@parametrize +@pytest.mark.asyncio +async def test_pydantic_default_field(use_async: bool) -> None: + # should be excluded when defaults are used + model = ModelWithDefaultField.construct() + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {} + + # should be included when the default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default=None, with_str_default="foo") + assert model.with_none_default is None + assert model.with_str_default == "foo" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": None, "with_str_default": "foo"} + + # should be included when a non-default value is explicitly given + model = ModelWithDefaultField.construct(with_none_default="bar", with_str_default="baz") + assert model.with_none_default == "bar" + assert model.with_str_default == "baz" + assert cast(Any, await transform(model, Any, use_async)) == {"with_none_default": "bar", "with_str_default": "baz"} + + +class TypedDictIterableUnion(TypedDict): + foo: Annotated[Union[Bar8, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +class Bar8(TypedDict): + foo_bar: Annotated[str, PropertyInfo(alias="fooBar")] + + +class Baz8(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_of_dictionaries(use_async: bool) -> None: + assert await transform({"foo": [{"foo_baz": "bar"}]}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "bar"}] + } + assert cast(Any, await transform({"foo": ({"foo_baz": "bar"},)}, TypedDictIterableUnion, use_async)) == { + "FOO": [{"fooBaz": "bar"}] + } + + def my_iter() -> Iterable[Baz8]: + yield {"foo_baz": "hello"} + yield {"foo_baz": "world"} + + assert await transform({"foo": my_iter()}, TypedDictIterableUnion, use_async) == { + "FOO": [{"fooBaz": "hello"}, {"fooBaz": "world"}] + } + + +@parametrize +@pytest.mark.asyncio +async def test_dictionary_items(use_async: bool) -> None: + class DictItems(TypedDict): + foo_baz: Annotated[str, PropertyInfo(alias="fooBaz")] + + assert await transform({"foo": {"foo_baz": "bar"}}, Dict[str, DictItems], use_async) == {"foo": {"fooBaz": "bar"}} + + +class TypedDictIterableUnionStr(TypedDict): + foo: Annotated[Union[str, Iterable[Baz8]], PropertyInfo(alias="FOO")] + + +@parametrize +@pytest.mark.asyncio +async def test_iterable_union_str(use_async: bool) -> None: + assert await transform({"foo": "bar"}, TypedDictIterableUnionStr, use_async) == {"FOO": "bar"} + assert cast(Any, await transform(iter([{"foo_baz": "bar"}]), Union[str, Iterable[Baz8]], use_async)) == [ + {"fooBaz": "bar"} + ] + + +class TypedDictBase64Input(TypedDict): + foo: Annotated[Union[str, Base64FileInput], PropertyInfo(format="base64")] + + +@parametrize +@pytest.mark.asyncio +async def test_base64_file_input(use_async: bool) -> None: + # strings are left as-is + assert await transform({"foo": "bar"}, TypedDictBase64Input, use_async) == {"foo": "bar"} + + # pathlib.Path is automatically converted to base64 + assert await transform({"foo": SAMPLE_FILE_PATH}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQo=" + } # type: ignore[comparison-overlap] + + # io instances are automatically converted to base64 + assert await transform({"foo": io.StringIO("Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + assert await transform({"foo": io.BytesIO(b"Hello, world!")}, TypedDictBase64Input, use_async) == { + "foo": "SGVsbG8sIHdvcmxkIQ==" + } # type: ignore[comparison-overlap] + + +@parametrize +@pytest.mark.asyncio +async def test_transform_skipping(use_async: bool) -> None: + # lists of ints are left as-is + data = [1, 2, 3] + assert await transform(data, List[int], use_async) is data + + # iterables of ints are converted to a list + data = iter([1, 2, 3]) + assert await transform(data, Iterable[int], use_async) == [1, 2, 3] + + +@parametrize +@pytest.mark.asyncio +async def test_strips_notgiven(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": not_given}, Foo1, use_async) == {} + + +@parametrize +@pytest.mark.asyncio +async def test_strips_omit(use_async: bool) -> None: + assert await transform({"foo_bar": "bar"}, Foo1, use_async) == {"fooBar": "bar"} + assert await transform({"foo_bar": omit}, Foo1, use_async) == {} diff --git a/tests/test_utils/test_datetime_parse.py b/tests/test_utils/test_datetime_parse.py new file mode 100644 index 0000000..9eba9ae --- /dev/null +++ b/tests/test_utils/test_datetime_parse.py @@ -0,0 +1,110 @@ +""" +Copied from https://github.com/pydantic/pydantic/blob/v1.10.22/tests/test_datetime_parse.py +with modifications so it works without pydantic v1 imports. +""" + +from typing import Type, Union +from datetime import date, datetime, timezone, timedelta + +import pytest + +from vitable_connect._utils import parse_date, parse_datetime + + +def create_tz(minutes: int) -> timezone: + return timezone(timedelta(minutes=minutes)) + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + ("1494012444.883309", date(2017, 5, 5)), + (b"1494012444.883309", date(2017, 5, 5)), + (1_494_012_444.883_309, date(2017, 5, 5)), + ("1494012444", date(2017, 5, 5)), + (1_494_012_444, date(2017, 5, 5)), + (0, date(1970, 1, 1)), + ("2012-04-23", date(2012, 4, 23)), + (b"2012-04-23", date(2012, 4, 23)), + ("2012-4-9", date(2012, 4, 9)), + (date(2012, 4, 9), date(2012, 4, 9)), + (datetime(2012, 4, 9, 12, 15), date(2012, 4, 9)), + # Invalid inputs + ("x20120423", ValueError), + ("2012-04-56", ValueError), + (19_999_999_999, date(2603, 10, 11)), # just before watershed + (20_000_000_001, date(1970, 8, 20)), # just after watershed + (1_549_316_052, date(2019, 2, 4)), # nowish in s + (1_549_316_052_104, date(2019, 2, 4)), # nowish in ms + (1_549_316_052_104_324, date(2019, 2, 4)), # nowish in μs + (1_549_316_052_104_324_096, date(2019, 2, 4)), # nowish in ns + ("infinity", date(9999, 12, 31)), + ("inf", date(9999, 12, 31)), + (float("inf"), date(9999, 12, 31)), + ("infinity ", date(9999, 12, 31)), + (int("1" + "0" * 100), date(9999, 12, 31)), + (1e1000, date(9999, 12, 31)), + ("-infinity", date(1, 1, 1)), + ("-inf", date(1, 1, 1)), + ("nan", ValueError), + ], +) +def test_date_parsing(value: Union[str, bytes, int, float], result: Union[date, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_date(value) + else: + assert parse_date(value) == result + + +@pytest.mark.parametrize( + "value,result", + [ + # Valid inputs + # values in seconds + ("1494012444.883309", datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + (1_494_012_444.883_309, datetime(2017, 5, 5, 19, 27, 24, 883_309, tzinfo=timezone.utc)), + ("1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (b"1494012444", datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + (1_494_012_444, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + # values in ms + ("1494012444000.883309", datetime(2017, 5, 5, 19, 27, 24, 883, tzinfo=timezone.utc)), + ("-1494012444000.883309", datetime(1922, 8, 29, 4, 32, 35, 999117, tzinfo=timezone.utc)), + (1_494_012_444_000, datetime(2017, 5, 5, 19, 27, 24, tzinfo=timezone.utc)), + ("2012-04-23T09:15:00", datetime(2012, 4, 23, 9, 15)), + ("2012-4-9 4:8:16", datetime(2012, 4, 9, 4, 8, 16)), + ("2012-04-23T09:15:00Z", datetime(2012, 4, 23, 9, 15, 0, 0, timezone.utc)), + ("2012-4-9 4:8:16-0320", datetime(2012, 4, 9, 4, 8, 16, 0, create_tz(-200))), + ("2012-04-23T10:20:30.400+02:30", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(150))), + ("2012-04-23T10:20:30.400+02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(120))), + ("2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (b"2012-04-23T10:20:30.400-02", datetime(2012, 4, 23, 10, 20, 30, 400_000, create_tz(-120))), + (datetime(2017, 5, 5), datetime(2017, 5, 5)), + (0, datetime(1970, 1, 1, 0, 0, 0, tzinfo=timezone.utc)), + # Invalid inputs + ("x20120423091500", ValueError), + ("2012-04-56T09:15:90", ValueError), + ("2012-04-23T11:05:00-25:00", ValueError), + (19_999_999_999, datetime(2603, 10, 11, 11, 33, 19, tzinfo=timezone.utc)), # just before watershed + (20_000_000_001, datetime(1970, 8, 20, 11, 33, 20, 1000, tzinfo=timezone.utc)), # just after watershed + (1_549_316_052, datetime(2019, 2, 4, 21, 34, 12, 0, tzinfo=timezone.utc)), # nowish in s + (1_549_316_052_104, datetime(2019, 2, 4, 21, 34, 12, 104_000, tzinfo=timezone.utc)), # nowish in ms + (1_549_316_052_104_324, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in μs + (1_549_316_052_104_324_096, datetime(2019, 2, 4, 21, 34, 12, 104_324, tzinfo=timezone.utc)), # nowish in ns + ("infinity", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf", datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("inf ", datetime(9999, 12, 31, 23, 59, 59, 999999)), + (1e50, datetime(9999, 12, 31, 23, 59, 59, 999999)), + (float("inf"), datetime(9999, 12, 31, 23, 59, 59, 999999)), + ("-infinity", datetime(1, 1, 1, 0, 0)), + ("-inf", datetime(1, 1, 1, 0, 0)), + ("nan", ValueError), + ], +) +def test_datetime_parsing(value: Union[str, bytes, int, float], result: Union[datetime, Type[Exception]]) -> None: + if type(result) == type and issubclass(result, Exception): # pyright: ignore[reportUnnecessaryIsInstance] + with pytest.raises(result): + parse_datetime(value) + else: + assert parse_datetime(value) == result diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 0000000..1d31652 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from vitable_connect import _compat +from vitable_connect._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py new file mode 100644 index 0000000..49c0cc5 --- /dev/null +++ b/tests/test_utils/test_path.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from vitable_connect._utils._path import path_template + + +@pytest.mark.parametrize( + "template, kwargs, expected", + [ + ("/v1/{id}", dict(id="abc"), "/v1/abc"), + ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"), + ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"), + ("/{w}/{w}", dict(w="echo"), "/echo/echo"), + ("/v1/static", {}, "/v1/static"), + ("", {}, ""), + ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"), + ("/v1/{v}", dict(v=None), "/v1/null"), + ("/v1/{v}", dict(v=True), "/v1/true"), + ("/v1/{v}", dict(v=False), "/v1/false"), + ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok + ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok + ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok + ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok + ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine + ( + "/v1/{a}?query={b}", + dict(a="../../other/endpoint", b="a&bad=true"), + "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue", + ), + ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"), + ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"), + ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"), + ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input + # Query: slash and ? are safe, # is not + ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"), + ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"), + ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"), + ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"), + # Fragment: slash and ? are safe + ("/docs#{v}", dict(v="a/b"), "/docs#a/b"), + ("/docs#{v}", dict(v="a?b"), "/docs#a?b"), + # Path: slash, ? and # are all encoded + ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"), + ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"), + ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"), + # same var encoded differently by component + ( + "/v1/{v}?q={v}#{v}", + dict(v="a/b?c#d"), + "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d", + ), + ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection + ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection + ], +) +def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None: + assert path_template(template, **kwargs) == expected + + +def test_missing_kwarg_raises_key_error() -> None: + with pytest.raises(KeyError, match="org_id"): + path_template("/v1/{org_id}") + + +@pytest.mark.parametrize( + "template, kwargs", + [ + ("{a}/path", dict(a=".")), + ("{a}/path", dict(a="..")), + ("/v1/{a}", dict(a=".")), + ("/v1/{a}", dict(a="..")), + ("/v1/{a}/path", dict(a=".")), + ("/v1/{a}/path", dict(a="..")), + ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".." + ("/v1/{a}.", dict(a=".")), # var + static → ".." + ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "." + ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text + ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static + ("/v1/{v}?q=1", dict(v="..")), + ("/v1/{v}#frag", dict(v="..")), + ], +) +def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None: + with pytest.raises(ValueError, match="dot-segment"): + path_template(template, **kwargs) diff --git a/tests/test_utils/test_proxy.py b/tests/test_utils/test_proxy.py new file mode 100644 index 0000000..219f639 --- /dev/null +++ b/tests/test_utils/test_proxy.py @@ -0,0 +1,34 @@ +import operator +from typing import Any +from typing_extensions import override + +from vitable_connect._utils import LazyProxy + + +class RecursiveLazyProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + return self + + def __call__(self, *_args: Any, **_kwds: Any) -> Any: + raise RuntimeError("This should never be called!") + + +def test_recursive_proxy() -> None: + proxy = RecursiveLazyProxy() + assert repr(proxy) == "RecursiveLazyProxy" + assert str(proxy) == "RecursiveLazyProxy" + assert dir(proxy) == [] + assert type(proxy).__name__ == "RecursiveLazyProxy" + assert type(operator.attrgetter("name.foo.bar.baz")(proxy)).__name__ == "RecursiveLazyProxy" + + +def test_isinstance_does_not_error() -> None: + class AlwaysErrorProxy(LazyProxy[Any]): + @override + def __load__(self) -> Any: + raise RuntimeError("Mocking missing dependency") + + proxy = AlwaysErrorProxy() + assert not isinstance(proxy, dict) + assert isinstance(proxy, LazyProxy) diff --git a/tests/test_utils/test_typing.py b/tests/test_utils/test_typing.py new file mode 100644 index 0000000..e4062bf --- /dev/null +++ b/tests/test_utils/test_typing.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +from typing import Generic, TypeVar, cast + +from vitable_connect._utils import extract_type_var_from_base + +_T = TypeVar("_T") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") + + +class BaseGeneric(Generic[_T]): ... + + +class SubclassGeneric(BaseGeneric[_T]): ... + + +class BaseGenericMultipleTypeArgs(Generic[_T, _T2, _T3]): ... + + +class SubclassGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T, _T2, _T3]): ... + + +class SubclassDifferentOrderGenericMultipleTypeArgs(BaseGenericMultipleTypeArgs[_T2, _T, _T3]): ... + + +def test_extract_type_var() -> None: + assert ( + extract_type_var_from_base( + BaseGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_generic_subclass() -> None: + assert ( + extract_type_var_from_base( + SubclassGeneric[int], + index=0, + generic_bases=cast("tuple[type, ...]", (BaseGeneric,)), + ) + == int + ) + + +def test_extract_type_var_multiple() -> None: + typ = BaseGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_multiple() -> None: + typ = SubclassGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) + + +def test_extract_type_var_generic_subclass_different_ordering_multiple() -> None: + typ = SubclassDifferentOrderGenericMultipleTypeArgs[int, str, None] + + generic_bases = cast("tuple[type, ...]", (BaseGenericMultipleTypeArgs,)) + assert extract_type_var_from_base(typ, index=0, generic_bases=generic_bases) == int + assert extract_type_var_from_base(typ, index=1, generic_bases=generic_bases) == str + assert extract_type_var_from_base(typ, index=2, generic_bases=generic_bases) == type(None) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 0000000..257bcc2 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import os +import inspect +import traceback +import contextlib +from typing import Any, TypeVar, Iterator, Sequence, cast +from datetime import date, datetime +from typing_extensions import Literal, get_args, get_origin, assert_type + +from vitable_connect._types import Omit, NoneType +from vitable_connect._utils import ( + is_dict, + is_list, + is_list_type, + is_union_type, + extract_type_arg, + is_sequence_type, + is_annotated_type, + is_type_alias_type, +) +from vitable_connect._compat import PYDANTIC_V1, field_outer_type, get_model_fields +from vitable_connect._models import BaseModel + +BaseModelT = TypeVar("BaseModelT", bound=BaseModel) + + +def assert_matches_model(model: type[BaseModelT], value: BaseModelT, *, path: list[str]) -> bool: + for name, field in get_model_fields(model).items(): + field_value = getattr(value, name) + if PYDANTIC_V1: + # in v1 nullability was structured differently + # https://docs.pydantic.dev/2.0/migration/#required-optional-and-nullable-fields + allow_none = getattr(field, "allow_none", False) + else: + allow_none = False + + assert_matches_type( + field_outer_type(field), + field_value, + path=[*path, name], + allow_none=allow_none, + ) + + return True + + +# Note: the `path` argument is only used to improve error messages when `--showlocals` is used +def assert_matches_type( + type_: Any, + value: object, + *, + path: list[str], + allow_none: bool = False, +) -> None: + if is_type_alias_type(type_): + type_ = type_.__value__ + + # unwrap `Annotated[T, ...]` -> `T` + if is_annotated_type(type_): + type_ = extract_type_arg(type_, 0) + + if allow_none and value is None: + return + + if type_ is None or type_ is NoneType: + assert value is None + return + + origin = get_origin(type_) or type_ + + if is_list_type(type_): + return _assert_list_type(type_, value) + + if is_sequence_type(type_): + assert isinstance(value, Sequence) + inner_type = get_args(type_)[0] + for entry in value: # type: ignore + assert_type(inner_type, entry) # type: ignore + return + + if origin == str: + assert isinstance(value, str) + elif origin == int: + assert isinstance(value, int) + elif origin == bool: + assert isinstance(value, bool) + elif origin == float: + assert isinstance(value, float) + elif origin == bytes: + assert isinstance(value, bytes) + elif origin == datetime: + assert isinstance(value, datetime) + elif origin == date: + assert isinstance(value, date) + elif origin == object: + # nothing to do here, the expected type is unknown + pass + elif origin == Literal: + assert value in get_args(type_) + elif origin == dict: + assert is_dict(value) + + args = get_args(type_) + key_type = args[0] + items_type = args[1] + + for key, item in value.items(): + assert_matches_type(key_type, key, path=[*path, ""]) + assert_matches_type(items_type, item, path=[*path, ""]) + elif is_union_type(type_): + variants = get_args(type_) + + try: + none_index = variants.index(type(None)) + except ValueError: + pass + else: + # special case Optional[T] for better error messages + if len(variants) == 2: + if value is None: + # valid + return + + return assert_matches_type(type_=variants[not none_index], value=value, path=path) + + for i, variant in enumerate(variants): + try: + assert_matches_type(variant, value, path=[*path, f"variant {i}"]) + return + except AssertionError: + traceback.print_exc() + continue + + raise AssertionError("Did not match any variants") + elif issubclass(origin, BaseModel): + assert isinstance(value, type_) + assert assert_matches_model(type_, cast(Any, value), path=path) + elif inspect.isclass(origin) and origin.__name__ == "HttpxBinaryResponseContent": + assert value.__class__.__name__ == "HttpxBinaryResponseContent" + else: + assert None, f"Unhandled field type: {type_}" + + +def _assert_list_type(type_: type[object], value: object) -> None: + assert is_list(value) + + inner_type = get_args(type_)[0] + for entry in value: + assert_type(inner_type, entry) # type: ignore + + +@contextlib.contextmanager +def update_env(**new_env: str | Omit) -> Iterator[None]: + old = os.environ.copy() + + try: + for name, value in new_env.items(): + if isinstance(value, Omit): + os.environ.pop(name, None) + else: + os.environ[name] = value + + yield None + finally: + os.environ.clear() + os.environ.update(old) diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py deleted file mode 100644 index f3ea265..0000000 --- a/tests/utils/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - diff --git a/tests/utils/assets/models/__init__.py b/tests/utils/assets/models/__init__.py deleted file mode 100644 index 2cf0126..0000000 --- a/tests/utils/assets/models/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# This file was auto-generated by Fern from our API Definition. - -from .circle import CircleParams -from .object_with_defaults import ObjectWithDefaultsParams -from .object_with_optional_field import ObjectWithOptionalFieldParams -from .shape import Shape_CircleParams, Shape_SquareParams, ShapeParams -from .square import SquareParams -from .undiscriminated_shape import UndiscriminatedShapeParams - -__all__ = [ - "CircleParams", - "ObjectWithDefaultsParams", - "ObjectWithOptionalFieldParams", - "ShapeParams", - "Shape_CircleParams", - "Shape_SquareParams", - "SquareParams", - "UndiscriminatedShapeParams", -] diff --git a/tests/utils/assets/models/circle.py b/tests/utils/assets/models/circle.py deleted file mode 100644 index 304097c..0000000 --- a/tests/utils/assets/models/circle.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# This file was auto-generated by Fern from our API Definition. - -import typing_extensions - -from vitable_connect.core.serialization import FieldMetadata - - -class CircleParams(typing_extensions.TypedDict): - radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] diff --git a/tests/utils/assets/models/color.py b/tests/utils/assets/models/color.py deleted file mode 100644 index 2aa2c4c..0000000 --- a/tests/utils/assets/models/color.py +++ /dev/null @@ -1,7 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# This file was auto-generated by Fern from our API Definition. - -import typing - -Color = typing.Union[typing.Literal["red", "blue"], typing.Any] diff --git a/tests/utils/assets/models/object_with_defaults.py b/tests/utils/assets/models/object_with_defaults.py deleted file mode 100644 index a977b1d..0000000 --- a/tests/utils/assets/models/object_with_defaults.py +++ /dev/null @@ -1,15 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# This file was auto-generated by Fern from our API Definition. - -import typing_extensions - - -class ObjectWithDefaultsParams(typing_extensions.TypedDict): - """ - Defines properties with default values and validation rules. - """ - - decimal: typing_extensions.NotRequired[float] - string: typing_extensions.NotRequired[str] - required_string: str diff --git a/tests/utils/assets/models/object_with_optional_field.py b/tests/utils/assets/models/object_with_optional_field.py deleted file mode 100644 index 3b46896..0000000 --- a/tests/utils/assets/models/object_with_optional_field.py +++ /dev/null @@ -1,35 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# This file was auto-generated by Fern from our API Definition. - -import datetime as dt -import typing -import uuid - -import typing_extensions -from .color import Color -from .shape import ShapeParams -from .undiscriminated_shape import UndiscriminatedShapeParams - -from vitable_connect.core.serialization import FieldMetadata - - -class ObjectWithOptionalFieldParams(typing_extensions.TypedDict): - literal: typing.Literal["lit_one"] - string: typing_extensions.NotRequired[str] - integer: typing_extensions.NotRequired[int] - long_: typing_extensions.NotRequired[typing_extensions.Annotated[int, FieldMetadata(alias="long")]] - double: typing_extensions.NotRequired[float] - bool_: typing_extensions.NotRequired[typing_extensions.Annotated[bool, FieldMetadata(alias="bool")]] - datetime: typing_extensions.NotRequired[dt.datetime] - date: typing_extensions.NotRequired[dt.date] - uuid_: typing_extensions.NotRequired[typing_extensions.Annotated[uuid.UUID, FieldMetadata(alias="uuid")]] - base_64: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="base64")]] - list_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Sequence[str], FieldMetadata(alias="list")]] - set_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Set[str], FieldMetadata(alias="set")]] - map_: typing_extensions.NotRequired[typing_extensions.Annotated[typing.Dict[int, str], FieldMetadata(alias="map")]] - enum: typing_extensions.NotRequired[Color] - union: typing_extensions.NotRequired[ShapeParams] - second_union: typing_extensions.NotRequired[ShapeParams] - undiscriminated_union: typing_extensions.NotRequired[UndiscriminatedShapeParams] - any: typing.Optional[typing.Any] diff --git a/tests/utils/assets/models/shape.py b/tests/utils/assets/models/shape.py deleted file mode 100644 index c9f4d98..0000000 --- a/tests/utils/assets/models/shape.py +++ /dev/null @@ -1,28 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# This file was auto-generated by Fern from our API Definition. - -from __future__ import annotations - -import typing - -import typing_extensions - -from vitable_connect.core.serialization import FieldMetadata - - -class Base(typing_extensions.TypedDict): - id: str - - -class Shape_CircleParams(Base): - shape_type: typing_extensions.Annotated[typing.Literal["circle"], FieldMetadata(alias="shapeType")] - radius_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="radiusMeasurement")] - - -class Shape_SquareParams(Base): - shape_type: typing_extensions.Annotated[typing.Literal["square"], FieldMetadata(alias="shapeType")] - length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] - - -ShapeParams = typing.Union[Shape_CircleParams, Shape_SquareParams] diff --git a/tests/utils/assets/models/square.py b/tests/utils/assets/models/square.py deleted file mode 100644 index 21e1406..0000000 --- a/tests/utils/assets/models/square.py +++ /dev/null @@ -1,11 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# This file was auto-generated by Fern from our API Definition. - -import typing_extensions - -from vitable_connect.core.serialization import FieldMetadata - - -class SquareParams(typing_extensions.TypedDict): - length_measurement: typing_extensions.Annotated[float, FieldMetadata(alias="lengthMeasurement")] diff --git a/tests/utils/assets/models/undiscriminated_shape.py b/tests/utils/assets/models/undiscriminated_shape.py deleted file mode 100644 index 99f12b3..0000000 --- a/tests/utils/assets/models/undiscriminated_shape.py +++ /dev/null @@ -1,10 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -# This file was auto-generated by Fern from our API Definition. - -import typing - -from .circle import CircleParams -from .square import SquareParams - -UndiscriminatedShapeParams = typing.Union[CircleParams, SquareParams] diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py deleted file mode 100644 index 64936e3..0000000 --- a/tests/utils/test_http_client.py +++ /dev/null @@ -1,853 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Any, Dict, Tuple, cast -from unittest.mock import AsyncMock, MagicMock, patch - -import httpx -import pytest - -from vitable_connect.core.http_client import ( - AsyncHttpClient, - HttpClient, - _build_url, - _should_retry, - drop_content_type_without_body, - get_request_body, - remove_none_from_dict, -) -from vitable_connect.core.request_options import RequestOptions - - -# Stub clients for testing HttpClient and AsyncHttpClient -class _DummySyncClient: - """A minimal stub for httpx.Client that records request arguments.""" - - def __init__(self) -> None: - self.last_request_kwargs: Dict[str, Any] = {} - - def request(self, **kwargs: Any) -> "_DummyResponse": - self.last_request_kwargs = kwargs - return _DummyResponse() - - -class _DummyAsyncClient: - """A minimal stub for httpx.AsyncClient that records request arguments.""" - - def __init__(self) -> None: - self.last_request_kwargs: Dict[str, Any] = {} - - async def request(self, **kwargs: Any) -> "_DummyResponse": - self.last_request_kwargs = kwargs - return _DummyResponse() - - -class _DummyResponse: - """A minimal stub for httpx.Response.""" - - status_code = 200 - headers: Dict[str, str] = {} - - -def get_request_options() -> RequestOptions: - return {"additional_body_parameters": {"see you": "later"}} - - -def get_request_options_with_none() -> RequestOptions: - return {"additional_body_parameters": {"see you": "later", "optional": None}} - - -def test_get_json_request_body() -> None: - json_body, data_body = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=None) - assert json_body == {"hello": "world"} - assert data_body is None - - json_body_extras, data_body_extras = get_request_body( - json={"goodbye": "world"}, data=None, request_options=get_request_options(), omit=None - ) - - assert json_body_extras == {"goodbye": "world", "see you": "later"} - assert data_body_extras is None - - -def test_get_files_request_body() -> None: - json_body, data_body = get_request_body(json=None, data={"hello": "world"}, request_options=None, omit=None) - assert data_body == {"hello": "world"} - assert json_body is None - - json_body_extras, data_body_extras = get_request_body( - json=None, data={"goodbye": "world"}, request_options=get_request_options(), omit=None - ) - - assert data_body_extras == {"goodbye": "world", "see you": "later"} - assert json_body_extras is None - - -def test_get_none_request_body() -> None: - json_body, data_body = get_request_body(json=None, data=None, request_options=None, omit=None) - assert data_body is None - assert json_body is None - - json_body_extras, data_body_extras = get_request_body( - json=None, data=None, request_options=get_request_options(), omit=None - ) - - assert json_body_extras == {"see you": "later"} - assert data_body_extras is None - - -def test_get_empty_json_request_body() -> None: - """Test that implicit empty bodies (json=None) are collapsed to None.""" - unrelated_request_options: RequestOptions = {"max_retries": 3} - json_body, data_body = get_request_body(json=None, data=None, request_options=unrelated_request_options, omit=None) - assert json_body is None - assert data_body is None - - -def test_explicit_empty_json_body_is_preserved() -> None: - """Test that explicit empty bodies (json={}) are preserved and sent as {}. - - This is important for endpoints where the request body is required but all - fields are optional. The server expects valid JSON ({}) not an empty body. - """ - unrelated_request_options: RequestOptions = {"max_retries": 3} - - # Explicit json={} should be preserved - json_body, data_body = get_request_body(json={}, data=None, request_options=unrelated_request_options, omit=None) - assert json_body == {} - assert data_body is None - - # Explicit data={} should also be preserved - json_body2, data_body2 = get_request_body(json=None, data={}, request_options=unrelated_request_options, omit=None) - assert json_body2 is None - assert data_body2 == {} - - -def test_omitted_body_sends_no_content() -> None: - """A body left at the sentinel was never passed, so nothing is sent. - - This is how an endpoint whose body may be omitted stays bodyless, as opposed to - sending an empty ``{}``. An explicit ``None`` still reaches the wire as ``null``. - """ - omit = cast(Any, ...) - unrelated_request_options: RequestOptions = {"max_retries": 3} - - json_body, data_body = get_request_body(json=omit, data=None, request_options=unrelated_request_options, omit=omit) - assert json_body is None - assert data_body is None - - json_body2, data_body2 = get_request_body( - json=None, data=omit, request_options=unrelated_request_options, omit=omit - ) - assert json_body2 is None - assert data_body2 is None - - # an explicitly passed body is untouched - json_body3, _ = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=omit) - assert json_body3 == {"hello": "world"} - - -def test_optional_body_sends_no_content_when_every_property_is_omitted() -> None: - """An endpoint that inlines an omittable body sends nothing once every property is omitted. - - The body reaches the client as a dict of sentinels rather than as a single argument, so - ``optional_body`` is what tells the client that an empty result means "no body at all". - """ - omit = cast(Any, ...) - - json_body, data_body = get_request_body( - json={"amount": omit, "source": omit}, data=None, request_options=None, omit=omit, optional_body=True - ) - assert json_body is None - assert data_body is None - - # a body the API requires still goes out as `{}` - required_json_body, _ = get_request_body( - json={"amount": omit, "source": omit}, data=None, request_options=None, omit=omit - ) - assert required_json_body == {} - - # a property the caller did pass keeps the body - populated_json_body, _ = get_request_body( - json={"amount": 1, "source": omit}, data=None, request_options=None, omit=omit, optional_body=True - ) - assert populated_json_body == {"amount": 1} - - # additional body parameters keep the body, since the caller asked for them - with_additional_body_parameters, _ = get_request_body( - json={"amount": omit}, - data=None, - request_options={"additional_body_parameters": {"custom": "value"}}, - omit=omit, - optional_body=True, - ) - assert with_additional_body_parameters == {"custom": "value"} - - -def test_json_body_preserves_none_values() -> None: - """Test that JSON bodies preserve None values (they become JSON null).""" - json_body, data_body = get_request_body( - json={"hello": "world", "optional": None}, data=None, request_options=None, omit=None - ) - # JSON bodies should preserve None values - assert json_body == {"hello": "world", "optional": None} - assert data_body is None - - -def test_data_body_preserves_none_values_without_multipart() -> None: - """Test that data bodies preserve None values when not using multipart. - - The filtering of None values happens in HttpClient.request/stream methods, - not in get_request_body. This test verifies get_request_body doesn't filter None. - """ - json_body, data_body = get_request_body( - json=None, data={"hello": "world", "optional": None}, request_options=None, omit=None - ) - # get_request_body should preserve None values in data body - # The filtering happens later in HttpClient.request when multipart is detected - assert data_body == {"hello": "world", "optional": None} - assert json_body is None - - -def test_remove_none_from_dict_filters_none_values() -> None: - """Test that remove_none_from_dict correctly filters out None values.""" - original = {"hello": "world", "optional": None, "another": "value", "also_none": None} - filtered = remove_none_from_dict(original) - assert filtered == {"hello": "world", "another": "value"} - # Original should not be modified - assert original == {"hello": "world", "optional": None, "another": "value", "also_none": None} - - -def test_remove_none_from_dict_empty_dict() -> None: - """Test that remove_none_from_dict handles empty dict.""" - assert remove_none_from_dict({}) == {} - - -def test_remove_none_from_dict_all_none() -> None: - """Test that remove_none_from_dict handles dict with all None values.""" - assert remove_none_from_dict({"a": None, "b": None}) == {} - - -def test_http_client_does_not_pass_empty_params_list() -> None: - """Test that HttpClient passes params=None when params are empty. - - This prevents httpx from stripping existing query parameters from the URL, - which happens when params=[] or params={} is passed. - """ - dummy_client = _DummySyncClient() - http_client = HttpClient( - httpx_client=dummy_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - ) - - # Use a path with query params (e.g., pagination cursor URL) - http_client.request( - path="resource?after=123", - method="GET", - params=None, - request_options=None, - ) - - # We care that httpx receives params=None, not [] or {} - assert "params" in dummy_client.last_request_kwargs - assert dummy_client.last_request_kwargs["params"] is None - - # Verify the query string in the URL is preserved - url = str(dummy_client.last_request_kwargs["url"]) - assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" - - -def test_http_client_passes_encoded_params_when_present() -> None: - """Test that HttpClient passes encoded params when params are provided.""" - dummy_client = _DummySyncClient() - http_client = HttpClient( - httpx_client=dummy_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com/resource", - ) - - http_client.request( - path="", - method="GET", - params={"after": "456"}, - request_options=None, - ) - - params = dummy_client.last_request_kwargs["params"] - # For a simple dict, encode_query should give a single (key, value) tuple - assert params == [("after", "456")] - - -@pytest.mark.asyncio -async def test_async_http_client_does_not_pass_empty_params_list() -> None: - """Test that AsyncHttpClient passes params=None when params are empty. - - This prevents httpx from stripping existing query parameters from the URL, - which happens when params=[] or params={} is passed. - """ - dummy_client = _DummyAsyncClient() - http_client = AsyncHttpClient( - httpx_client=dummy_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - async_base_headers=None, - ) - - # Use a path with query params (e.g., pagination cursor URL) - await http_client.request( - path="resource?after=123", - method="GET", - params=None, - request_options=None, - ) - - # We care that httpx receives params=None, not [] or {} - assert "params" in dummy_client.last_request_kwargs - assert dummy_client.last_request_kwargs["params"] is None - - # Verify the query string in the URL is preserved - url = str(dummy_client.last_request_kwargs["url"]) - assert "after=123" in url, f"Expected query param 'after=123' in URL, got: {url}" - - -@pytest.mark.asyncio -async def test_async_http_client_passes_encoded_params_when_present() -> None: - """Test that AsyncHttpClient passes encoded params when params are provided.""" - dummy_client = _DummyAsyncClient() - http_client = AsyncHttpClient( - httpx_client=dummy_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com/resource", - async_base_headers=None, - ) - - await http_client.request( - path="", - method="GET", - params={"after": "456"}, - request_options=None, - ) - - params = dummy_client.last_request_kwargs["params"] - # For a simple dict, encode_query should give a single (key, value) tuple - assert params == [("after", "456")] - - -def test_basic_url_joining() -> None: - """Test basic URL joining with a simple base URL and path.""" - result = _build_url("https://api.example.com", "/users") - assert result == "https://api.example.com/users" - - -def test_basic_url_joining_trailing_slash() -> None: - """Test basic URL joining with a simple base URL and path.""" - result = _build_url("https://api.example.com/", "/users") - assert result == "https://api.example.com/users" - - -def test_preserves_base_url_path_prefix() -> None: - """Test that path prefixes in base URL are preserved. - - This is the critical bug fix - urllib.parse.urljoin() would strip - the path prefix when the path starts with '/'. - """ - result = _build_url("https://cloud.example.com/org/tenant/api", "/users") - assert result == "https://cloud.example.com/org/tenant/api/users" - - -def test_preserves_base_url_path_prefix_trailing_slash() -> None: - """Test that path prefixes in base URL are preserved.""" - result = _build_url("https://cloud.example.com/org/tenant/api/", "/users") - assert result == "https://cloud.example.com/org/tenant/api/users" - - -# --------------------------------------------------------------------------- -# Connection error retry tests -# --------------------------------------------------------------------------- - - -def _make_sync_http_client(mock_client: Any) -> HttpClient: - return HttpClient( - httpx_client=mock_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - ) - - -def _make_async_http_client(mock_client: Any) -> AsyncHttpClient: - return AsyncHttpClient( - httpx_client=mock_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - async_base_headers=None, - ) - - -@patch("vitable_connect.core.http_client.time.sleep", return_value=None) -def test_sync_retries_on_connect_error(mock_sleep: MagicMock) -> None: - """Sync: connection error retries on httpx.ConnectError.""" - mock_client = MagicMock() - mock_client.request.side_effect = [ - httpx.ConnectError("connection failed"), - _DummyResponse(), - ] - http_client = _make_sync_http_client(mock_client) - - response = http_client.request(path="/test", method="GET") - - assert response.status_code == 200 - assert mock_client.request.call_count == 2 - mock_sleep.assert_called_once() - - -@patch("vitable_connect.core.http_client.time.sleep", return_value=None) -def test_sync_retries_on_remote_protocol_error(mock_sleep: MagicMock) -> None: - """Sync: connection error retries on httpx.RemoteProtocolError.""" - mock_client = MagicMock() - mock_client.request.side_effect = [ - httpx.RemoteProtocolError("Remote end closed connection without response"), - _DummyResponse(), - ] - http_client = _make_sync_http_client(mock_client) - - response = http_client.request(path="/test", method="GET") - - assert response.status_code == 200 - assert mock_client.request.call_count == 2 - mock_sleep.assert_called_once() - - -@patch("vitable_connect.core.http_client.time.sleep", return_value=None) -def test_sync_connection_error_exhausts_retries(mock_sleep: MagicMock) -> None: - """Sync: connection error exhausts retries then raises.""" - mock_client = MagicMock() - mock_client.request.side_effect = httpx.ConnectError("connection failed") - http_client = _make_sync_http_client(mock_client) - - with pytest.raises(httpx.ConnectError): - http_client.request( - path="/test", - method="GET", - request_options={"max_retries": 2}, - ) - - # 1 initial + 2 retries = 3 total attempts - assert mock_client.request.call_count == 3 - assert mock_sleep.call_count == 2 - - -@patch("vitable_connect.core.http_client.time.sleep", return_value=None) -def test_sync_connection_error_respects_max_retries_zero(mock_sleep: MagicMock) -> None: - """Sync: connection error respects max_retries=0.""" - mock_client = MagicMock() - mock_client.request.side_effect = httpx.ConnectError("connection failed") - http_client = _make_sync_http_client(mock_client) - - with pytest.raises(httpx.ConnectError): - http_client.request( - path="/test", - method="GET", - request_options={"max_retries": 0}, - ) - - # No retries, just the initial attempt - assert mock_client.request.call_count == 1 - mock_sleep.assert_not_called() - - -@pytest.mark.asyncio -@patch("vitable_connect.core.http_client.asyncio.sleep", new_callable=AsyncMock) -async def test_async_retries_on_connect_error(mock_sleep: AsyncMock) -> None: - """Async: connection error retries on httpx.ConnectError.""" - mock_client = MagicMock() - mock_client.request = AsyncMock( - side_effect=[ - httpx.ConnectError("connection failed"), - _DummyResponse(), - ] - ) - http_client = _make_async_http_client(mock_client) - - response = await http_client.request(path="/test", method="GET") - - assert response.status_code == 200 - assert mock_client.request.call_count == 2 - mock_sleep.assert_called_once() - - -@pytest.mark.asyncio -@patch("vitable_connect.core.http_client.asyncio.sleep", new_callable=AsyncMock) -async def test_async_retries_on_remote_protocol_error(mock_sleep: AsyncMock) -> None: - """Async: connection error retries on httpx.RemoteProtocolError.""" - mock_client = MagicMock() - mock_client.request = AsyncMock( - side_effect=[ - httpx.RemoteProtocolError("Remote end closed connection without response"), - _DummyResponse(), - ] - ) - http_client = _make_async_http_client(mock_client) - - response = await http_client.request(path="/test", method="GET") - - assert response.status_code == 200 - assert mock_client.request.call_count == 2 - mock_sleep.assert_called_once() - - -@pytest.mark.asyncio -@patch("vitable_connect.core.http_client.asyncio.sleep", new_callable=AsyncMock) -async def test_async_connection_error_exhausts_retries(mock_sleep: AsyncMock) -> None: - """Async: connection error exhausts retries then raises.""" - mock_client = MagicMock() - mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed")) - http_client = _make_async_http_client(mock_client) - - with pytest.raises(httpx.ConnectError): - await http_client.request( - path="/test", - method="GET", - request_options={"max_retries": 2}, - ) - - # 1 initial + 2 retries = 3 total attempts - assert mock_client.request.call_count == 3 - assert mock_sleep.call_count == 2 - - -# --------------------------------------------------------------------------- -# base_max_retries constructor parameter tests -# --------------------------------------------------------------------------- - - -def test_sync_http_client_default_base_max_retries() -> None: - """HttpClient defaults to base_max_retries=2.""" - http_client = HttpClient( - httpx_client=MagicMock(), # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - ) - assert http_client.base_max_retries == 2 - - -def test_async_http_client_default_base_max_retries() -> None: - """AsyncHttpClient defaults to base_max_retries=2.""" - http_client = AsyncHttpClient( - httpx_client=MagicMock(), # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - ) - assert http_client.base_max_retries == 2 - - -def test_sync_http_client_custom_base_max_retries() -> None: - """HttpClient accepts a custom base_max_retries value.""" - http_client = HttpClient( - httpx_client=MagicMock(), # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_max_retries=5, - ) - assert http_client.base_max_retries == 5 - - -def test_async_http_client_custom_base_max_retries() -> None: - """AsyncHttpClient accepts a custom base_max_retries value.""" - http_client = AsyncHttpClient( - httpx_client=MagicMock(), # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_max_retries=5, - ) - assert http_client.base_max_retries == 5 - - -@patch("vitable_connect.core.http_client.time.sleep", return_value=None) -def test_sync_base_max_retries_zero_disables_retries(mock_sleep: MagicMock) -> None: - """Sync: base_max_retries=0 disables retries when no request_options override.""" - mock_client = MagicMock() - mock_client.request.side_effect = httpx.ConnectError("connection failed") - http_client = HttpClient( - httpx_client=mock_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - base_max_retries=0, - ) - - with pytest.raises(httpx.ConnectError): - http_client.request(path="/test", method="GET") - - # No retries, just the initial attempt - assert mock_client.request.call_count == 1 - mock_sleep.assert_not_called() - - -@pytest.mark.asyncio -@patch("vitable_connect.core.http_client.asyncio.sleep", new_callable=AsyncMock) -async def test_async_base_max_retries_zero_disables_retries(mock_sleep: AsyncMock) -> None: - """Async: base_max_retries=0 disables retries when no request_options override.""" - mock_client = MagicMock() - mock_client.request = AsyncMock(side_effect=httpx.ConnectError("connection failed")) - http_client = AsyncHttpClient( - httpx_client=mock_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - base_max_retries=0, - ) - - with pytest.raises(httpx.ConnectError): - await http_client.request(path="/test", method="GET") - - # No retries, just the initial attempt - assert mock_client.request.call_count == 1 - mock_sleep.assert_not_called() - - -@patch("vitable_connect.core.http_client.time.sleep", return_value=None) -def test_sync_request_options_override_base_max_retries(mock_sleep: MagicMock) -> None: - """Sync: request_options max_retries overrides base_max_retries.""" - mock_client = MagicMock() - mock_client.request.side_effect = [ - httpx.ConnectError("connection failed"), - httpx.ConnectError("connection failed"), - _DummyResponse(), - ] - http_client = HttpClient( - httpx_client=mock_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - base_max_retries=0, # base says no retries - ) - - # But request_options overrides to allow 2 retries - response = http_client.request( - path="/test", - method="GET", - request_options={"max_retries": 2}, - ) - - assert response.status_code == 200 - # 1 initial + 2 retries = 3 total attempts - assert mock_client.request.call_count == 3 - - -@pytest.mark.asyncio -@patch("vitable_connect.core.http_client.asyncio.sleep", new_callable=AsyncMock) -async def test_async_request_options_override_base_max_retries(mock_sleep: AsyncMock) -> None: - """Async: request_options max_retries overrides base_max_retries.""" - mock_client = MagicMock() - mock_client.request = AsyncMock( - side_effect=[ - httpx.ConnectError("connection failed"), - httpx.ConnectError("connection failed"), - _DummyResponse(), - ] - ) - http_client = AsyncHttpClient( - httpx_client=mock_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - base_max_retries=0, # base says no retries - ) - - # But request_options overrides to allow 2 retries - response = await http_client.request( - path="/test", - method="GET", - request_options={"max_retries": 2}, - ) - - assert response.status_code == 200 - # 1 initial + 2 retries = 3 total attempts - assert mock_client.request.call_count == 3 - - -@patch("vitable_connect.core.http_client.time.sleep", return_value=None) -def test_sync_base_max_retries_used_as_default(mock_sleep: MagicMock) -> None: - """Sync: base_max_retries is used when request_options has no max_retries.""" - mock_client = MagicMock() - mock_client.request.side_effect = [ - httpx.ConnectError("fail"), - httpx.ConnectError("fail"), - httpx.ConnectError("fail"), - _DummyResponse(), - ] - http_client = HttpClient( - httpx_client=mock_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - base_max_retries=3, - ) - - response = http_client.request(path="/test", method="GET") - - assert response.status_code == 200 - # 1 initial + 3 retries = 4 total attempts - assert mock_client.request.call_count == 4 - - -@pytest.mark.asyncio -@patch("vitable_connect.core.http_client.asyncio.sleep", new_callable=AsyncMock) -async def test_async_base_max_retries_used_as_default(mock_sleep: AsyncMock) -> None: - """Async: base_max_retries is used when request_options has no max_retries.""" - mock_client = MagicMock() - mock_client.request = AsyncMock( - side_effect=[ - httpx.ConnectError("fail"), - httpx.ConnectError("fail"), - httpx.ConnectError("fail"), - _DummyResponse(), - ] - ) - http_client = AsyncHttpClient( - httpx_client=mock_client, # type: ignore[arg-type] - base_timeout=lambda: None, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - base_max_retries=3, - ) - - response = await http_client.request(path="/test", method="GET") - - assert response.status_code == 200 - # 1 initial + 3 retries = 4 total attempts - assert mock_client.request.call_count == 4 - - -# --------------------------------------------------------------------------- -# _should_retry unit tests -# --------------------------------------------------------------------------- - - -def _make_response(status_code: int) -> httpx.Response: - return httpx.Response(status_code=status_code, content=b"") - - -@pytest.mark.parametrize( - "status_code", - [408, 409, 429, 500, 501, 502, 503, 504, 599], -) -def test_should_retry_retryable_status_codes(status_code: int) -> None: - """Legacy mode: retries on 408, 409, 429, and all >= 500.""" - assert _should_retry(_make_response(status_code)) is True - - -@pytest.mark.parametrize( - "status_code", - [200, 201, 301, 400, 401, 403, 404], -) -def test_should_not_retry_non_retryable_status_codes(status_code: int) -> None: - assert _should_retry(_make_response(status_code)) is False - - -def test_should_retry_599_upper_boundary() -> None: - """Legacy mode retries on >= 500, which includes 599.""" - assert _should_retry(_make_response(599)) is True - - -# --------------------------------------------------------------------------- -# RequestOptions timeout resolution tests (timeout / deprecated timeout_in_seconds) -# --------------------------------------------------------------------------- - - -def _sync_client_with_base_timeout(base_timeout: Any) -> Tuple[HttpClient, _DummySyncClient]: - dummy_client = _DummySyncClient() - http_client = HttpClient( - httpx_client=dummy_client, # type: ignore[arg-type] - base_timeout=lambda: base_timeout, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - ) - return http_client, dummy_client - - -def test_sync_request_options_timeout_used() -> None: - """The new `timeout` request option is passed through to httpx (in seconds).""" - http_client, dummy_client = _sync_client_with_base_timeout(60) - http_client.request(path="/test", method="GET", request_options={"timeout": 30}) - assert dummy_client.last_request_kwargs["timeout"] == 30 - - -def test_sync_request_options_timeout_in_seconds_still_works() -> None: - """The deprecated `timeout_in_seconds` request option remains backwards compatible.""" - http_client, dummy_client = _sync_client_with_base_timeout(60) - http_client.request(path="/test", method="GET", request_options={"timeout_in_seconds": 45}) - assert dummy_client.last_request_kwargs["timeout"] == 45 - - -def test_sync_request_options_timeout_takes_precedence() -> None: - """When both are set, `timeout` wins over `timeout_in_seconds` (same seconds unit).""" - http_client, dummy_client = _sync_client_with_base_timeout(60) - http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45}) - assert dummy_client.last_request_kwargs["timeout"] == 30 - - -def test_sync_request_options_timeout_falls_back_to_base() -> None: - """When neither key is set, the client-level base timeout is used.""" - http_client, dummy_client = _sync_client_with_base_timeout(60) - http_client.request(path="/test", method="GET", request_options=None) - assert dummy_client.last_request_kwargs["timeout"] == 60 - - -def test_sync_request_options_timeout_none_falls_back_to_deprecated() -> None: - """An explicit `timeout=None` (dynamic caller) falls back to the deprecated `timeout_in_seconds`.""" - http_client, dummy_client = _sync_client_with_base_timeout(60) - request_options = cast(RequestOptions, {"timeout": None, "timeout_in_seconds": 45}) - http_client.request(path="/test", method="GET", request_options=request_options) - assert dummy_client.last_request_kwargs["timeout"] == 45 - - -@pytest.mark.asyncio -async def test_async_request_options_timeout_takes_precedence() -> None: - """Async: `timeout` wins over the deprecated `timeout_in_seconds`.""" - dummy_client = _DummyAsyncClient() - http_client = AsyncHttpClient( - httpx_client=dummy_client, # type: ignore[arg-type] - base_timeout=lambda: 60, - base_headers=lambda: {}, - base_url=lambda: "https://example.com", - async_base_headers=None, - ) - await http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45}) - assert dummy_client.last_request_kwargs["timeout"] == 30 - - -def test_drop_content_type_without_body_omits_header_for_bodyless_optional_call() -> None: - """An optional-body endpoint the caller left empty must not advertise a media type. - - `get_request_body` drops the body, but the endpoint still hands over the content type it - would have used, so a server that branches on the header would see a JSON request carrying - nothing at all. - """ - headers = {"content-type": "application/json", "authorization": "Bearer x"} - - assert drop_content_type_without_body(headers, json_body=None, data_body=None, optional_body=True) == { - "authorization": "Bearer x" - } - - -def test_drop_content_type_without_body_keeps_header_when_a_body_is_sent() -> None: - headers = {"content-type": "application/json"} - - assert ( - drop_content_type_without_body(headers, json_body={"amount": 60}, data_body=None, optional_body=True) == headers - ) - assert drop_content_type_without_body(headers, json_body=None, data_body="raw", optional_body=True) == headers - - -def test_drop_content_type_without_body_leaves_required_body_endpoints_alone() -> None: - """Without the opt-in, an endpoint keeps the header it has always sent.""" - headers = {"Content-Type": "application/json"} - - assert drop_content_type_without_body(headers, json_body=None, data_body=None, optional_body=False) == headers diff --git a/tests/utils/test_query_encoding.py b/tests/utils/test_query_encoding.py deleted file mode 100644 index 7bf392e..0000000 --- a/tests/utils/test_query_encoding.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from vitable_connect.core.query_encoder import encode_query - - -def test_query_encoding_deep_objects() -> None: - assert encode_query({"hello world": "hello world"}) == [("hello world", "hello world")] - assert encode_query({"hello_world": {"hello": "world"}}) == [("hello_world[hello]", "world")] - assert encode_query({"hello_world": {"hello": {"world": "today"}, "test": "this"}, "hi": "there"}) == [ - ("hello_world[hello][world]", "today"), - ("hello_world[test]", "this"), - ("hi", "there"), - ] - - -def test_query_encoding_deep_object_arrays() -> None: - assert encode_query({"objects": [{"key": "hello", "value": "world"}, {"key": "foo", "value": "bar"}]}) == [ - ("objects[key]", "hello"), - ("objects[value]", "world"), - ("objects[key]", "foo"), - ("objects[value]", "bar"), - ] - assert encode_query( - {"users": [{"name": "string", "tags": ["string"]}, {"name": "string2", "tags": ["string2", "string3"]}]} - ) == [ - ("users[name]", "string"), - ("users[tags]", "string"), - ("users[name]", "string2"), - ("users[tags]", "string2"), - ("users[tags]", "string3"), - ] - - -def test_encode_query_with_none() -> None: - encoded = encode_query(None) - assert encoded is None diff --git a/tests/utils/test_serialization.py b/tests/utils/test_serialization.py deleted file mode 100644 index 9d332ea..0000000 --- a/tests/utils/test_serialization.py +++ /dev/null @@ -1,72 +0,0 @@ -# This file was auto-generated by Fern from our API Definition. - -from typing import Any, List - -from .assets.models import ObjectWithOptionalFieldParams, ShapeParams - -from vitable_connect.core.serialization import convert_and_respect_annotation_metadata - -UNION_TEST: ShapeParams = {"radius_measurement": 1.0, "shape_type": "circle", "id": "1"} -UNION_TEST_CONVERTED = {"shapeType": "circle", "radiusMeasurement": 1.0, "id": "1"} - - -def test_convert_and_respect_annotation_metadata() -> None: - data: ObjectWithOptionalFieldParams = { - "string": "string", - "long_": 12345, - "bool_": True, - "literal": "lit_one", - "any": "any", - } - converted = convert_and_respect_annotation_metadata( - object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" - ) - assert converted == {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"} - - -def test_convert_and_respect_annotation_metadata_in_list() -> None: - data: List[ObjectWithOptionalFieldParams] = [ - {"string": "string", "long_": 12345, "bool_": True, "literal": "lit_one", "any": "any"}, - {"string": "another string", "long_": 67890, "list_": [], "literal": "lit_one", "any": "any"}, - ] - converted = convert_and_respect_annotation_metadata( - object_=data, annotation=List[ObjectWithOptionalFieldParams], direction="write" - ) - - assert converted == [ - {"string": "string", "long": 12345, "bool": True, "literal": "lit_one", "any": "any"}, - {"string": "another string", "long": 67890, "list": [], "literal": "lit_one", "any": "any"}, - ] - - -def test_convert_and_respect_annotation_metadata_in_nested_object() -> None: - data: ObjectWithOptionalFieldParams = { - "string": "string", - "long_": 12345, - "union": UNION_TEST, - "literal": "lit_one", - "any": "any", - } - converted = convert_and_respect_annotation_metadata( - object_=data, annotation=ObjectWithOptionalFieldParams, direction="write" - ) - - assert converted == { - "string": "string", - "long": 12345, - "union": UNION_TEST_CONVERTED, - "literal": "lit_one", - "any": "any", - } - - -def test_convert_and_respect_annotation_metadata_in_union() -> None: - converted = convert_and_respect_annotation_metadata(object_=UNION_TEST, annotation=ShapeParams, direction="write") - - assert converted == UNION_TEST_CONVERTED - - -def test_convert_and_respect_annotation_metadata_with_empty_object() -> None: - data: Any = {} - converted = convert_and_respect_annotation_metadata(object_=data, annotation=ShapeParams, direction="write") - assert converted == data diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a9b4c92 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1829 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version < '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", +] +conflicts = [[ + { package = "vitable-connect", group = "pydantic-v1" }, + { package = "vitable-connect", group = "pydantic-v2" }, +]] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/d6/5aec9313ee6ea9c7cde8b891b69f4ff4001416867104580670a31daeba5b/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7", size = 738950, upload-time = "2026-01-03T17:29:13.002Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/8fa90a7e6d11ff20a18837a8e2b5dd23db01aabc475aa9271c8ad33299f5/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821", size = 496099, upload-time = "2026-01-03T17:29:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/d2/23/b81f744d402510a8366b74eb420fc0cc1170d0c43daca12d10814df85f10/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845", size = 491072, upload-time = "2026-01-03T17:29:16.922Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e1/56d1d1c0dd334cd203dd97706ce004c1aa24b34a813b0b8daf3383039706/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af", size = 1671588, upload-time = "2026-01-03T17:29:18.539Z" }, + { url = "https://files.pythonhosted.org/packages/5f/34/8d7f962604f4bc2b4e39eb1220dac7d4e4cba91fb9ba0474b4ecd67db165/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940", size = 1640334, upload-time = "2026-01-03T17:29:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/94/1d/fcccf2c668d87337ddeef9881537baee13c58d8f01f12ba8a24215f2b804/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160", size = 1722656, upload-time = "2026-01-03T17:29:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/aa/98/c6f3b081c4c606bc1e5f2ec102e87d6411c73a9ef3616fea6f2d5c98c062/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7", size = 1817625, upload-time = "2026-01-03T17:29:24.276Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c0/cfcc3d2e11b477f86e1af2863f3858c8850d751ce8dc39c4058a072c9e54/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455", size = 1672604, upload-time = "2026-01-03T17:29:26.099Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/6b4ffcbcac4c6a5d041343a756f34a6dd26174ae07f977a64fe028dda5b0/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279", size = 1554370, upload-time = "2026-01-03T17:29:28.121Z" }, + { url = "https://files.pythonhosted.org/packages/f2/f0/e3ddfa93f17d689dbe014ba048f18e0c9f9b456033b70e94349a2e9048be/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e", size = 1642023, upload-time = "2026-01-03T17:29:30.002Z" }, + { url = "https://files.pythonhosted.org/packages/eb/45/c14019c9ec60a8e243d06d601b33dcc4fd92379424bde3021725859d7f99/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d", size = 1649680, upload-time = "2026-01-03T17:29:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fd/09c9451dae5aa5c5ed756df95ff9ef549d45d4be663bafd1e4954fd836f0/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808", size = 1692407, upload-time = "2026-01-03T17:29:33.392Z" }, + { url = "https://files.pythonhosted.org/packages/a6/81/938bc2ec33c10efd6637ccb3d22f9f3160d08e8f3aa2587a2c2d5ab578eb/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40", size = 1543047, upload-time = "2026-01-03T17:29:34.855Z" }, + { url = "https://files.pythonhosted.org/packages/f7/23/80488ee21c8d567c83045e412e1d9b7077d27171591a4eb7822586e8c06a/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29", size = 1715264, upload-time = "2026-01-03T17:29:36.389Z" }, + { url = "https://files.pythonhosted.org/packages/e2/83/259a8da6683182768200b368120ab3deff5370bed93880fb9a3a86299f34/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11", size = 1657275, upload-time = "2026-01-03T17:29:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/3f/4f/2c41f800a0b560785c10fb316216ac058c105f9be50bdc6a285de88db625/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd", size = 434053, upload-time = "2026-01-03T17:29:40.074Z" }, + { url = "https://files.pythonhosted.org/packages/80/df/29cd63c7ecfdb65ccc12f7d808cac4fa2a19544660c06c61a4a48462de0c/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c", size = 456687, upload-time = "2026-01-03T17:29:41.819Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" }, + { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" }, + { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" }, + { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" }, + { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/446655656861d3e7e2c32bfcf160c7aa9e9dc63776a691b124dba65cdd77/aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e", size = 741433, upload-time = "2026-01-03T17:32:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/cb/49/773c4b310b5140d2fb5e79bb0bf40b7b41dad80a288ca1a8759f5f72bda9/aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7", size = 497332, upload-time = "2026-01-03T17:32:28.37Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/1dcbc4b83a4e6f76a0ad883f07f21ffbfe29750c89db97381701508c9f45/aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02", size = 492365, upload-time = "2026-01-03T17:32:30.234Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b5/b50657496c8754482cd7964e50aaf3aa84b3db61ed45daec4c1aec5b94b4/aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43", size = 1660440, upload-time = "2026-01-03T17:32:32.586Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/9b69e5139d89d75127569298931444ad78ea86a5befd5599780b1e9a6880/aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6", size = 1632740, upload-time = "2026-01-03T17:32:34.793Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fe/3ea9b5af694b4e3aec0d0613a806132ca744747146fca68e96bf056f61a7/aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce", size = 1719782, upload-time = "2026-01-03T17:32:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c2/46b3b06e60851cbb71efb0f79a3267279cbef7b12c58e68a1e897f269cca/aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80", size = 1813527, upload-time = "2026-01-03T17:32:39.973Z" }, + { url = "https://files.pythonhosted.org/packages/36/23/71ceb78c769ed65fe4c697692de232b63dab399210678d2b00961ccb0619/aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a", size = 1661268, upload-time = "2026-01-03T17:32:42.082Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/86e929523d955e85ebab7c0e2b9e0cb63604cfc27dc3280e10d0063cf682/aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6", size = 1552742, upload-time = "2026-01-03T17:32:44.622Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/3f5987cba1bab6bd151f0d97aa60f0ce04d3c83316692a6bb6ba2fb69f92/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558", size = 1632918, upload-time = "2026-01-03T17:32:46.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/2c/7e1e85121f2e31ee938cb83a8f32dfafd4908530c10fabd6d46761c12ac7/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7", size = 1644446, upload-time = "2026-01-03T17:32:49.063Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/ce6133d423ad0e8ca976a7c848f7146bca3520eea4ccf6b95e2d077c9d20/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877", size = 1689487, upload-time = "2026-01-03T17:32:51.113Z" }, + { url = "https://files.pythonhosted.org/packages/50/f7/ff7a27c15603d460fd1366b3c22054f7ae4fa9310aca40b43bde35867fcd/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3", size = 1540715, upload-time = "2026-01-03T17:32:53.38Z" }, + { url = "https://files.pythonhosted.org/packages/17/02/053f11346e5b962e6d8a1c4f8c70c29d5970a1b4b8e7894c68e12c27a57f/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704", size = 1711835, upload-time = "2026-01-03T17:32:56.088Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/9b9761ddf276fd6708d13720197cbac19b8d67ecfa9116777924056cfcaa/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f", size = 1649593, upload-time = "2026-01-03T17:32:58.181Z" }, + { url = "https://files.pythonhosted.org/packages/ae/72/5d817e9ea218acae12a5e3b9ad1178cf0c12fc3570c0b47eea2daf95f9ea/aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1", size = 434831, upload-time = "2026-01-03T17:33:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/39/cb/22659d9bf3149b7a2927bc2769cc9c8f8f5a80eba098398e03c199a43a85/aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538", size = 457697, upload-time = "2026-01-03T17:33:03.167Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dirty-equals" +version = "0.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/c2/59/ae5cdac87a00962122ea37bb346d41b66aec05f9ce328fa2b9e216f8967b/frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47", size = 86967, upload-time = "2025-10-06T05:37:55.607Z" }, + { url = "https://files.pythonhosted.org/packages/8a/10/17059b2db5a032fd9323c41c39e9d1f5f9d0c8f04d1e4e3e788573086e61/frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca", size = 49984, upload-time = "2025-10-06T05:37:57.049Z" }, + { url = "https://files.pythonhosted.org/packages/4b/de/ad9d82ca8e5fa8f0c636e64606553c79e2b859ad253030b62a21fe9986f5/frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068", size = 50240, upload-time = "2025-10-06T05:37:58.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/45/3dfb7767c2a67d123650122b62ce13c731b6c745bc14424eea67678b508c/frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95", size = 219472, upload-time = "2025-10-06T05:37:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/bf/5bf23d913a741b960d5c1dac7c1985d8a2a1d015772b2d18ea168b08e7ff/frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459", size = 221531, upload-time = "2025-10-06T05:38:00.521Z" }, + { url = "https://files.pythonhosted.org/packages/d0/03/27ec393f3b55860859f4b74cdc8c2a4af3dbf3533305e8eacf48a4fd9a54/frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675", size = 219211, upload-time = "2025-10-06T05:38:01.842Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ad/0fd00c404fa73fe9b169429e9a972d5ed807973c40ab6b3cf9365a33d360/frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61", size = 231775, upload-time = "2025-10-06T05:38:03.384Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c3/86962566154cb4d2995358bc8331bfc4ea19d07db1a96f64935a1607f2b6/frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6", size = 236631, upload-time = "2025-10-06T05:38:04.609Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/6ffad161dbd83782d2c66dc4d378a9103b31770cb1e67febf43aea42d202/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5", size = 218632, upload-time = "2025-10-06T05:38:05.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/b2/4677eee46e0a97f9b30735e6ad0bf6aba3e497986066eb68807ac85cf60f/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3", size = 235967, upload-time = "2025-10-06T05:38:07.614Z" }, + { url = "https://files.pythonhosted.org/packages/05/f3/86e75f8639c5a93745ca7addbbc9de6af56aebb930d233512b17e46f6493/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1", size = 228799, upload-time = "2025-10-06T05:38:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/30/00/39aad3a7f0d98f5eb1d99a3c311215674ed87061aecee7851974b335c050/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178", size = 230566, upload-time = "2025-10-06T05:38:10.52Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4d/aa144cac44568d137846ddc4d5210fb5d9719eb1d7ec6fa2728a54b5b94a/frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda", size = 217715, upload-time = "2025-10-06T05:38:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/64/4c/8f665921667509d25a0dd72540513bc86b356c95541686f6442a3283019f/frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087", size = 39933, upload-time = "2025-10-06T05:38:13.061Z" }, + { url = "https://files.pythonhosted.org/packages/79/bd/bcc926f87027fad5e59926ff12d136e1082a115025d33c032d1cd69ab377/frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a", size = 44121, upload-time = "2025-10-06T05:38:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/4c/07/9c2e4eb7584af4b705237b971b89a4155a8e57599c4483a131a39256a9a0/frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103", size = 40312, upload-time = "2025-10-06T05:38:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-aiohttp" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/2c/b894861cecf030fb45675ea24aa55b5722e97c602a163d872fca66c5a6d8/httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c", size = 275945, upload-time = "2025-12-12T10:12:15.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8d/85c9701e9af72ca132a1783e2a54364a90c6da832304416a30fc11196ab2/httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141", size = 6367, upload-time = "2025-12-12T10:12:14.018Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/90/d7/4cf84257902265c4250769ac49f4eaab81c182ee9aff8bf59d2714dbb174/multidict-6.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:363eb68a0a59bd2303216d2346e6c441ba10d36d1f9969fcb6f1ba700de7bb5c", size = 77073, upload-time = "2025-10-06T14:51:57.386Z" }, + { url = "https://files.pythonhosted.org/packages/6d/51/194e999630a656e76c2965a1590d12faa5cd528170f2abaa04423e09fe8d/multidict-6.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d874eb056410ca05fed180b6642e680373688efafc7f077b2a2f61811e873a40", size = 44928, upload-time = "2025-10-06T14:51:58.791Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6b/2a195373c33068c9158e0941d0b46cfcc9c1d894ca2eb137d1128081dff0/multidict-6.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b55d5497b51afdfde55925e04a022f1de14d4f4f25cdfd4f5d9b0aa96166851", size = 44581, upload-time = "2025-10-06T14:52:00.174Z" }, + { url = "https://files.pythonhosted.org/packages/69/7b/7f4f2e644b6978bf011a5fd9a5ebb7c21de3f38523b1f7897d36a1ac1311/multidict-6.7.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f8e5c0031b90ca9ce555e2e8fd5c3b02a25f14989cbc310701823832c99eb687", size = 239901, upload-time = "2025-10-06T14:52:02.416Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b5/952c72786710a031aa204a9adf7db66d7f97a2c6573889d58b9e60fe6702/multidict-6.7.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cf41880c991716f3c7cec48e2f19ae4045fc9db5fc9cff27347ada24d710bb5", size = 240534, upload-time = "2025-10-06T14:52:04.105Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ef/109fe1f2471e4c458c74242c7e4a833f2d9fc8a6813cd7ee345b0bad18f9/multidict-6.7.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cfc12a8630a29d601f48d47787bd7eb730e475e83edb5d6c5084317463373eb", size = 219545, upload-time = "2025-10-06T14:52:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/42/bd/327d91288114967f9fe90dc53de70aa3fec1b9073e46aa32c4828f771a87/multidict-6.7.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3996b50c3237c4aec17459217c1e7bbdead9a22a0fcd3c365564fbd16439dde6", size = 251187, upload-time = "2025-10-06T14:52:08.049Z" }, + { url = "https://files.pythonhosted.org/packages/f4/13/a8b078ebbaceb7819fd28cd004413c33b98f1b70d542a62e6a00b74fb09f/multidict-6.7.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7f5170993a0dd3ab871c74f45c0a21a4e2c37a2f2b01b5f722a2ad9c6650469e", size = 249379, upload-time = "2025-10-06T14:52:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6d/ab12e1246be4d65d1f55de1e6f6aaa9b8120eddcfdd1d290439c7833d5ce/multidict-6.7.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec81878ddf0e98817def1e77d4f50dae5ef5b0e4fe796fae3bd674304172416e", size = 239241, upload-time = "2025-10-06T14:52:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/079a93625208c173b8fa756396814397c0fd9fee61ef87b75a748820b86e/multidict-6.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9281bf5b34f59afbc6b1e477a372e9526b66ca446f4bf62592839c195a718b32", size = 237418, upload-time = "2025-10-06T14:52:13.671Z" }, + { url = "https://files.pythonhosted.org/packages/c9/29/03777c2212274aa9440918d604dc9d6af0e6b4558c611c32c3dcf1a13870/multidict-6.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:68af405971779d8b37198726f2b6fe3955db846fee42db7a4286fc542203934c", size = 232987, upload-time = "2025-10-06T14:52:15.708Z" }, + { url = "https://files.pythonhosted.org/packages/d9/00/11188b68d85a84e8050ee34724d6ded19ad03975caebe0c8dcb2829b37bf/multidict-6.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3ba3ef510467abb0667421a286dc906e30eb08569365f5cdb131d7aff7c2dd84", size = 240985, upload-time = "2025-10-06T14:52:17.317Z" }, + { url = "https://files.pythonhosted.org/packages/df/0c/12eef6aeda21859c6cdf7d75bd5516d83be3efe3d8cc45fd1a3037f5b9dc/multidict-6.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b61189b29081a20c7e4e0b49b44d5d44bb0dc92be3c6d06a11cc043f81bf9329", size = 246855, upload-time = "2025-10-06T14:52:19.096Z" }, + { url = "https://files.pythonhosted.org/packages/69/f6/076120fd8bb3975f09228e288e08bff6b9f1bfd5166397c7ba284f622ab2/multidict-6.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fb287618b9c7aa3bf8d825f02d9201b2f13078a5ed3b293c8f4d953917d84d5e", size = 241804, upload-time = "2025-10-06T14:52:21.166Z" }, + { url = "https://files.pythonhosted.org/packages/5f/51/41bb950c81437b88a93e6ddfca1d8763569ae861e638442838c4375f7497/multidict-6.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:521f33e377ff64b96c4c556b81c55d0cfffb96a11c194fd0c3f1e56f3d8dd5a4", size = 235321, upload-time = "2025-10-06T14:52:23.208Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cf/5bbd31f055199d56c1f6b04bbadad3ccb24e6d5d4db75db774fc6d6674b8/multidict-6.7.0-cp39-cp39-win32.whl", hash = "sha256:ce8fdc2dca699f8dbf055a61d73eaa10482569ad20ee3c36ef9641f69afa8c91", size = 41435, upload-time = "2025-10-06T14:52:24.735Z" }, + { url = "https://files.pythonhosted.org/packages/af/01/547ffe9c2faec91c26965c152f3fea6cff068b6037401f61d310cc861ff4/multidict-6.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:7e73299c99939f089dd9b2120a04a516b95cdf8c1cd2b18c53ebf0de80b1f18f", size = 46193, upload-time = "2025-10-06T14:52:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/27/77/cfa5461d1d2651d6fc24216c92b4a21d4e385a41c46e0d9f3b070675167b/multidict-6.7.0-cp39-cp39-win_arm64.whl", hash = "sha256:6bdce131e14b04fd34a809b6380dbfd826065c3e2fe8a50dbae659fa0c390546", size = 43118, upload-time = "2025-10-06T14:52:27.876Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mypy" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/e3/034322d5a779685218ed69286c32faa505247f1f096251ef66c8fd203b08/mypy-1.17.0.tar.gz", hash = "sha256:e5d7ccc08ba089c06e2f5629c660388ef1fee708444f1dee0b9203fa031dee03", size = 3352114, upload-time = "2025-07-14T20:34:30.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/31/e762baa3b73905c856d45ab77b4af850e8159dffffd86a52879539a08c6b/mypy-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8e08de6138043108b3b18f09d3f817a4783912e48828ab397ecf183135d84d6", size = 10998313, upload-time = "2025-07-14T20:33:24.519Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c1/25b2f0d46fb7e0b5e2bee61ec3a47fe13eff9e3c2f2234f144858bbe6485/mypy-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce4a17920ec144647d448fc43725b5873548b1aae6c603225626747ededf582d", size = 10128922, upload-time = "2025-07-14T20:34:06.414Z" }, + { url = "https://files.pythonhosted.org/packages/02/78/6d646603a57aa8a2886df1b8881fe777ea60f28098790c1089230cd9c61d/mypy-1.17.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ff25d151cc057fdddb1cb1881ef36e9c41fa2a5e78d8dd71bee6e4dcd2bc05b", size = 11913524, upload-time = "2025-07-14T20:33:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/19/dae6c55e87ee426fb76980f7e78484450cad1c01c55a1dc4e91c930bea01/mypy-1.17.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93468cf29aa9a132bceb103bd8475f78cacde2b1b9a94fd978d50d4bdf616c9a", size = 12650527, upload-time = "2025-07-14T20:32:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/86/e1/f916845a235235a6c1e4d4d065a3930113767001d491b8b2e1b61ca56647/mypy-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:98189382b310f16343151f65dd7e6867386d3e35f7878c45cfa11383d175d91f", size = 12897284, upload-time = "2025-07-14T20:33:38.168Z" }, + { url = "https://files.pythonhosted.org/packages/ae/dc/414760708a4ea1b096bd214d26a24e30ac5e917ef293bc33cdb6fe22d2da/mypy-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:c004135a300ab06a045c1c0d8e3f10215e71d7b4f5bb9a42ab80236364429937", size = 9506493, upload-time = "2025-07-14T20:34:01.093Z" }, + { url = "https://files.pythonhosted.org/packages/d4/24/82efb502b0b0f661c49aa21cfe3e1999ddf64bf5500fc03b5a1536a39d39/mypy-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d4fe5c72fd262d9c2c91c1117d16aac555e05f5beb2bae6a755274c6eec42be", size = 10914150, upload-time = "2025-07-14T20:31:51.985Z" }, + { url = "https://files.pythonhosted.org/packages/03/96/8ef9a6ff8cedadff4400e2254689ca1dc4b420b92c55255b44573de10c54/mypy-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96b196e5c16f41b4f7736840e8455958e832871990c7ba26bf58175e357ed61", size = 10039845, upload-time = "2025-07-14T20:32:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/df/32/7ce359a56be779d38021d07941cfbb099b41411d72d827230a36203dbb81/mypy-1.17.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73a0ff2dd10337ceb521c080d4147755ee302dcde6e1a913babd59473904615f", size = 11837246, upload-time = "2025-07-14T20:32:01.28Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/b775047054de4d8dbd668df9137707e54b07fe18c7923839cd1e524bf756/mypy-1.17.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cfcc1179c4447854e9e406d3af0f77736d631ec87d31c6281ecd5025df625d", size = 12571106, upload-time = "2025-07-14T20:34:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cf/fa33eaf29a606102c8d9ffa45a386a04c2203d9ad18bf4eef3e20c43ebc8/mypy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c56f180ff6430e6373db7a1d569317675b0a451caf5fef6ce4ab365f5f2f6c3", size = 12759960, upload-time = "2025-07-14T20:33:42.882Z" }, + { url = "https://files.pythonhosted.org/packages/94/75/3f5a29209f27e739ca57e6350bc6b783a38c7621bdf9cac3ab8a08665801/mypy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:eafaf8b9252734400f9b77df98b4eee3d2eecab16104680d51341c75702cad70", size = 9503888, upload-time = "2025-07-14T20:32:34.392Z" }, + { url = "https://files.pythonhosted.org/packages/12/e9/e6824ed620bbf51d3bf4d6cbbe4953e83eaf31a448d1b3cfb3620ccb641c/mypy-1.17.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f986f1cab8dbec39ba6e0eaa42d4d3ac6686516a5d3dccd64be095db05ebc6bb", size = 11086395, upload-time = "2025-07-14T20:34:11.452Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/a4afd1ae279707953be175d303f04a5a7bd7e28dc62463ad29c1c857927e/mypy-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51e455a54d199dd6e931cd7ea987d061c2afbaf0960f7f66deef47c90d1b304d", size = 10120052, upload-time = "2025-07-14T20:33:09.897Z" }, + { url = "https://files.pythonhosted.org/packages/8a/71/19adfeac926ba8205f1d1466d0d360d07b46486bf64360c54cb5a2bd86a8/mypy-1.17.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3204d773bab5ff4ebbd1f8efa11b498027cd57017c003ae970f310e5b96be8d8", size = 11861806, upload-time = "2025-07-14T20:32:16.028Z" }, + { url = "https://files.pythonhosted.org/packages/0b/64/d6120eca3835baf7179e6797a0b61d6c47e0bc2324b1f6819d8428d5b9ba/mypy-1.17.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1051df7ec0886fa246a530ae917c473491e9a0ba6938cfd0ec2abc1076495c3e", size = 12744371, upload-time = "2025-07-14T20:33:33.503Z" }, + { url = "https://files.pythonhosted.org/packages/1f/dc/56f53b5255a166f5bd0f137eed960e5065f2744509dfe69474ff0ba772a5/mypy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f773c6d14dcc108a5b141b4456b0871df638eb411a89cd1c0c001fc4a9d08fc8", size = 12914558, upload-time = "2025-07-14T20:33:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/69/ac/070bad311171badc9add2910e7f89271695a25c136de24bbafc7eded56d5/mypy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1619a485fd0e9c959b943c7b519ed26b712de3002d7de43154a489a2d0fd817d", size = 9585447, upload-time = "2025-07-14T20:32:20.594Z" }, + { url = "https://files.pythonhosted.org/packages/be/7b/5f8ab461369b9e62157072156935cec9d272196556bdc7c2ff5f4c7c0f9b/mypy-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c41aa59211e49d717d92b3bb1238c06d387c9325d3122085113c79118bebb06", size = 11070019, upload-time = "2025-07-14T20:32:07.99Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/c49c9e5a2ac0badcc54beb24e774d2499748302c9568f7f09e8730e953fa/mypy-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e69db1fb65b3114f98c753e3930a00514f5b68794ba80590eb02090d54a5d4a", size = 10114457, upload-time = "2025-07-14T20:33:47.285Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/fb3f9c939ad9beed3e328008b3fb90b20fda2cddc0f7e4c20dbefefc3b33/mypy-1.17.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03ba330b76710f83d6ac500053f7727270b6b8553b0423348ffb3af6f2f7b889", size = 11857838, upload-time = "2025-07-14T20:33:14.462Z" }, + { url = "https://files.pythonhosted.org/packages/4c/66/85607ab5137d65e4f54d9797b77d5a038ef34f714929cf8ad30b03f628df/mypy-1.17.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037bc0f0b124ce46bfde955c647f3e395c6174476a968c0f22c95a8d2f589bba", size = 12731358, upload-time = "2025-07-14T20:32:25.579Z" }, + { url = "https://files.pythonhosted.org/packages/73/d0/341dbbfb35ce53d01f8f2969facbb66486cee9804048bf6c01b048127501/mypy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c38876106cb6132259683632b287238858bd58de267d80defb6f418e9ee50658", size = 12917480, upload-time = "2025-07-14T20:34:21.868Z" }, + { url = "https://files.pythonhosted.org/packages/64/63/70c8b7dbfc520089ac48d01367a97e8acd734f65bd07813081f508a8c94c/mypy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d30ba01c0f151998f367506fab31c2ac4527e6a7b2690107c7a7f9e3cb419a9c", size = 9589666, upload-time = "2025-07-14T20:34:16.841Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a0/6263dd11941231f688f0a8f2faf90ceac1dc243d148d314a089d2fe25108/mypy-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:63e751f1b5ab51d6f3d219fe3a2fe4523eaa387d854ad06906c63883fde5b1ab", size = 10988185, upload-time = "2025-07-14T20:33:04.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/13/b8f16d6b0dc80277129559c8e7dbc9011241a0da8f60d031edb0e6e9ac8f/mypy-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f7fb09d05e0f1c329a36dcd30e27564a3555717cde87301fae4fb542402ddfad", size = 10120169, upload-time = "2025-07-14T20:32:38.84Z" }, + { url = "https://files.pythonhosted.org/packages/14/ef/978ba79df0d65af680e20d43121363cf643eb79b04bf3880d01fc8afeb6f/mypy-1.17.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b72c34ce05ac3a1361ae2ebb50757fb6e3624032d91488d93544e9f82db0ed6c", size = 11918121, upload-time = "2025-07-14T20:33:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/55ef70b104151a0d8280474f05268ff0a2a79be8d788d5e647257d121309/mypy-1.17.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434ad499ad8dde8b2f6391ddfa982f41cb07ccda8e3c67781b1bfd4e5f9450a8", size = 12648821, upload-time = "2025-07-14T20:32:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/26/8c/7781fcd2e1eef48fbedd3a422c21fe300a8e03ed5be2eb4bd10246a77f4e/mypy-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f105f61a5eff52e137fd73bee32958b2add9d9f0a856f17314018646af838e97", size = 12896955, upload-time = "2025-07-14T20:32:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/78/13/03ac759dabe86e98ca7b6681f114f90ee03f3ff8365a57049d311bd4a4e3/mypy-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:ba06254a5a22729853209550d80f94e28690d5530c661f9416a68ac097b13fc4", size = 9512957, upload-time = "2025-07-14T20:33:28.619Z" }, + { url = "https://files.pythonhosted.org/packages/e3/fc/ee058cc4316f219078464555873e99d170bde1d9569abd833300dbeb484a/mypy-1.17.0-py3-none-any.whl", hash = "sha256:15d9d0018237ab058e5de3d8fce61b6fa72cc59cc78fd91f1b474bce12abf496", size = 2283195, upload-time = "2025-07-14T20:31:54.753Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathspec" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/b2/bb8e495d5262bfec41ab5cb18f522f1012933347fb5d9e62452d446baca2/pathspec-1.0.3.tar.gz", hash = "sha256:bac5cf97ae2c2876e2d25ebb15078eb04d76e4b98921ee31c6f85ade8b59444d", size = 130841, upload-time = "2026-01-09T15:46:46.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/2b/121e912bd60eebd623f873fd090de0e84f322972ab25a7f9044c056804ed/pathspec-1.0.3-py3-none-any.whl", hash = "sha256:e80767021c1cc524aa3fb14bedda9c34406591343cc42797b386ce7b9354fb6c", size = 55021, upload-time = "2026-01-09T15:46:44.652Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/0ebaec9003f5d619a7475165961f8e3083cf8644d704b60395df3601632d/propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff", size = 80277, upload-time = "2025-10-08T19:48:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/04af97ac586b4ef6b9026c3fd36ee7798b737a832f5d3440a4280dcebd3a/propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb", size = 45865, upload-time = "2025-10-08T19:48:37.859Z" }, + { url = "https://files.pythonhosted.org/packages/7c/19/b65d98ae21384518b291d9939e24a8aeac4fdb5101b732576f8f7540e834/propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac", size = 47636, upload-time = "2025-10-08T19:48:39.038Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/317048c6d91c356c7154dca5af019e6effeb7ee15fa6a6db327cc19e12b4/propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888", size = 201126, upload-time = "2025-10-08T19:48:40.774Z" }, + { url = "https://files.pythonhosted.org/packages/71/69/0b2a7a5a6ee83292b4b997dbd80549d8ce7d40b6397c1646c0d9495f5a85/propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc", size = 209837, upload-time = "2025-10-08T19:48:42.167Z" }, + { url = "https://files.pythonhosted.org/packages/a5/92/c699ac495a6698df6e497fc2de27af4b6ace10d8e76528357ce153722e45/propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a", size = 215578, upload-time = "2025-10-08T19:48:43.56Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ee/14de81c5eb02c0ee4f500b4e39c4e1bd0677c06e72379e6ab18923c773fc/propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88", size = 197187, upload-time = "2025-10-08T19:48:45.309Z" }, + { url = "https://files.pythonhosted.org/packages/1d/94/48dce9aaa6d8dd5a0859bad75158ec522546d4ac23f8e2f05fac469477dd/propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00", size = 193478, upload-time = "2025-10-08T19:48:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/60/b5/0516b563e801e1ace212afde869a0596a0d7115eec0b12d296d75633fb29/propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0", size = 190650, upload-time = "2025-10-08T19:48:49.373Z" }, + { url = "https://files.pythonhosted.org/packages/24/89/e0f7d4a5978cd56f8cd67735f74052f257dc471ec901694e430f0d1572fe/propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e", size = 200251, upload-time = "2025-10-08T19:48:51.4Z" }, + { url = "https://files.pythonhosted.org/packages/06/7d/a1fac863d473876ed4406c914f2e14aa82d2f10dd207c9e16fc383cc5a24/propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781", size = 200919, upload-time = "2025-10-08T19:48:53.227Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/f86a256ff24944cf5743e4e6c6994e3526f6acfcfb55e21694c2424f758c/propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183", size = 193211, upload-time = "2025-10-08T19:48:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/6e/3f/3fbad5f4356b068f1b047d300a6ff2c66614d7030f078cd50be3fec04228/propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19", size = 38314, upload-time = "2025-10-08T19:48:56.792Z" }, + { url = "https://files.pythonhosted.org/packages/a4/45/d78d136c3a3d215677abb886785aae744da2c3005bcb99e58640c56529b1/propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f", size = 41912, upload-time = "2025-10-08T19:48:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/b0632941f25139f4e58450b307242951f7c2717a5704977c6d5323a800af/propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938", size = 38450, upload-time = "2025-10-08T19:48:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pydantic" +version = "1.10.26" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-15-vitable-connect-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/da/fd89f987a376c807cd81ea0eff4589aade783bbb702637b4734ef2c743a2/pydantic-1.10.26.tar.gz", hash = "sha256:8c6aa39b494c5af092e690127c283d84f363ac36017106a9e66cb33a22ac412e", size = 357906, upload-time = "2025-12-18T15:47:46.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/08/2587a6d4314e7539eec84acd062cb7b037638edb57a0335d20e4c5b8878c/pydantic-1.10.26-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7ae36fa0ecef8d39884120f212e16c06bb096a38f523421278e2f39c1784546", size = 2444588, upload-time = "2025-12-18T15:46:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/47/e6/10df5f08c105bcbb4adbee7d1108ff4b347702b110fed058f6a03f1c6b73/pydantic-1.10.26-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d95a76cf503f0f72ed7812a91de948440b2bf564269975738a4751e4fadeb572", size = 2255972, upload-time = "2025-12-18T15:46:31.72Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/fdb961e7adc2c31f394feba6f560ef2c74c446f0285e2c2eb87d2b7206c7/pydantic-1.10.26-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a943ce8e00ad708ed06a1d9df5b4fd28f5635a003b82a4908ece6f24c0b18464", size = 2857175, upload-time = "2025-12-18T15:46:34Z" }, + { url = "https://files.pythonhosted.org/packages/8f/6c/f21e27dda475d4c562bd01b5874284dd3180f336c1e669413b743ca8b278/pydantic-1.10.26-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:465ad8edb29b15c10b779b16431fe8e77c380098badf6db367b7a1d3e572cf53", size = 2947001, upload-time = "2025-12-18T15:46:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f6/27ea206232cbb6ec24dc4e4e8888a9a734f96a1eaf13504be4b30ef26aa7/pydantic-1.10.26-cp310-cp310-win_amd64.whl", hash = "sha256:80e6be6272839c8a7641d26ad569ab77772809dd78f91d0068dc0fc97f071945", size = 2066217, upload-time = "2025-12-18T15:46:37.614Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c1/d521e64c8130e1ad9d22c270bed3fabcc0940c9539b076b639c88fd32a8d/pydantic-1.10.26-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:116233e53889bcc536f617e38c1b8337d7fa9c280f0fd7a4045947515a785637", size = 2428347, upload-time = "2025-12-18T15:46:39.41Z" }, + { url = "https://files.pythonhosted.org/packages/2c/08/f4b804a00c16e3ea994cb640a7c25c579b4f1fa674cde6a19fa0dfb0ae4f/pydantic-1.10.26-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c3cfdd361addb6eb64ccd26ac356ad6514cee06a61ab26b27e16b5ed53108f77", size = 2212605, upload-time = "2025-12-18T15:46:41.006Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/0df4b9efef29bbc5e39f247fcba99060d15946b4463d82a5589cf7923d71/pydantic-1.10.26-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e4451951a9a93bf9a90576f3e25240b47ee49ab5236adccb8eff6ac943adf0f", size = 2753560, upload-time = "2025-12-18T15:46:43.215Z" }, + { url = "https://files.pythonhosted.org/packages/68/66/6ab6c1d3a116d05d2508fce64f96e35242938fac07544d611e11d0d363a0/pydantic-1.10.26-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9858ed44c6bea5f29ffe95308db9e62060791c877766c67dd5f55d072c8612b5", size = 2859235, upload-time = "2025-12-18T15:46:45.112Z" }, + { url = "https://files.pythonhosted.org/packages/61/4e/f1676bb0fcdf6ed2ce4670d7d1fc1d6c3a06d84497644acfbe02649503f1/pydantic-1.10.26-cp311-cp311-win_amd64.whl", hash = "sha256:ac1089f723e2106ebde434377d31239e00870a7563245072968e5af5cc4d33df", size = 2066646, upload-time = "2025-12-18T15:46:46.816Z" }, + { url = "https://files.pythonhosted.org/packages/02/6c/cd97a5a776c4515e6ee2ae81c2f2c5be51376dda6c31f965d7746ce0019f/pydantic-1.10.26-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:468d5b9cacfcaadc76ed0a4645354ab6f263ec01a63fb6d05630ea1df6ae453f", size = 2433795, upload-time = "2025-12-18T15:46:49.321Z" }, + { url = "https://files.pythonhosted.org/packages/47/12/de20affa30dcef728fcf9cc98e13ff4438c7a630de8d2f90eb38eba0891c/pydantic-1.10.26-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2c1b0b914be31671000ca25cf7ea17fcaaa68cfeadf6924529c5c5aa24b7ab1f", size = 2227387, upload-time = "2025-12-18T15:46:50.877Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1d/9d65dcc5b8c17ba590f1f9f486e9306346831902318b7ee93f63516f4003/pydantic-1.10.26-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15b13b9f8ba8867095769e1156e0d7fbafa1f65b898dd40fd1c02e34430973cb", size = 2629594, upload-time = "2025-12-18T15:46:53.42Z" }, + { url = "https://files.pythonhosted.org/packages/3f/76/acb41409356789e23e1a7ef58f93821410c96409183ce314ddb58d97f23e/pydantic-1.10.26-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad7025ca324ae263d4313998e25078dcaec5f9ed0392c06dedb57e053cc8086b", size = 2745305, upload-time = "2025-12-18T15:46:55.987Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/a98c0c5e527a66057d969fedd61675223c7975ade61acebbca9f1abd6dc0/pydantic-1.10.26-cp312-cp312-win_amd64.whl", hash = "sha256:4482b299874dabb88a6c3759e3d85c6557c407c3b586891f7d808d8a38b66b9c", size = 1937647, upload-time = "2025-12-18T15:46:57.905Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/17a5a5a421c23ac27486b977724a42c9d5f8b7f0f4aab054251066223900/pydantic-1.10.26-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ae7913bb40a96c87e3d3f6fe4e918ef53bf181583de4e71824360a9b11aef1c", size = 2494599, upload-time = "2025-12-18T15:47:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8e/6e3bd4241076cf227b443d7577245dd5d181ecf40b3182fcb908bc8c197d/pydantic-1.10.26-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8154c13f58d4de5d3a856bb6c909c7370f41fb876a5952a503af6b975265f4ba", size = 2254391, upload-time = "2025-12-18T15:47:02.268Z" }, + { url = "https://files.pythonhosted.org/packages/a8/30/a1c4092eda2145ecbead6c92db489b223e101e1ba0da82576d0cf73dd422/pydantic-1.10.26-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8af0507bf6118b054a9765fb2e402f18a8b70c964f420d95b525eb711122d62", size = 2609445, upload-time = "2025-12-18T15:47:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/3a/2a/0491f1729ee4b7b6bc859ec22f69752f0c09bee1b66ac6f5f701136f34c3/pydantic-1.10.26-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dcb5a7318fb43189fde6af6f21ac7149c4bcbcfffc54bc87b5becddc46084847", size = 2732124, upload-time = "2025-12-18T15:47:07.464Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/b59f3b2f84e1df2b04ae768a1bb04d9f0288ff71b67cdcbb07683757b2c0/pydantic-1.10.26-cp313-cp313-win_amd64.whl", hash = "sha256:71cde228bc0600cf8619f0ee62db050d1880dcc477eba0e90b23011b4ee0f314", size = 1939888, upload-time = "2025-12-18T15:47:09.618Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8b/0c3dc02d4b97790b0f199bf933f677c14e7be4a8d21307c5f2daae06aa41/pydantic-1.10.26-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6b40730cc81d53d515dc0b8bb5c9b43fadb9bed46de4a3c03bd95e8571616dba", size = 2502689, upload-time = "2025-12-18T15:47:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9d/d31aeea45542b2ae4b09ecba92b88aaba696b801c31919811aa979a1242d/pydantic-1.10.26-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c3bbb9c0eecdf599e4db9b372fa9cc55be12e80a0d9c6d307950a39050cb0e37", size = 2269494, upload-time = "2025-12-18T15:47:14.53Z" }, + { url = "https://files.pythonhosted.org/packages/78/c1/3a4d069593283ca4dd0006039ba33644e21e432cddc09da706ac50441610/pydantic-1.10.26-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc2e3fe7bc4993626ef6b6fa855defafa1d6f8996aa1caef2deb83c5ac4d043a", size = 2620047, upload-time = "2025-12-18T15:47:17.089Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0e/340c3d29197d99c15ab04093d43bb9c9d0fd17c2a34b80cb9d36ed732b09/pydantic-1.10.26-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:36d9e46b588aaeb1dcd2409fa4c467fe0b331f3cc9f227b03a7a00643704e962", size = 2747625, upload-time = "2025-12-18T15:47:19.21Z" }, + { url = "https://files.pythonhosted.org/packages/1e/58/f12ab3727339b172c830b32151919456b67787cdfe8808b2568b322fb15c/pydantic-1.10.26-cp314-cp314-win_amd64.whl", hash = "sha256:81ce3c8616d12a7be31b4aadfd3434f78f6b44b75adbfaec2fe1ad4f7f999b8c", size = 1976436, upload-time = "2025-12-18T15:47:21.384Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8a/3a5a6267d5f03617b5c0f1985aa9fdfbafd33a50ef6dadd866a15ed4d123/pydantic-1.10.26-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:502b9d30d18a2dfaf81b7302f6ba0e5853474b1c96212449eb4db912cb604b7d", size = 2457039, upload-time = "2025-12-18T15:47:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/343ac0db26918a033ac6256c036d72c3b6eb1196b7de622e2e8a94b19079/pydantic-1.10.26-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0d8f6087bf697dec3bf7ffcd7fe8362674f16519f3151789f33cbe8f1d19fc15", size = 2266441, upload-time = "2025-12-18T15:47:36.807Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/1ab48136578608dba2f2a62e452f3db2083b474d4e49be5749c6ae0c123c/pydantic-1.10.26-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dd40a99c358419910c85e6f5d22f9c56684c25b5e7abc40879b3b4a52f34ae90", size = 2869383, upload-time = "2025-12-18T15:47:38.883Z" }, + { url = "https://files.pythonhosted.org/packages/a2/25/41dbf1bffc31eb242cece8080561a4133eaeb513372dec36a84477a3fb71/pydantic-1.10.26-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ce3293b86ca9f4125df02ff0a70be91bc7946522467cbd98e7f1493f340616ba", size = 2963582, upload-time = "2025-12-18T15:47:40.854Z" }, + { url = "https://files.pythonhosted.org/packages/61/2f/f072ae160a300c85eb9f059915101fd33dacf12d8df08c2b804acb3b95d1/pydantic-1.10.26-cp39-cp39-win_amd64.whl", hash = "sha256:1a4e3062b71ab1d5df339ba12c48f9ed5817c5de6cb92a961dd5c64bb32e7b96", size = 2075530, upload-time = "2025-12-18T15:47:43.181Z" }, + { url = "https://files.pythonhosted.org/packages/1f/98/556e82f00b98486def0b8af85da95e69d2be7e367cf2431408e108bc3095/pydantic-1.10.26-py3-none-any.whl", hash = "sha256:c43ad70dc3ce7787543d563792426a16fd7895e14be4b194b5665e36459dd917", size = 166975, upload-time = "2025-12-18T15:47:44.927Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version < '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", +] +dependencies = [ + { name = "annotated-types", marker = "extra == 'group-15-vitable-connect-pydantic-v2' or extra != 'group-15-vitable-connect-pydantic-v1'" }, + { name = "pydantic-core", marker = "extra == 'group-15-vitable-connect-pydantic-v2' or extra != 'group-15-vitable-connect-pydantic-v1'" }, + { name = "typing-extensions", marker = "extra == 'group-15-vitable-connect-pydantic-v2' or extra != 'group-15-vitable-connect-pydantic-v1'" }, + { name = "typing-inspection", marker = "extra == 'group-15-vitable-connect-pydantic-v2' or extra != 'group-15-vitable-connect-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-15-vitable-connect-pydantic-v2' or extra != 'group-15-vitable-connect-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.399" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/9d/d91d5f6d26b2db95476fefc772e2b9a16d54c6bd0ea6bb5c1b6d635ab8b4/pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b", size = 3856954, upload-time = "2025-04-10T04:40:25.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/b5/380380c9e7a534cb1783c70c3e8ac6d1193c599650a55838d0557586796e/pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b", size = 5592584, upload-time = "2025-04-10T04:40:23.502Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version < '3.10' and sys_platform == 'win32') or (python_full_version >= '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", +] +dependencies = [ + { name = "colorama", marker = "(python_full_version >= '3.10' and sys_platform == 'win32') or (python_full_version < '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2') or (sys_platform != 'win32' and extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "packaging", marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pluggy", marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pygments", marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "tomli", marker = "python_full_version == '3.10.*' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "typing-extensions", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", +] +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version == '3.10.*' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "typing-extensions", marker = "(python_full_version >= '3.10' and python_full_version < '3.13') or (python_full_version < '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2') or (python_full_version >= '3.13' and extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/0a/1914efb7903174b381ee2ffeebb4253e729de57f114e63595114c8ca451f/ruff-0.14.13.tar.gz", hash = "sha256:83cd6c0763190784b99650a20fec7633c59f6ebe41c5cc9d45ee42749563ad47", size = 6059504, upload-time = "2026-01-15T20:15:16.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/ae/0deefbc65ca74b0ab1fd3917f94dc3b398233346a74b8bbb0a916a1a6bf6/ruff-0.14.13-py3-none-linux_armv6l.whl", hash = "sha256:76f62c62cd37c276cb03a275b198c7c15bd1d60c989f944db08a8c1c2dbec18b", size = 13062418, upload-time = "2026-01-15T20:14:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/47/df/5916604faa530a97a3c154c62a81cb6b735c0cb05d1e26d5ad0f0c8ac48a/ruff-0.14.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:914a8023ece0528d5cc33f5a684f5f38199bbb566a04815c2c211d8f40b5d0ed", size = 13442344, upload-time = "2026-01-15T20:15:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/e0e694dd69163c3a1671e102aa574a50357536f18a33375050334d5cd517/ruff-0.14.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d24899478c35ebfa730597a4a775d430ad0d5631b8647a3ab368c29b7e7bd063", size = 12354720, upload-time = "2026-01-15T20:15:09.854Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/67f5fcbbaee25e8fc3b56cc33e9892eca7ffe09f773c8e5907757a7e3bdb/ruff-0.14.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aaf3870f14d925bbaf18b8a2347ee0ae7d95a2e490e4d4aea6813ed15ebc80e", size = 12774493, upload-time = "2026-01-15T20:15:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ce/d2e9cb510870b52a9565d885c0d7668cc050e30fa2c8ac3fb1fda15c083d/ruff-0.14.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac5b7f63dd3b27cc811850f5ffd8fff845b00ad70e60b043aabf8d6ecc304e09", size = 12815174, upload-time = "2026-01-15T20:15:05.74Z" }, + { url = "https://files.pythonhosted.org/packages/88/00/c38e5da58beebcf4fa32d0ddd993b63dfacefd02ab7922614231330845bf/ruff-0.14.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2b1097750d90ba82ce4ba676e85230a0ed694178ca5e61aa9b459970b3eb9", size = 13680909, upload-time = "2026-01-15T20:15:14.537Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/cd37c9dd5bd0a3099ba79b2a5899ad417d8f3b04038810b0501a80814fd7/ruff-0.14.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d0bf87705acbbcb8d4c24b2d77fbb73d40210a95c3903b443cd9e30824a5032", size = 15144215, upload-time = "2026-01-15T20:15:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/56/8a/85502d7edbf98c2df7b8876f316c0157359165e16cdf98507c65c8d07d3d/ruff-0.14.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3eb5da8e2c9e9f13431032fdcbe7681de9ceda5835efee3269417c13f1fed5c", size = 14706067, upload-time = "2026-01-15T20:14:48.271Z" }, + { url = "https://files.pythonhosted.org/packages/7e/2f/de0df127feb2ee8c1e54354dc1179b4a23798f0866019528c938ba439aca/ruff-0.14.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:642442b42957093811cd8d2140dfadd19c7417030a7a68cf8d51fcdd5f217427", size = 14133916, upload-time = "2026-01-15T20:14:57.357Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/9b99686bb9fe07a757c82f6f95e555c7a47801a9305576a9c67e0a31d280/ruff-0.14.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4acdf009f32b46f6e8864af19cbf6841eaaed8638e65c8dac845aea0d703c841", size = 13859207, upload-time = "2026-01-15T20:14:55.111Z" }, + { url = "https://files.pythonhosted.org/packages/7d/46/2bdcb34a87a179a4d23022d818c1c236cb40e477faf0d7c9afb6813e5876/ruff-0.14.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:591a7f68860ea4e003917d19b5c4f5ac39ff558f162dc753a2c5de897fd5502c", size = 14043686, upload-time = "2026-01-15T20:14:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/1a/a9/5c6a4f56a0512c691cf143371bcf60505ed0f0860f24a85da8bd123b2bf1/ruff-0.14.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:774c77e841cc6e046fc3e91623ce0903d1cd07e3a36b1a9fe79b81dab3de506b", size = 12663837, upload-time = "2026-01-15T20:15:18.921Z" }, + { url = "https://files.pythonhosted.org/packages/fe/bb/b920016ece7651fa7fcd335d9d199306665486694d4361547ccb19394c44/ruff-0.14.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:61f4e40077a1248436772bb6512db5fc4457fe4c49e7a94ea7c5088655dd21ae", size = 12805867, upload-time = "2026-01-15T20:14:59.272Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b3/0bd909851e5696cd21e32a8fc25727e5f58f1934b3596975503e6e85415c/ruff-0.14.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6d02f1428357fae9e98ac7aa94b7e966fd24151088510d32cf6f902d6c09235e", size = 13208528, upload-time = "2026-01-15T20:15:03.732Z" }, + { url = "https://files.pythonhosted.org/packages/3b/3b/e2d94cb613f6bbd5155a75cbe072813756363eba46a3f2177a1fcd0cd670/ruff-0.14.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e399341472ce15237be0c0ae5fbceca4b04cd9bebab1a2b2c979e015455d8f0c", size = 13929242, upload-time = "2026-01-15T20:15:11.918Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/abd840d4132fd51a12f594934af5eba1d5d27298a6f5b5d6c3be45301caf/ruff-0.14.13-py3-none-win32.whl", hash = "sha256:ef720f529aec113968b45dfdb838ac8934e519711da53a0456038a0efecbd680", size = 12919024, upload-time = "2026-01-15T20:14:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/c2/55/6384b0b8ce731b6e2ade2b5449bf07c0e4c31e8a2e68ea65b3bafadcecc5/ruff-0.14.13-py3-none-win_amd64.whl", hash = "sha256:6070bd026e409734b9257e03e3ef18c6e1a216f0435c6751d7a8ec69cb59abef", size = 14097887, upload-time = "2026-01-15T20:15:01.48Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/7348090988095e4e39560cfc2f7555b1b2a7357deba19167b600fdf5215d/ruff-0.14.13-py3-none-win_arm64.whl", hash = "sha256:7ab819e14f1ad9fe39f246cfcc435880ef7a9390d81a2b6ac7e01039083dd247", size = 13080224, upload-time = "2026-01-15T20:14:45.853Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "time-machine" +version = "2.19.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "python-dateutil", marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/a4/1b5fdd165f61b67f445fac2a7feb0c655118edef429cd09ff5a8067f7f1d/time_machine-2.19.0.tar.gz", hash = "sha256:7c5065a8b3f2bbb449422c66ef71d114d3f909c276a6469642ecfffb6a0fcd29", size = 14576, upload-time = "2025-08-19T17:22:08.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/8f/19125611ebbcb3a14da14cd982b9eb4573e2733db60c9f1fbf6a39534f40/time_machine-2.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b5169018ef47206997b46086ce01881cd3a4666fd2998c9d76a87858ca3e49e9", size = 19659, upload-time = "2025-08-19T17:20:30.062Z" }, + { url = "https://files.pythonhosted.org/packages/74/da/9b0a928321e7822a3ff96dbd1eae089883848e30e9e1b149b85fb96ba56b/time_machine-2.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85bb7ed440fccf6f6d0c8f7d68d849e7c3d1f771d5e0b2cdf871fa6561da569f", size = 15157, upload-time = "2025-08-19T17:20:31.931Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/d7e943422038f5f2161fe2c2d791e64a45be691ef946020b20f3a6efc4d4/time_machine-2.19.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a3b12028af1cdc09ccd595be2168b7b26f206c1e190090b048598fbe278beb8e", size = 32860, upload-time = "2025-08-19T17:20:33.241Z" }, + { url = "https://files.pythonhosted.org/packages/fc/80/2b0f1070ed9808ee7da7a6da62a4a0b776957cb4d861578348f86446e778/time_machine-2.19.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c261f073086cf081d1443cbf7684148c662659d3d139d06b772bfe3fe7cc71a6", size = 34510, upload-time = "2025-08-19T17:20:34.221Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b4/48038691c8d89924b36c83335a73adeeb68c884f5a1da08a5b17b8a956f3/time_machine-2.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:011954d951230a9f1079f22b39ed1a3a9abb50ee297dfb8c557c46351659d94d", size = 36204, upload-time = "2025-08-19T17:20:35.163Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/60e8adb541df195e83cb74b720b2cfb1f22ed99c5a7f8abf2a9ab3442cb5/time_machine-2.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b0f83308b29c7872006803f2e77318874eb84d0654f2afe0e48e3822e7a2e39b", size = 34936, upload-time = "2025-08-19T17:20:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/e8cee59c6cd99dd3b25b8001a0253e779a286aa8f44d5b40777cbd66210b/time_machine-2.19.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:39733ef844e2984620ec9382a42d00cccc4757d75a5dd572be8c2572e86e50b9", size = 32932, upload-time = "2025-08-19T17:20:37.901Z" }, + { url = "https://files.pythonhosted.org/packages/2c/eb/83f300d93c1504965d944e03679f1c943a923bce2d0fdfadef0e2e22cc13/time_machine-2.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f8db99f6334432e9ffbf00c215caf2ae9773f17cec08304d77e9e90febc3507b", size = 34010, upload-time = "2025-08-19T17:20:39.202Z" }, + { url = "https://files.pythonhosted.org/packages/e1/77/f35f2500e04daac5033a22fbfd17e68467822b8406ee77966bf222ccaa26/time_machine-2.19.0-cp310-cp310-win32.whl", hash = "sha256:72bf66cd19e27ffd26516b9cbe676d50c2e0b026153289765dfe0cf406708128", size = 17121, upload-time = "2025-08-19T17:20:40.108Z" }, + { url = "https://files.pythonhosted.org/packages/db/df/32d3e0404be1760a64a44caab2af34b07e952bfe00a23134fea9ddba3e8a/time_machine-2.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:46f1c945934ce3d6b4f388b8e581fce7f87ec891ea90d7128e19520e434f96f0", size = 17957, upload-time = "2025-08-19T17:20:41.079Z" }, + { url = "https://files.pythonhosted.org/packages/66/df/598a71a1afb4b509a4587273b76590b16d9110a3e9106f01eedc68d02bb2/time_machine-2.19.0-cp310-cp310-win_arm64.whl", hash = "sha256:fb4897c7a5120a4fd03f0670f332d83b7e55645886cd8864a71944c4c2e5b35b", size = 16821, upload-time = "2025-08-19T17:20:41.967Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ed/4815ebcc9b6c14273f692b9be38a9b09eae52a7e532407cc61a51912b121/time_machine-2.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5ee91664880434d98e41585c3446dac7180ec408c786347451ddfca110d19296", size = 19342, upload-time = "2025-08-19T17:20:43.207Z" }, + { url = "https://files.pythonhosted.org/packages/ee/08/154cce8b11b60d8238b0b751b8901d369999f4e8f7c3a5f917caa5d95b0b/time_machine-2.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed3732b83a893d1c7b8cabde762968b4dc5680ee0d305b3ecca9bb516f4e3862", size = 14978, upload-time = "2025-08-19T17:20:44.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b7/b689d8c8eeca7af375cfcd64973e49e83aa817cc00f80f98548d42c0eb50/time_machine-2.19.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6ba0303e9cc9f7f947e344f501e26bedfb68fab521e3c2729d370f4f332d2d55", size = 30964, upload-time = "2025-08-19T17:20:45.366Z" }, + { url = "https://files.pythonhosted.org/packages/80/91/38bf9c79674e95ce32e23c267055f281dff651eec77ed32a677db3dc011a/time_machine-2.19.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2851825b524a988ee459c37c1c26bdfaa7eff78194efb2b562ea497a6f375b0a", size = 32606, upload-time = "2025-08-19T17:20:46.693Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/e9222d85d4de68975a5e799f539a9d32f3a134a9101fca0a61fa6aa33d68/time_machine-2.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68d32b09ecfd7fef59255c091e8e7c24dd117f882c4880b5c7ab8c5c32a98f89", size = 34405, upload-time = "2025-08-19T17:20:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/09480d608d42d6876f9ff74593cfc9197a7eb2c31381a74fb2b145575b65/time_machine-2.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60c46ab527bf2fa144b530f639cc9e12803524c9e1f111dc8c8f493bb6586eeb", size = 33181, upload-time = "2025-08-19T17:20:48.937Z" }, + { url = "https://files.pythonhosted.org/packages/84/64/f9359e000fad32d9066305c48abc527241d608bcdf77c19d67d66e268455/time_machine-2.19.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:56f26ab9f0201c453d18fe76bb7d1cf05fe58c1b9d9cb0c7d243d05132e01292", size = 31036, upload-time = "2025-08-19T17:20:50.276Z" }, + { url = "https://files.pythonhosted.org/packages/71/0d/fab2aacec71e3e482bd7fce0589381f9414a4a97f8766bddad04ad047b7b/time_machine-2.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6c806cf3c1185baa1d807b7f51bed0db7a6506832c961d5d1b4c94c775749bc0", size = 32145, upload-time = "2025-08-19T17:20:51.449Z" }, + { url = "https://files.pythonhosted.org/packages/44/fb/faeba2405fb27553f7b28db441a500e2064ffdb2dcba001ee315fdd2c121/time_machine-2.19.0-cp311-cp311-win32.whl", hash = "sha256:b30039dfd89855c12138095bee39c540b4633cbc3684580d684ef67a99a91587", size = 17004, upload-time = "2025-08-19T17:20:52.38Z" }, + { url = "https://files.pythonhosted.org/packages/2f/84/87e483d660ca669426192969280366635c845c3154a9fe750be546ed3afc/time_machine-2.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:13ed8b34430f1de79905877f5600adffa626793ab4546a70a99fb72c6a3350d8", size = 17822, upload-time = "2025-08-19T17:20:53.348Z" }, + { url = "https://files.pythonhosted.org/packages/41/f4/ebf7bbf5047854a528adaf54a5e8780bc5f7f0104c298ab44566a3053bf8/time_machine-2.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:cc29a50a0257d8750b08056b66d7225daab47606832dea1a69e8b017323bf511", size = 16680, upload-time = "2025-08-19T17:20:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/9b/aa/7e00614d339e4d687f6e96e312a1566022528427d237ec639df66c4547bc/time_machine-2.19.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c85cf437dc3c07429456d8d6670ac90ecbd8241dcd0fbf03e8db2800576f91ff", size = 19308, upload-time = "2025-08-19T17:20:55.25Z" }, + { url = "https://files.pythonhosted.org/packages/ab/3c/bde3c757394f5bca2fbc1528d4117960a26c38f9b160bf471b38d2378d8f/time_machine-2.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d9238897e8ef54acdf59f5dff16f59ca0720e7c02d820c56b4397c11db5d3eb9", size = 15019, upload-time = "2025-08-19T17:20:56.204Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e0/8ca916dd918018352d377f1f5226ee071cfbeb7dbbde2b03d14a411ac2b1/time_machine-2.19.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e312c7d5d6bfffb96c6a7b39ff29e3046de100d7efaa3c01552654cfbd08f14c", size = 33079, upload-time = "2025-08-19T17:20:57.166Z" }, + { url = "https://files.pythonhosted.org/packages/48/69/184a0209f02dd0cb5e01e8d13cd4c97a5f389c4e3d09b95160dd676ad1e7/time_machine-2.19.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:714c40b2c90d1c57cc403382d5a9cf16e504cb525bfe9650095317da3c3d62b5", size = 34925, upload-time = "2025-08-19T17:20:58.117Z" }, + { url = "https://files.pythonhosted.org/packages/43/42/4bbf4309e8e57cea1086eb99052d97ff6ddecc1ab6a3b07aa4512f8bf963/time_machine-2.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eaa1c675d500dc3ccae19e9fb1feff84458a68c132bbea47a80cc3dd2df7072", size = 36384, upload-time = "2025-08-19T17:20:59.108Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/9f510dc1719157348c1a2e87423aed406589070b54b503cb237d9bf3a4fe/time_machine-2.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e77a414e9597988af53b2b2e67242c9d2f409769df0d264b6d06fda8ca3360d4", size = 34881, upload-time = "2025-08-19T17:21:00.116Z" }, + { url = "https://files.pythonhosted.org/packages/ca/28/61764a635c70cc76c76ba582dfdc1a84834cddaeb96789023af5214426b2/time_machine-2.19.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd93996970e11c382b04d4937c3cd0b0167adeef14725ece35aae88d8a01733c", size = 32931, upload-time = "2025-08-19T17:21:01.095Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e0/f028d93b266e6ade8aca5851f76ebbc605b2905cdc29981a2943b43e1a6c/time_machine-2.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8e20a6d8d6e23174bd7e931e134d9610b136db460b249d07e84ecdad029ec352", size = 34241, upload-time = "2025-08-19T17:21:02.052Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a6/36d1950ed1d3f613158024cf1dcc73db1d9ef0b9117cf51ef2e37dc06499/time_machine-2.19.0-cp312-cp312-win32.whl", hash = "sha256:95afc9bc65228b27be80c2756799c20b8eb97c4ef382a9b762b6d7888bc84099", size = 17021, upload-time = "2025-08-19T17:21:03.374Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0d/e2dce93355abda3cac69e77fe96566757e98b8fe7fdcbddce89c9ced3f5f/time_machine-2.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:e84909af950e2448f4e2562ea5759c946248c99ab380d2b47d79b62bd76fa236", size = 17857, upload-time = "2025-08-19T17:21:04.331Z" }, + { url = "https://files.pythonhosted.org/packages/eb/28/50ae6fb83b7feeeca7a461c0dc156cf7ef5e6ef594a600d06634fde6a2cb/time_machine-2.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:0390a1ea9fa7e9d772a39b7c61b34fdcca80eb9ffac339cc0441c6c714c81470", size = 16677, upload-time = "2025-08-19T17:21:05.39Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b8/24ebce67aa531bae2cbe164bb3f4abc6467dc31f3aead35e77f5a075ea3e/time_machine-2.19.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5e172866753e6041d3b29f3037dc47c20525176a494a71bbd0998dfdc4f11f2f", size = 19373, upload-time = "2025-08-19T17:21:06.701Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/c9a5240fd2f845d3ff9fa26f8c8eaa29f7239af9d65007e61d212250f15b/time_machine-2.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f70f68379bd6f542ae6775cce9a4fa3dcc20bf7959c42eaef871c14469e18097", size = 15056, upload-time = "2025-08-19T17:21:07.667Z" }, + { url = "https://files.pythonhosted.org/packages/b9/92/66cce5d2fb2a5e68459aca85fd18a7e2d216f725988940cd83f96630f2f1/time_machine-2.19.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e69e0b0f694728a00e72891ef8dd00c7542952cb1c87237db594b6b27d504a96", size = 33172, upload-time = "2025-08-19T17:21:08.619Z" }, + { url = "https://files.pythonhosted.org/packages/ae/20/b499e9ab4364cd466016c33dcdf4f56629ca4c20b865bd4196d229f31d92/time_machine-2.19.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3ae0a8b869574301ec5637e32c270c7384cca5cd6e230f07af9d29271a7fa293", size = 35042, upload-time = "2025-08-19T17:21:09.622Z" }, + { url = "https://files.pythonhosted.org/packages/41/32/b252d3d32791eb16c07d553c820dbc33d9c7fa771de3d1c602190bded2b7/time_machine-2.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:554e4317de90e2f7605ff80d153c8bb56b38c0d0c0279feb17e799521e987b8c", size = 36535, upload-time = "2025-08-19T17:21:10.571Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/4d0470062b9742e1b040ab81bad04d1a5d1de09806507bb6188989cfa1a7/time_machine-2.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6567a5ec5538ed550539ac29be11b3cb36af1f9894e2a72940cba0292cc7c3c9", size = 34945, upload-time = "2025-08-19T17:21:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/24/71/2f741b29d98b1c18f6777a32236497c3d3264b6077e431cea4695684c8a1/time_machine-2.19.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82e9ffe8dfff07b0d810a2ad015a82cd78c6a237f6c7cf185fa7f747a3256f8a", size = 33014, upload-time = "2025-08-19T17:21:12.858Z" }, + { url = "https://files.pythonhosted.org/packages/e8/83/ca8dba6106562843fd99f672e5aaf95badbc10f4f13f7cfe8d8640a7019d/time_machine-2.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7e1c4e578cdd69b3531d8dd3fbcb92a0cd879dadb912ee37af99c3a9e3c0d285", size = 34350, upload-time = "2025-08-19T17:21:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/21/7f/34fe540450e18d0a993240100e4b86e8d03d831b92af8bb6ddb2662dc6fc/time_machine-2.19.0-cp313-cp313-win32.whl", hash = "sha256:72dbd4cbc3d96dec9dd281ddfbb513982102776b63e4e039f83afb244802a9e5", size = 17047, upload-time = "2025-08-19T17:21:14.874Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/c8be73df82c7ebe7cd133279670e89b8b110af3ce1412c551caa9d08e625/time_machine-2.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:e17e3e089ac95f9a145ce07ff615e3c85674f7de36f2d92aaf588493a23ffb4b", size = 17868, upload-time = "2025-08-19T17:21:15.819Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/2dfd3b8fb285308f61cd7aa9bfa96f46ddf916e3549a0f0afd094c556599/time_machine-2.19.0-cp313-cp313-win_arm64.whl", hash = "sha256:149072aff8e3690e14f4916103d898ea0d5d9c95531b6aa0995251c299533f7b", size = 16710, upload-time = "2025-08-19T17:21:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/05/c1/deebb361727d2c5790f9d4d874be1b19afd41f4375581df465e6718b46a2/time_machine-2.19.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f3589fee1ed0ab6ee424a55b0ea1ec694c4ba64cc26895bcd7d99f3d1bc6a28a", size = 20053, upload-time = "2025-08-19T17:21:17.704Z" }, + { url = "https://files.pythonhosted.org/packages/45/e8/fe3376951e6118d8ec1d1f94066a169b791424fe4a26c7dfc069b153ee08/time_machine-2.19.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7887e85275c4975fe54df03dcdd5f38bd36be973adc68a8c77e17441c3b443d6", size = 15423, upload-time = "2025-08-19T17:21:18.668Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c7/f88d95cd1a87c650cf3749b4d64afdaf580297aa18ad7f4b44ec9d252dfc/time_machine-2.19.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ce0be294c209928563fcce1c587963e60ec803436cf1e181acd5bc1e425d554b", size = 39630, upload-time = "2025-08-19T17:21:19.645Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5d/65a5c48a65357e56ec6f032972e4abd1c02d4fca4b0717a3aaefd19014d4/time_machine-2.19.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a62fd1ab380012c86f4c042010418ed45eb31604f4bf4453e17c9fa60bc56a29", size = 41242, upload-time = "2025-08-19T17:21:20.979Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/fe5209e1615fde0a8cad6c4e857157b150333ed1fe31a7632b08cfe0ebdd/time_machine-2.19.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25ec853a4530a5800731257f93206b12cbdee85ede964ebf8011b66086a7914", size = 44278, upload-time = "2025-08-19T17:21:21.984Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/a5e5fe9c5d614cde0a9387ff35e8dfd12c5ef6384e4c1a21b04e6e0b905d/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a430e4d0e0556f021a9c78e9b9f68e5e8910bdace4aa34ed4d1a73e239ed9384", size = 42321, upload-time = "2025-08-19T17:21:23.755Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c5/56eca774e9162bc1ce59111d2bd69140dc8908c9478c92ec7bd15d547600/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2415b7495ec4364c8067071e964fbadfe746dd4cdb43983f2f0bd6ebed13315c", size = 39270, upload-time = "2025-08-19T17:21:26.009Z" }, + { url = "https://files.pythonhosted.org/packages/9b/69/5dd0c420667578169a12acc8c8fd7452e8cfb181e41c9b4ac7e88fa36686/time_machine-2.19.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbfc6b90c10f288594e1bf89a728a98cc0030791fd73541bbdc6b090aff83143", size = 40193, upload-time = "2025-08-19T17:21:27.054Z" }, + { url = "https://files.pythonhosted.org/packages/75/a7/de974d421bd55c9355583427c2a38fb0237bb5fd6614af492ba89dacb2f9/time_machine-2.19.0-cp313-cp313t-win32.whl", hash = "sha256:16f5d81f650c0a4d117ab08036dc30b5f8b262e11a4a0becc458e7f1c011b228", size = 17542, upload-time = "2025-08-19T17:21:28.674Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/aa0d05becd5d06ae8d3f16d657dc8cc9400c8d79aef80299de196467ff12/time_machine-2.19.0-cp313-cp313t-win_amd64.whl", hash = "sha256:645699616ec14e147094f601e6ab9553ff6cea37fad9c42720a6d7ed04bcd5dc", size = 18703, upload-time = "2025-08-19T17:21:29.663Z" }, + { url = "https://files.pythonhosted.org/packages/1f/c0/f785a4c7c73aa176510f7c48b84b49c26be84af0d534deb222e0327f750e/time_machine-2.19.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b32daa965d13237536ea3afaa5ad61ade2b2d9314bc3a20196a0d2e1d7b57c6a", size = 17020, upload-time = "2025-08-19T17:21:30.653Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/c5fb51def06c0b2b6735332ad118ab35b4d9b85368792e5b638e99b1b686/time_machine-2.19.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:31cb43c8fd2d961f31bed0ff4e0026964d2b35e5de9e0fabbfecf756906d3612", size = 19360, upload-time = "2025-08-19T17:21:31.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4e/2d795f7d6b7f5205ffe737a05bb1cf19d8038233b797062b2ef412b8512b/time_machine-2.19.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bdf481a75afc6bff3e520db594501975b652f7def21cd1de6aa971d35ba644e6", size = 15033, upload-time = "2025-08-19T17:21:32.934Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/9bad501e360b4e758c58fae616ca5f8c7ad974b343f2463a15b2bf77a366/time_machine-2.19.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:00bee4bb950ac6a08d62af78e4da0cf2b4fc2abf0de2320d0431bf610db06e7c", size = 33379, upload-time = "2025-08-19T17:21:33.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/45/eda0ca4d793dfd162478d6163759b1c6ce7f6e61daa7fd7d62b31f21f87f/time_machine-2.19.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f02199490906582302ce09edd32394fb393271674c75d7aa76c7a3245f16003", size = 35123, upload-time = "2025-08-19T17:21:34.945Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/97e16325442ae5731fcaac794f0a1ef9980eff8a5491e58201d7eb814a34/time_machine-2.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e35726c7ba625f844c13b1fc0d4f81f394eefaee1d3a094a9093251521f2ef15", size = 36588, upload-time = "2025-08-19T17:21:35.975Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9d/bf0b2ccc930cc4a316f26f1c78d3f313cd0fa13bb7480369b730a8f129db/time_machine-2.19.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:304315023999cd401ff02698870932b893369e1cfeb2248d09f6490507a92e97", size = 35013, upload-time = "2025-08-19T17:21:37.017Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5a/39ac6a3078174f9715d88364871348b249631f12e76de1b862433b3f8862/time_machine-2.19.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9765d4f003f263ea8bfd90d2d15447ca4b3dfa181922cf6cf808923b02ac180a", size = 33303, upload-time = "2025-08-19T17:21:38.352Z" }, + { url = "https://files.pythonhosted.org/packages/b3/ac/d8646baf9f95f2e792a6d7a7b35e92fca253c4a992afff801beafae0e5c2/time_machine-2.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7837ef3fd5911eb9b480909bb93d922737b6bdecea99dfcedb0a03807de9b2d3", size = 34440, upload-time = "2025-08-19T17:21:39.382Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/8b6568c5ae966d80ead03ab537be3c6acf2af06fb501c2d466a3162c6295/time_machine-2.19.0-cp314-cp314-win32.whl", hash = "sha256:4bb5bd43b1bdfac3007b920b51d8e761f024ed465cfeec63ac4296922a4ec428", size = 17162, upload-time = "2025-08-19T17:21:40.381Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/211c1ab4566eba5308b2dc001b6349e3a032e3f6afa67ca2f27ea6b27af5/time_machine-2.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:f583bbd0aa8ab4a7c45a684bf636d9e042d466e30bcbae1d13e7541e2cbe7207", size = 18040, upload-time = "2025-08-19T17:21:41.363Z" }, + { url = "https://files.pythonhosted.org/packages/b8/fc/4c2fb705f6371cb83824da45a8b967514a922fc092a0ef53979334d97a70/time_machine-2.19.0-cp314-cp314-win_arm64.whl", hash = "sha256:f379c6f8a6575a8284592179cf528ce89373f060301323edcc44f1fa1d37be12", size = 16752, upload-time = "2025-08-19T17:21:42.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/ab/6437d18f31c666b5116c97572a282ac2590a82a0a9867746a6647eaf4613/time_machine-2.19.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a3b8981f9c663b0906b05ab4d0ca211fae4b63b47c6ec26de5374fe56c836162", size = 20057, upload-time = "2025-08-19T17:21:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a2/e03639ec2ba7200328bbcad8a2b2b1d5fccca9cceb9481b164a1cabdcb33/time_machine-2.19.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8e9c6363893e7f52c226afbebb23e825259222d100e67dfd24c8a6d35f1a1907", size = 15430, upload-time = "2025-08-19T17:21:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ff/39e63a48e840f3e36ce24846ee51dd99c6dba635659b1750a2993771e88e/time_machine-2.19.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:206fcd6c9a6f00cac83db446ad1effc530a8cec244d2780af62db3a2d0a9871b", size = 39622, upload-time = "2025-08-19T17:21:45.821Z" }, + { url = "https://files.pythonhosted.org/packages/9a/2e/ee5ac79c4954768705801e54817c7d58e07e25a0bb227e775f501f3e2122/time_machine-2.19.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf33016a1403c123373ffaeff25e26e69d63bf2c63b6163932efed94160db7ef", size = 41235, upload-time = "2025-08-19T17:21:46.783Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3e/9af5f39525e779185c77285b8bbae15340eeeaa0afb33d458bc8b47d459b/time_machine-2.19.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9247c4bb9bbd3ff584ef4efbdec8efd9f37aa08bcfc4728bde1e489c2cb445bd", size = 44276, upload-time = "2025-08-19T17:21:47.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/572c7443cc27140bbeae3947279bbd4a120f9e8622253a20637f260b7813/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:77f9bb0b86758d1f2d9352642c874946ad5815df53ef4ca22eb9d532179fe50d", size = 42330, upload-time = "2025-08-19T17:21:48.881Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/1a81c2e08ee7dae13ec8ceed27a29afa980c3d63852e42f1e023bf0faa03/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0b529e262df3b9c449f427385f4d98250828c879168c2e00eec844439f40b370", size = 39281, upload-time = "2025-08-19T17:21:49.907Z" }, + { url = "https://files.pythonhosted.org/packages/d2/60/6f0d6e5108978ca1a2a4ffb4d1c7e176d9199bb109fd44efe2680c60b52a/time_machine-2.19.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9199246e31cdc810e5d89cb71d09144c4d745960fdb0824da4994d152aca3303", size = 40201, upload-time = "2025-08-19T17:21:50.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/b9/3ea4951e8293b0643feb98c0b9a176fa822154f1810835db3f282968ab10/time_machine-2.19.0-cp314-cp314t-win32.whl", hash = "sha256:0fe81bae55b7aefc2c2a34eb552aa82e6c61a86b3353a3c70df79b9698cb02ca", size = 17743, upload-time = "2025-08-19T17:21:51.948Z" }, + { url = "https://files.pythonhosted.org/packages/e4/8b/cd802884ca8a98e2b6cdc2397d57dd12ff8a7d1481e06fc3fad3d4e7e5ff/time_machine-2.19.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7253791b8d7e7399fbeed7a8193cb01bc004242864306288797056badbdaf80b", size = 18956, upload-time = "2025-08-19T17:21:52.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/49/cabb1593896082fd55e34768029b8b0ca23c9be8b2dc127e0fc14796d33e/time_machine-2.19.0-cp314-cp314t-win_arm64.whl", hash = "sha256:536bd1ac31ab06a1522e7bf287602188f502dc19d122b1502c4f60b1e8efac79", size = 17068, upload-time = "2025-08-19T17:21:54.064Z" }, + { url = "https://files.pythonhosted.org/packages/d6/05/0608376c3167afe6cf7cdfd2b05c142ea4c42616eee9ba06d1799965806a/time_machine-2.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8bb00b30ec9fe56d01e9812df1ffe39f331437cef9bfaebcc81c83f7f8f8ee2", size = 19659, upload-time = "2025-08-19T17:21:55.426Z" }, + { url = "https://files.pythonhosted.org/packages/11/c4/72eb8c7b36830cf36c51d7bc2f1ac313d68881c3a58040fb6b42c4523d20/time_machine-2.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d821c60efc08a97cc11e5482798e6fd5eba5c0f22a02db246b50895dbdc0de41", size = 15153, upload-time = "2025-08-19T17:21:56.505Z" }, + { url = "https://files.pythonhosted.org/packages/89/1a/0782e1f5c8ab8809ebd992709e1bb69d67600191baa023af7a5d32023a3c/time_machine-2.19.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb051aec7b3b6e96a200d911c225901e6133ff3da11e470e24111a53bbc13637", size = 32555, upload-time = "2025-08-19T17:21:57.74Z" }, + { url = "https://files.pythonhosted.org/packages/94/b0/8ef58e2f6321851d5900ca3d18044938832c2ed42a2ac7570ca6aa29768a/time_machine-2.19.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe59909d95a2ef5e01ce3354fdea3908404c2932c2069f00f66dff6f27e9363e", size = 34185, upload-time = "2025-08-19T17:21:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/82/74/ce0c9867f788c1fb22c417ec1aae47a24117e53d51f6ff97d7c6ca5392f6/time_machine-2.19.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29e84b8682645b16eb6f9e8ec11c35324ad091841a11cf4fc3fc7f6119094c89", size = 35917, upload-time = "2025-08-19T17:22:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/d2/70/6f97a8f552dbaa66feb10170b5726dab74bc531673d1ed9d6f271547e54c/time_machine-2.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a11f1c0e0d06023dc01614c964e256138913551d3ae6dca5148f79081156336", size = 34584, upload-time = "2025-08-19T17:22:01.447Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/cf139088ce537c15d7f03cf56ec317d3a5cfb520e30aa711ea0248d0ae8a/time_machine-2.19.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:57a235a6307c54df50e69f1906e2f199e47da91bde4b886ee05aff57fe4b6bf6", size = 32608, upload-time = "2025-08-19T17:22:02.548Z" }, + { url = "https://files.pythonhosted.org/packages/b1/17/0ec41ef7a30c6753fb226a28b74162b264b35724905ced4098f2f5076ded/time_machine-2.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:426aba552f7af9604adad9ef570c859af7c1081d878db78089fac159cd911b0a", size = 33686, upload-time = "2025-08-19T17:22:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/b0/19/586f15159083ec84f178d494c60758c46603b00c9641b04deb63f1950128/time_machine-2.19.0-cp39-cp39-win32.whl", hash = "sha256:67772c7197a3a712d1b970ed545c6e98db73524bd90e245fd3c8fa7ad7630768", size = 17133, upload-time = "2025-08-19T17:22:04.989Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/bfe4b906a9fe0bf2d011534314212ed752d6b8f392c9c82f6ac63dccc5ab/time_machine-2.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:011d7859089263204dc5fdf83dce7388f986fe833c9381d6106b4edfda2ebd3e", size = 17972, upload-time = "2025-08-19T17:22:06.026Z" }, + { url = "https://files.pythonhosted.org/packages/5d/73/182343eba05aa5787732aaa68f3b3feb5e40ddf86b928ae941be45646393/time_machine-2.19.0-cp39-cp39-win_arm64.whl", hash = "sha256:e1af66550fa4685434f00002808a525f176f1f92746646c0019bb86fbff48b27", size = 16820, upload-time = "2025-08-19T17:22:07.227Z" }, +] + +[[package]] +name = "time-machine" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and python_full_version < '3.14' and extra != 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra == 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", + "python_full_version >= '3.10' and extra != 'group-15-vitable-connect-pydantic-v1' and extra != 'group-15-vitable-connect-pydantic-v2'", +] +sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/31/6bf41cb4a326230518d9b76c910dfc11d4fc23444d1cbfdf2d7652bd99f4/time_machine-3.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:68142c070e78b62215d8029ec7394905083a4f9aacb0a2a11514ce70b5951b13", size = 19447, upload-time = "2025-12-17T23:31:30.181Z" }, + { url = "https://files.pythonhosted.org/packages/fa/14/d71ce771712e1cbfa15d8c24452225109262b16cb6caaf967e9f60662b67/time_machine-3.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:161bbd0648802ffdfcb4bb297ecb26b3009684a47d3a4dedb90bc549df4fa2ad", size = 15432, upload-time = "2025-12-17T23:31:31.381Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/dcb43a11f8029561996fad58ff9d3dc5e6d7f32b74f0745a2965d7e4b4f3/time_machine-3.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1359ba8c258be695ba69253bc84db882fd616fe69b426cc6056536da2c7bf68e", size = 32956, upload-time = "2025-12-17T23:31:32.469Z" }, + { url = "https://files.pythonhosted.org/packages/77/da/d802cd3c335c414f9b11b479f7459aa72df5de6485c799966cfdf8856d53/time_machine-3.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c85b169998ca2c24a78fb214586ec11c4cad56d9c38f55ad8326235cb481c884", size = 34556, upload-time = "2025-12-17T23:31:33.946Z" }, + { url = "https://files.pythonhosted.org/packages/85/ee/51ad553514ab0b940c7c82c6e1519dd10fd06ac07b32039a1d153ef09c88/time_machine-3.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65b9367cb8a10505bc8f67da0da514ba20fa816fc47e11f434f7c60350322b4c", size = 36101, upload-time = "2025-12-17T23:31:35.462Z" }, + { url = "https://files.pythonhosted.org/packages/11/39/938b111b5bb85a2b07502d0f9d8a704fc75bd760d62e76bce23c89ed16c9/time_machine-3.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9faca6a0f1973d7df3233c951fc2a11ff0c54df74087d8aaf41ae3deb19d0893", size = 34905, upload-time = "2025-12-17T23:31:36.543Z" }, + { url = "https://files.pythonhosted.org/packages/dd/50/0951f73b23e76455de0b4a3a58ac5a24bd8d10489624b1c5e03f10c6fc0b/time_machine-3.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:213b1ada7f385d467e598999b642eda4a8e89ae10ad5dc4f5d8f672cbf604261", size = 33012, upload-time = "2025-12-17T23:31:37.967Z" }, + { url = "https://files.pythonhosted.org/packages/4f/95/5304912d3dcecc4e14ed222dbe0396352efdf8497534abc3c9edd67a7528/time_machine-3.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:160b6afd94c39855af04d39c58e4cf602406abd6d79427ab80e830ea71789cfb", size = 34104, upload-time = "2025-12-17T23:31:39.449Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/af56518652ec7adac4ced193b7a42c4ff354fef28a412b3b5ffa5763aead/time_machine-3.2.0-cp310-cp310-win32.whl", hash = "sha256:c15d9ac257c78c124d112e4fc91fa9f3dcb004bdda913c19f0e7368d713cf080", size = 17468, upload-time = "2025-12-17T23:31:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/0213f00ca3cf6fe1c9fdbd7fd467e801052fc85534f30c0e4684bd474190/time_machine-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:3bf0f428487f93b8fe9d27aa01eccc817885da3290b467341b4a4a795e1d1891", size = 18313, upload-time = "2025-12-17T23:31:41.617Z" }, + { url = "https://files.pythonhosted.org/packages/77/e4/811f96aa7a634b2b264d9a476f3400e710744dda503b4ad87a5c76db32c9/time_machine-3.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:347f6be2129fcd35b1c94b9387fcb2cbe7949b1e649228c5f22949a811b78976", size = 17037, upload-time = "2025-12-17T23:31:42.924Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e1/03aae5fbaa53859f665094af696338fc7cae733d926a024af69982712350/time_machine-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c188a9dda9fcf975022f1b325b466651b96a4dfc223c523ed7ed8d979f9bf3e8", size = 19143, upload-time = "2025-12-17T23:31:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/98cb17bebb52b22ff4ec26984dd44280f9c71353c3bae0640a470e6683e5/time_machine-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17245f1cc2dd13f9d63a174be59bb2684a9e5e0a112ab707e37be92068cd655f", size = 15273, upload-time = "2025-12-17T23:31:45.246Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2f/ca11e4a7897234bb9331fcc5f4ed4714481ba4012370cc79a0ae8c42ea0a/time_machine-3.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9bd1de1996e76efd36ae15970206c5089fb3728356794455bd5cd8d392b5537", size = 31049, upload-time = "2025-12-17T23:31:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/d17d83a59943094e6b6c6a3743caaf6811b12203c3e07a30cc7bcc2ab7ee/time_machine-3.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98493cd50e8b7f941eab69b9e18e697ad69db1a0ec1959f78f3d7b0387107e5c", size = 32632, upload-time = "2025-12-17T23:31:47.72Z" }, + { url = "https://files.pythonhosted.org/packages/71/50/d60576d047a0dfb5638cdfb335e9c3deb6e8528544fa0b3966a8480f72b7/time_machine-3.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f2a33d595d9f91eb9bc7f157f0dc5721f5789f4c4a9e8b852cdedb2a7d9b16", size = 34289, upload-time = "2025-12-17T23:31:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/fa/fe/4afa602dbdebddde6d0ea4a7fe849e49b9bb85dc3fb415725a87ccb4b471/time_machine-3.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f78ac4213c10fbc44283edd1a29cfb7d3382484f4361783ddc057292aaa1889", size = 33175, upload-time = "2025-12-17T23:31:50.611Z" }, + { url = "https://files.pythonhosted.org/packages/0d/87/c152e23977c1d7d7c94eb3ed3ea45cc55971796205125c6fdff40db2c60f/time_machine-3.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c1326b09e947b360926d529a96d1d9e126ce120359b63b506ecdc6ee20755c23", size = 31170, upload-time = "2025-12-17T23:31:51.645Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/54acf51d0f3ade3b51eab73df6192937c9a938753ef5456dff65eb8630be/time_machine-3.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f2949f03d15264cc15c38918a2cda8966001f0f4ebe190cbfd9c56d91aed8ac", size = 32292, upload-time = "2025-12-17T23:31:52.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/bc/3745963f36e75661a807196428639327a366f4332f35f1f775c074d4062f/time_machine-3.2.0-cp311-cp311-win32.whl", hash = "sha256:6dfe48e0499e6e16751476b9799e67be7514e6ef04cdf39571ef95a279645831", size = 17349, upload-time = "2025-12-17T23:31:54.19Z" }, + { url = "https://files.pythonhosted.org/packages/82/a2/057469232a99d1f5a0160ae7c5bae7b095c9168b333dd598fcbcfbc1c87b/time_machine-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:809bdf267a29189c304154873620fe0bcc0c9513295fa46b19e21658231c4915", size = 18191, upload-time = "2025-12-17T23:31:55.472Z" }, + { url = "https://files.pythonhosted.org/packages/79/d8/bf9c8de57262ee7130d92a6ed49ed6a6e40a36317e46979428d373630c12/time_machine-3.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:a3f4c17fa90f54902a3f8692c75caf67be87edc3429eeb71cb4595da58198f8e", size = 16905, upload-time = "2025-12-17T23:31:56.658Z" }, + { url = "https://files.pythonhosted.org/packages/71/8b/080c8eedcd67921a52ba5bd0e075362062509ab63c86fc1a0442fad241a6/time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb", size = 19255, upload-time = "2025-12-17T23:31:58.057Z" }, + { url = "https://files.pythonhosted.org/packages/66/17/0e5291e9eb705bf8a5a1305f826e979af307bbeb79def4ddbf4b3f9a81e0/time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1", size = 15360, upload-time = "2025-12-17T23:31:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/9ab87b71d2e2b62463b9b058b7ae7ac09fb57f8fcd88729dec169d304340/time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb", size = 33029, upload-time = "2025-12-17T23:32:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/b5ca19da6f25ea905b3e10a0ea95d697c1aeba0404803a43c68f1af253e6/time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d", size = 34579, upload-time = "2025-12-17T23:32:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/79/ca/6ac7ad5f10ea18cc1d9de49716ba38c32132c7b64532430d92ef240c116b/time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7", size = 35961, upload-time = "2025-12-17T23:32:02.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/390dd958bed395ab32d79a9fe61fe111825c0dd4ded54dbba7e867f171e6/time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa", size = 34668, upload-time = "2025-12-17T23:32:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/da/57/c88fff034a4e9538b3ae7c68c9cfb283670b14d17522c5a8bc17d29f9a4b/time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50", size = 32891, upload-time = "2025-12-17T23:32:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/ebbb76022dba0fec8f9156540fc647e4beae1680c787c01b1b6200e56d70/time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548", size = 34080, upload-time = "2025-12-17T23:32:06.146Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/2ca9e7af3df540dc1c79e3de588adeddb7dcc2107829248e6969c4f14167/time_machine-3.2.0-cp312-cp312-win32.whl", hash = "sha256:3f74623648b936fdce5f911caf386c0a0b579456410975de8c0dfeaaffece1d8", size = 17371, upload-time = "2025-12-17T23:32:07.164Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ce/21d23efc9c2151939af1b7ee4e60d86d661b74ef32b8eaa148f6fe8c899c/time_machine-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:34e26a41d994b5e4b205136a90e9578470386749cc9a2ecf51ca18f83ce25e23", size = 18132, upload-time = "2025-12-17T23:32:08.447Z" }, + { url = "https://files.pythonhosted.org/packages/2f/34/c2b70be483accf6db9e5d6c3139bce3c38fe51f898ccf64e8d3fe14fbf4d/time_machine-3.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:0615d3d82c418d6293f271c348945c5091a71f37e37173653d5c26d0e74b13a8", size = 16930, upload-time = "2025-12-17T23:32:09.477Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/43ad5efc88298af3c59b66769cea7f055567a85071579ed40536188530c1/time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090", size = 19318, upload-time = "2025-12-17T23:32:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f6/084010ef7f4a3f38b5a4900923d7c85b29e797655c4f6ee4ce54d903cca8/time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b", size = 15390, upload-time = "2025-12-17T23:32:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/1cabb74134f492270dc6860cb7865859bf40ecf828be65972827646e91ad/time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0", size = 33115, upload-time = "2025-12-17T23:32:13.219Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/78c5d7dfa366924eb4dbfcc3fc917c39a4280ca234b12819cc1f16c03d88/time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7", size = 34705, upload-time = "2025-12-17T23:32:14.29Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/d5e877c24541f674c6869ff6e9c56833369796010190252e92c9d7ae5f0f/time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1", size = 36104, upload-time = "2025-12-17T23:32:15.354Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/d4bae72f388f67efc9609f89b012e434bb19d9549c7a7b47d6c7d9e5c55d/time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10", size = 34765, upload-time = "2025-12-17T23:32:16.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c3/ac378cf301d527d8dfad2f0db6bad0dfb1ab73212eaa56d6b96ee5d9d20b/time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53", size = 33010, upload-time = "2025-12-17T23:32:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/06/35/7ce897319accda7a6970b288a9a8c52d25227342a7508505a2b3d235b649/time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5", size = 34185, upload-time = "2025-12-17T23:32:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/bf/28/f922022269749cb02eee2b62919671153c4088994fa955a6b0e50327ff81/time_machine-3.2.0-cp313-cp313-win32.whl", hash = "sha256:a66fe55a107e46916007a391d4030479df8864ec6ad6f6a6528221befc5c886e", size = 17397, upload-time = "2025-12-17T23:32:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/ee/dc/fd87cde397f4a7bea493152f0aca8fd569ec709cad9e0f2ca7011eb8c7f7/time_machine-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:30c9ce57165df913e4f74e285a8ab829ff9b7aa3e5ec0973f88f642b9a7b3d15", size = 18139, upload-time = "2025-12-17T23:32:20.991Z" }, + { url = "https://files.pythonhosted.org/packages/75/81/b8ce58233addc5d7d54d2fabc49dcbc02d79e3f079d150aa1bec3d5275ef/time_machine-3.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:89cad7e179e9bdcc84dcf09efe52af232c4cc7a01b3de868356bbd59d95bd9b8", size = 16964, upload-time = "2025-12-17T23:32:22.075Z" }, + { url = "https://files.pythonhosted.org/packages/67/e7/487f0ba5fe6c58186a5e1af2a118dfa2c160fedb37ef53a7e972d410408e/time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c", size = 20000, upload-time = "2025-12-17T23:32:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/eb2c0054c8d44dd42df84ccd434539249a9c7d0b8eb53f799be2102500ab/time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003", size = 15657, upload-time = "2025-12-17T23:32:24.125Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/93443b5d1dd850f8bb9442e90d817a9033dcce6bfbdd3aabbb9786251c80/time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e", size = 39216, upload-time = "2025-12-17T23:32:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/18544cf8acc72bb1dc03762231c82ecc259733f4bb6770a7bbe5cd138603/time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87", size = 40764, upload-time = "2025-12-17T23:32:26.643Z" }, + { url = "https://files.pythonhosted.org/packages/27/f7/9fe9ce2795636a3a7467307af6bdf38bb613ddb701a8a5cd50ec713beb5e/time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53", size = 43526, upload-time = "2025-12-17T23:32:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/a93e975ba9dec22e87ec92d18c28e67d36bd536f9119ffa439b2892b0c9c/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3", size = 41727, upload-time = "2025-12-17T23:32:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fb/e3633e5a6bbed1c76bb2e9810dabc2f8467532ffcd29b9aed404b473061a/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013", size = 38952, upload-time = "2025-12-17T23:32:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/3d/02e9fb2526b3d6b1b45bc8e4d912d95d1cd699d1a3f6df985817d37a0600/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a", size = 39829, upload-time = "2025-12-17T23:32:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/85/c8/c14265212436da8e0814c45463987b3f57de3eca4de023cc2eabb0c62ef3/time_machine-3.2.0-cp313-cp313t-win32.whl", hash = "sha256:3498719f8dab51da76d29a20c1b5e52ee7db083dddf3056af7fa69c1b94e1fe6", size = 17852, upload-time = "2025-12-17T23:32:32.079Z" }, + { url = "https://files.pythonhosted.org/packages/1d/bc/8acb13cf6149f47508097b158a9a8bec9ec4530a70cb406124e8023581f5/time_machine-3.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e0d90bee170b219e1d15e6a58164aa808f5170090e4f090bd0670303e34181b1", size = 18918, upload-time = "2025-12-17T23:32:33.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/87/c443ee508c2708fd2514ccce9052f5e48888783ce690506919629ebc8eb0/time_machine-3.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:051de220fdb6e20d648111bbad423d9506fdbb2e44d4429cef3dc0382abf1fc2", size = 17261, upload-time = "2025-12-17T23:32:34.446Z" }, + { url = "https://files.pythonhosted.org/packages/61/70/b4b980d126ed155c78d1879c50d60c8dcbd47bd11cb14ee7be50e0dfc07f/time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82", size = 19303, upload-time = "2025-12-17T23:32:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/73/73/eaa33603c69a68fe2b6f54f9dd75481693d62f1d29676531002be06e2d1c/time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b", size = 15431, upload-time = "2025-12-17T23:32:37.244Z" }, + { url = "https://files.pythonhosted.org/packages/76/10/b81e138e86cc7bab40cdb59d294b341e172201f4a6c84bb0ec080407977a/time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9", size = 33206, upload-time = "2025-12-17T23:32:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/d3/72/4deab446b579e8bd5dca91de98595c5d6bd6a17ce162abf5c5f2ce40d3d8/time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f", size = 34792, upload-time = "2025-12-17T23:32:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/2c/39/439c6b587ddee76d533fe972289d0646e0a5520e14dc83d0a30aeb5565f7/time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d", size = 36187, upload-time = "2025-12-17T23:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/2da4368db15180989bab83746a857bde05ad16e78f326801c142bb747a06/time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67", size = 34855, upload-time = "2025-12-17T23:32:42.817Z" }, + { url = "https://files.pythonhosted.org/packages/88/84/120a431fee50bc4c241425bee4d3a4910df4923b7ab5f7dff1bf0c772f08/time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f", size = 33222, upload-time = "2025-12-17T23:32:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/89cfda82bb8c57ff91bb9a26751aa234d6d90e9b4d5ab0ad9dce0f9f0329/time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7", size = 34270, upload-time = "2025-12-17T23:32:45.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/aa/235357da4f69a51a8d35fcbfcfa77cdc7dc24f62ae54025006570bda7e2d/time_machine-3.2.0-cp314-cp314-win32.whl", hash = "sha256:14d6778273c543441863dff712cd1d7803dee946b18de35921eb8df10714539d", size = 17544, upload-time = "2025-12-17T23:32:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/7b/51/6c8405a7276be79693b792cff22ce41067ec05db26a7d02f2d5b06324434/time_machine-3.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbf821da96dbc80d349fa9e7c36e670b41d68a878d28c8850057992fed430eef", size = 18423, upload-time = "2025-12-17T23:32:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/d9/03/a3cf419e20c35fc203c6e4fed48b5b667c1a2b4da456d9971e605f73ecef/time_machine-3.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:71c75d71f8e68abc8b669bca26ed2ddd558430a6c171e32b8620288565f18c0e", size = 17050, upload-time = "2025-12-17T23:32:48.91Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/142de946dc4393f910bf4564b5c3ba819906e1f49b06c9cb557519c849e4/time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c", size = 19991, upload-time = "2025-12-17T23:32:49.933Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/7f17def6289901f94726921811a16b9adce46e666362c75d45730c60274f/time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90", size = 15707, upload-time = "2025-12-17T23:32:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d3/3502fb9bd3acb159c18844b26c43220201a0d4a622c0c853785d07699a92/time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c", size = 39207, upload-time = "2025-12-17T23:32:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/8b27f4aa296fda14a5a2ad7f588ddd450603c33415ab3f8e85b2f1a44678/time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6", size = 40764, upload-time = "2025-12-17T23:32:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/fe4c4e5c8ab6d48fab3624c32be9116fb120173a35fe67e482e5cf68b3d2/time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7", size = 43508, upload-time = "2025-12-17T23:32:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/5a3ba2fce85b97655a425d6bb20a441550acd2b304c96b2c19d3839f721a/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f", size = 41712, upload-time = "2025-12-17T23:32:55.781Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/e38084be7fdabb4835db68a3a47e58c34182d79fc35df1ecbe0db2c5359f/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3", size = 38939, upload-time = "2025-12-17T23:32:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/ad3feb0a392ef4e0c08bc32024950373ddc0669002cbdcbb9f3bf0c2d114/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b", size = 39837, upload-time = "2025-12-17T23:32:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/9e/5f4b2ea63b267bd78f3245e76f5528836611b5f2d30b5e7300a722fe4428/time_machine-3.2.0-cp314-cp314t-win32.whl", hash = "sha256:75724762ffd517e7e80aaec1fad1ff5a7414bd84e2b3ee7a0bacfeb67c14926e", size = 18091, upload-time = "2025-12-17T23:32:59.403Z" }, + { url = "https://files.pythonhosted.org/packages/39/6f/456b1f4d2700ae02b19eba830f870596a4b89b74bac3b6c80666f1b108c5/time_machine-3.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2526abbd053c5bca898d1b3e7898eec34626b12206718d8c7ce88fd12c1c9c5c", size = 19208, upload-time = "2025-12-17T23:33:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/2f/22/8063101427ecd3d2652aada4d21d0876b07a3dc789125bca2ee858fec3ed/time_machine-3.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7f2fb6784b414edbe2c0b558bfaab0c251955ba27edd62946cce4a01675a992c", size = 17359, upload-time = "2025-12-17T23:33:01.54Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "extra == 'group-15-vitable-connect-pydantic-v2' or extra != 'group-15-vitable-connect-pydantic-v1'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "vitable-connect" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-15-vitable-connect-pydantic-v1'" }, + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'group-15-vitable-connect-pydantic-v2' or extra != 'group-15-vitable-connect-pydantic-v1'" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +aiohttp = [ + { name = "aiohttp" }, + { name = "httpx-aiohttp" }, +] + +[package.dev-dependencies] +dev = [ + { name = "dirty-equals" }, + { name = "importlib-metadata" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pytest-asyncio", version = "1.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "pytest-xdist" }, + { name = "respx" }, + { name = "rich" }, + { name = "ruff" }, + { name = "time-machine", version = "2.19.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, + { name = "time-machine", version = "3.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10' or (extra == 'group-15-vitable-connect-pydantic-v1' and extra == 'group-15-vitable-connect-pydantic-v2')" }, +] +pydantic-v1 = [ + { name = "pydantic", version = "1.10.26", source = { registry = "https://pypi.org/simple" } }, +] +pydantic-v2 = [ + { name = "pydantic", version = "2.12.5", source = { registry = "https://pypi.org/simple" } }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", marker = "extra == 'aiohttp'" }, + { name = "anyio", specifier = ">=3.5.0,<5" }, + { name = "distro", specifier = ">=1.7.0,<2" }, + { name = "httpx", specifier = ">=0.23.0,<1" }, + { name = "httpx-aiohttp", marker = "extra == 'aiohttp'", specifier = ">=0.1.9" }, + { name = "pydantic", specifier = ">=1.9.0,<3" }, + { name = "sniffio" }, + { name = "typing-extensions", specifier = ">=4.14,<5" }, +] +provides-extras = ["aiohttp"] + +[package.metadata.requires-dev] +dev = [ + { name = "dirty-equals", specifier = ">=0.6.0" }, + { name = "importlib-metadata", specifier = ">=6.7.0" }, + { name = "mypy", specifier = "==1.17" }, + { name = "pyright", specifier = "==1.1.399" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-xdist", specifier = ">=3.6.1" }, + { name = "respx" }, + { name = "rich", specifier = ">=13.7.1" }, + { name = "ruff" }, + { name = "time-machine" }, +] +pydantic-v1 = [{ name = "pydantic", specifier = ">=1.9.0,<2" }] +pydantic-v2 = [ + { name = "pydantic", marker = "python_full_version < '3.14'", specifier = "~=2.0" }, + { name = "pydantic", marker = "python_full_version >= '3.14'", specifier = "~=2.12" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/94/fd/6480106702a79bcceda5fd9c63cb19a04a6506bd5ce7fd8d9b63742f0021/yarl-1.22.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3aa27acb6de7a23785d81557577491f6c38a5209a254d1191519d07d8fe51748", size = 141301, upload-time = "2025-10-06T14:12:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/42/e1/6d95d21b17a93e793e4ec420a925fe1f6a9342338ca7a563ed21129c0990/yarl-1.22.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:af74f05666a5e531289cb1cc9c883d1de2088b8e5b4de48004e5ca8a830ac859", size = 93864, upload-time = "2025-10-06T14:12:21.05Z" }, + { url = "https://files.pythonhosted.org/packages/32/58/b8055273c203968e89808413ea4c984988b6649baabf10f4522e67c22d2f/yarl-1.22.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:62441e55958977b8167b2709c164c91a6363e25da322d87ae6dd9c6019ceecf9", size = 94706, upload-time = "2025-10-06T14:12:23.287Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/d7bfbc28a88c2895ecd0da6a874def0c147de78afc52c773c28e1aa233a3/yarl-1.22.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b580e71cac3f8113d3135888770903eaf2f507e9421e5697d6ee6d8cd1c7f054", size = 347100, upload-time = "2025-10-06T14:12:28.527Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e8/37a1e7b99721c0564b1fc7b0a4d1f595ef6fb8060d82ca61775b644185f7/yarl-1.22.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e81fda2fb4a07eda1a2252b216aa0df23ebcd4d584894e9612e80999a78fd95b", size = 318902, upload-time = "2025-10-06T14:12:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ef/34724449d7ef2db4f22df644f2dac0b8a275d20f585e526937b3ae47b02d/yarl-1.22.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99b6fc1d55782461b78221e95fc357b47ad98b041e8e20f47c1411d0aacddc60", size = 363302, upload-time = "2025-10-06T14:12:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/8a/04/88a39a5dad39889f192cce8d66cc4c58dbeca983e83f9b6bf23822a7ed91/yarl-1.22.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:088e4e08f033db4be2ccd1f34cf29fe994772fb54cfe004bbf54db320af56890", size = 370816, upload-time = "2025-10-06T14:12:34.01Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1f/5e895e547129413f56c76be2c3ce4b96c797d2d0ff3e16a817d9269b12e6/yarl-1.22.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4e1f6f0b4da23e61188676e3ed027ef0baa833a2e633c29ff8530800edccba", size = 346465, upload-time = "2025-10-06T14:12:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/11/13/a750e9fd6f9cc9ed3a52a70fe58ffe505322f0efe0d48e1fd9ffe53281f5/yarl-1.22.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84fc3ec96fce86ce5aa305eb4aa9358279d1aa644b71fab7b8ed33fe3ba1a7ca", size = 341506, upload-time = "2025-10-06T14:12:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/bb6024de76e7186611ebe626aec5b71a2d2ecf9453e795f2dbd80614784c/yarl-1.22.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5dbeefd6ca588b33576a01b0ad58aa934bc1b41ef89dee505bf2932b22ddffba", size = 335030, upload-time = "2025-10-06T14:12:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/50b38447fd94a7992996a62b8b463d0579323fcfc08c61bdba949eef8a5d/yarl-1.22.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14291620375b1060613f4aab9ebf21850058b6b1b438f386cc814813d901c60b", size = 358560, upload-time = "2025-10-06T14:12:41.547Z" }, + { url = "https://files.pythonhosted.org/packages/e2/89/c020b6f547578c4e3dbb6335bf918f26e2f34ad0d1e515d72fd33ac0c635/yarl-1.22.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:a4fcfc8eb2c34148c118dfa02e6427ca278bfd0f3df7c5f99e33d2c0e81eae3e", size = 357290, upload-time = "2025-10-06T14:12:43.861Z" }, + { url = "https://files.pythonhosted.org/packages/8c/52/c49a619ee35a402fa3a7019a4fa8d26878fec0d1243f6968bbf516789578/yarl-1.22.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:029866bde8d7b0878b9c160e72305bbf0a7342bcd20b9999381704ae03308dc8", size = 350700, upload-time = "2025-10-06T14:12:46.868Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f5042d87777bf6968435f04a2bbb15466b2f142e6e47fa4f34d1a3f32f0c/yarl-1.22.0-cp39-cp39-win32.whl", hash = "sha256:4dcc74149ccc8bba31ce1944acee24813e93cfdee2acda3c172df844948ddf7b", size = 82323, upload-time = "2025-10-06T14:12:48.633Z" }, + { url = "https://files.pythonhosted.org/packages/fd/58/d00f7cad9eba20c4eefac2682f34661d1d1b3a942fc0092eb60e78cfb733/yarl-1.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:10619d9fdee46d20edc49d3479e2f8269d0779f1b031e6f7c2aa1c76be04b7ed", size = 87145, upload-time = "2025-10-06T14:12:50.241Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/70904f365080780d38b919edd42d224b8c4ce224a86950d2eaa2a24366ad/yarl-1.22.0-cp39-cp39-win_arm64.whl", hash = "sha256:dd7afd3f8b0bfb4e0d9fc3c31bfe8a4ec7debe124cfd90619305def3c8ca8cd2", size = 82173, upload-time = "2025-10-06T14:12:51.869Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] From 9ca3f50d0702beed7b249f62951397d49924d778 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:41:15 +0000 Subject: [PATCH 2/2] release: 0.9.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 10 ++++++++++ pyproject.toml | 2 +- src/vitable_connect/_version.py | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6d78745..0598874 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.9.0" + ".": "0.9.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3311c5f..344112d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 0.9.1 (2026-09-04) + +Full Changelog: [v0.9.0...v0.9.1](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.9.0...v0.9.1) + +### Chores + +* prep for full Fern automated releases ([8af023f](https://github.com/Vitable-Inc/vitable-connect-python/commit/8af023f8e2b8c7a580d58240ba07a127d6a46296)) +* sync repo ([7b7d8ca](https://github.com/Vitable-Inc/vitable-connect-python/commit/7b7d8ca20c4af60c0748f331440396e5db887474)) +* update User-Agent header to SDK version placeholder ([5af7566](https://github.com/Vitable-Inc/vitable-connect-python/commit/5af7566746b78d25e8311213cc0ef102ffc246a8)) + ## 0.9.0 (2026-08-20) Full Changelog: [v0.8.0...v0.9.0](https://github.com/Vitable-Inc/vitable-connect-python/compare/v0.8.0...v0.9.0) diff --git a/pyproject.toml b/pyproject.toml index d4b93dd..09fd0c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "vitable_connect" -version = "0.9.0" +version = "0.9.1" description = "The official Python library for the vitable-connect API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/vitable_connect/_version.py b/src/vitable_connect/_version.py index f000ac5..bc3417c 100644 --- a/src/vitable_connect/_version.py +++ b/src/vitable_connect/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "vitable_connect" -__version__ = "0.9.0" # x-release-please-version +__version__ = "0.9.1" # x-release-please-version