holder-python 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- holder/__init__.py +21 -0
- holder/client.py +182 -0
- holder/collections.py +70 -0
- holder/events.py +238 -0
- holder/exceptions.py +35 -0
- holder/generated/__init__.py +8 -0
- holder/generated/api/__init__.py +1 -0
- holder/generated/api/default/__init__.py +1 -0
- holder/generated/api/default/delete_ai_messages_message_id.py +166 -0
- holder/generated/api/default/delete_ai_messages_message_id_links.py +205 -0
- holder/generated/api/default/delete_ai_providers_credentials_provider.py +168 -0
- holder/generated/api/default/delete_ai_threads_thread_id.py +166 -0
- holder/generated/api/default/delete_cards_card_id.py +225 -0
- holder/generated/api/default/delete_cards_card_id_links.py +205 -0
- holder/generated/api/default/delete_cards_card_id_milestones_milestone_id.py +175 -0
- holder/generated/api/default/delete_cards_card_id_resources.py +197 -0
- holder/generated/api/default/delete_cards_card_id_tags.py +197 -0
- holder/generated/api/default/delete_locations_location_id.py +105 -0
- holder/generated/api/default/delete_locations_location_id_binding.py +102 -0
- holder/generated/api/default/delete_projects_project_id.py +187 -0
- holder/generated/api/default/delete_resources_resource_id.py +161 -0
- holder/generated/api/default/delete_trash.py +181 -0
- holder/generated/api/default/delete_trash_type_id.py +175 -0
- holder/generated/api/default/get_ai_capabilities.py +166 -0
- holder/generated/api/default/get_ai_catalog_json.py +134 -0
- holder/generated/api/default/get_ai_catalog_yaml.py +132 -0
- holder/generated/api/default/get_ai_local_models_config.py +134 -0
- holder/generated/api/default/get_ai_messages.py +186 -0
- holder/generated/api/default/get_ai_messages_message_id.py +166 -0
- holder/generated/api/default/get_ai_messages_message_id_backlinks.py +194 -0
- holder/generated/api/default/get_ai_messages_message_id_links.py +194 -0
- holder/generated/api/default/get_ai_providers_catalog.py +139 -0
- holder/generated/api/default/get_ai_providers_credentials.py +136 -0
- holder/generated/api/default/get_ai_runner_pull_job_id.py +171 -0
- holder/generated/api/default/get_ai_runner_pull_job_id_events.py +218 -0
- holder/generated/api/default/get_ai_runs.py +186 -0
- holder/generated/api/default/get_ai_runs_run_id.py +166 -0
- holder/generated/api/default/get_ai_runs_run_id_events.py +213 -0
- holder/generated/api/default/get_ai_status.py +134 -0
- holder/generated/api/default/get_ai_threads.py +171 -0
- holder/generated/api/default/get_ai_threads_thread_id.py +166 -0
- holder/generated/api/default/get_calendar.py +221 -0
- holder/generated/api/default/get_cards.py +286 -0
- holder/generated/api/default/get_cards_card_id.py +166 -0
- holder/generated/api/default/get_cards_card_id_backlinks.py +194 -0
- holder/generated/api/default/get_cards_card_id_links.py +194 -0
- holder/generated/api/default/get_cards_card_id_milestones.py +173 -0
- holder/generated/api/default/get_cards_context.py +226 -0
- holder/generated/api/default/get_event_cursor.py +198 -0
- holder/generated/api/default/get_git_providers_json.py +134 -0
- holder/generated/api/default/get_git_providers_yaml.py +132 -0
- holder/generated/api/default/get_health.py +134 -0
- holder/generated/api/default/get_imports_job_id.py +159 -0
- holder/generated/api/default/get_locations.py +160 -0
- holder/generated/api/default/get_locations_location_id.py +157 -0
- holder/generated/api/default/get_openapi_yaml.py +132 -0
- holder/generated/api/default/get_ping.py +128 -0
- holder/generated/api/default/get_projects.py +261 -0
- holder/generated/api/default/get_projects_project_id.py +166 -0
- holder/generated/api/default/get_projects_project_id_cards.py +321 -0
- holder/generated/api/default/get_projects_project_id_encryption_check.py +166 -0
- holder/generated/api/default/get_projects_project_id_git_sync_status.py +171 -0
- holder/generated/api/default/get_projects_project_id_history.py +246 -0
- holder/generated/api/default/get_projects_project_id_history_cards_card_id.py +243 -0
- holder/generated/api/default/get_projects_project_id_history_cards_card_id_compare.py +286 -0
- holder/generated/api/default/get_projects_project_id_history_cards_card_id_snapshot.py +225 -0
- holder/generated/api/default/get_projects_project_id_tags.py +171 -0
- holder/generated/api/default/get_resources.py +242 -0
- holder/generated/api/default/get_resources_resource_id.py +161 -0
- holder/generated/api/default/get_resources_resource_id_assets_asset_id_content.py +247 -0
- holder/generated/api/default/get_search_ai.py +216 -0
- holder/generated/api/default/get_search_cards.py +216 -0
- holder/generated/api/default/get_trash.py +181 -0
- holder/generated/api/default/patch_ai_messages_message_id.py +192 -0
- holder/generated/api/default/patch_ai_threads_thread_id.py +192 -0
- holder/generated/api/default/patch_cards_card_id.py +192 -0
- holder/generated/api/default/patch_cards_card_id_milestones_milestone_id.py +214 -0
- holder/generated/api/default/patch_locations_location_id.py +117 -0
- holder/generated/api/default/patch_projects_project_id.py +192 -0
- holder/generated/api/default/patch_resources_resource_id.py +192 -0
- holder/generated/api/default/post_ai_messages.py +176 -0
- holder/generated/api/default/post_ai_messages_capture.py +181 -0
- holder/generated/api/default/post_ai_messages_message_id_links.py +192 -0
- holder/generated/api/default/post_ai_messages_message_id_restore.py +166 -0
- holder/generated/api/default/post_ai_runner_pull.py +181 -0
- holder/generated/api/default/post_ai_runner_retry.py +139 -0
- holder/generated/api/default/post_ai_runs.py +194 -0
- holder/generated/api/default/post_ai_threads.py +176 -0
- holder/generated/api/default/post_bye.py +166 -0
- holder/generated/api/default/post_card_references_resolve.py +181 -0
- holder/generated/api/default/post_cards.py +176 -0
- holder/generated/api/default/post_cards_card_id_links.py +192 -0
- holder/generated/api/default/post_cards_card_id_milestones.py +199 -0
- holder/generated/api/default/post_cards_card_id_move.py +222 -0
- holder/generated/api/default/post_cards_card_id_resources.py +197 -0
- holder/generated/api/default/post_cards_card_id_restore.py +186 -0
- holder/generated/api/default/post_cards_card_id_tags.py +197 -0
- holder/generated/api/default/post_imports.py +160 -0
- holder/generated/api/default/post_locations.py +160 -0
- holder/generated/api/default/post_locations_location_id_test.py +105 -0
- holder/generated/api/default/post_projects.py +171 -0
- holder/generated/api/default/post_projects_project_id_git_pull.py +211 -0
- holder/generated/api/default/post_projects_project_id_git_push.py +193 -0
- holder/generated/api/default/post_projects_project_id_git_sync.py +207 -0
- holder/generated/api/default/post_projects_project_id_git_test_remote.py +205 -0
- holder/generated/api/default/post_projects_project_id_history_cards_card_id_restore.py +240 -0
- holder/generated/api/default/post_projects_project_id_recovery_token_export.py +196 -0
- holder/generated/api/default/post_projects_project_id_recovery_token_import.py +196 -0
- holder/generated/api/default/post_projects_project_id_repair_hierarchy.py +240 -0
- holder/generated/api/default/post_rebuild.py +176 -0
- holder/generated/api/default/post_recovery_token_import.py +185 -0
- holder/generated/api/default/post_reindex.py +139 -0
- holder/generated/api/default/post_resources.py +176 -0
- holder/generated/api/default/put_ai_local_models_config.py +173 -0
- holder/generated/api/default/put_ai_providers_credentials.py +175 -0
- holder/generated/api/default/put_locations_location_id_binding.py +117 -0
- holder/generated/api/default/put_locations_preferred.py +107 -0
- holder/generated/api/default/stream_changes.py +256 -0
- holder/generated/client.py +280 -0
- holder/generated/errors.py +16 -0
- holder/generated/models/__init__.py +569 -0
- holder/generated/models/ai_local_model_config_data.py +111 -0
- holder/generated/models/ai_local_model_config_response.py +78 -0
- holder/generated/models/ai_local_model_config_update_request.py +134 -0
- holder/generated/models/ai_message.py +204 -0
- holder/generated/models/ai_message_capture_request.py +246 -0
- holder/generated/models/ai_message_capture_request_context_type_0.py +46 -0
- holder/generated/models/ai_message_capture_response.py +78 -0
- holder/generated/models/ai_message_capture_response_data.py +78 -0
- holder/generated/models/ai_message_create_request.py +186 -0
- holder/generated/models/ai_message_create_response.py +78 -0
- holder/generated/models/ai_message_create_response_data.py +62 -0
- holder/generated/models/ai_message_get_response.py +76 -0
- holder/generated/models/ai_message_list_response.py +84 -0
- holder/generated/models/ai_message_update_request.py +203 -0
- holder/generated/models/ai_message_update_response.py +78 -0
- holder/generated/models/ai_message_update_response_data.py +62 -0
- holder/generated/models/ai_provider_catalog_provider.py +265 -0
- holder/generated/models/ai_provider_catalog_provider_api_type_0.py +46 -0
- holder/generated/models/ai_provider_catalog_provider_auth_type_0.py +46 -0
- holder/generated/models/ai_provider_catalog_provider_models_item.py +46 -0
- holder/generated/models/ai_provider_catalog_response.py +78 -0
- holder/generated/models/ai_provider_catalog_response_data.py +100 -0
- holder/generated/models/ai_provider_credential.py +94 -0
- holder/generated/models/ai_provider_credential_delete_response.py +80 -0
- holder/generated/models/ai_provider_credential_delete_response_data.py +62 -0
- holder/generated/models/ai_provider_credential_upsert_request.py +92 -0
- holder/generated/models/ai_provider_credential_upsert_response.py +78 -0
- holder/generated/models/ai_provider_credentials_list_response.py +80 -0
- holder/generated/models/ai_provider_credentials_list_response_data.py +78 -0
- holder/generated/models/ai_run_create_request.py +133 -0
- holder/generated/models/ai_run_create_request_context.py +46 -0
- holder/generated/models/ai_run_event_data.py +146 -0
- holder/generated/models/ai_run_get_response.py +76 -0
- holder/generated/models/ai_run_item.py +328 -0
- holder/generated/models/ai_run_item_policy_trace_type_0.py +46 -0
- holder/generated/models/ai_run_list_response.py +84 -0
- holder/generated/models/ai_status_data.py +186 -0
- holder/generated/models/ai_status_pull_job.py +130 -0
- holder/generated/models/ai_status_response.py +76 -0
- holder/generated/models/ai_thread.py +116 -0
- holder/generated/models/ai_thread_create_request.py +119 -0
- holder/generated/models/ai_thread_create_response.py +78 -0
- holder/generated/models/ai_thread_create_response_data.py +62 -0
- holder/generated/models/ai_thread_get_response.py +76 -0
- holder/generated/models/ai_thread_list_response.py +84 -0
- holder/generated/models/ai_thread_patch_response.py +78 -0
- holder/generated/models/ai_thread_patch_response_data.py +62 -0
- holder/generated/models/ai_thread_update_request.py +104 -0
- holder/generated/models/asset_import_job.py +141 -0
- holder/generated/models/asset_import_job_response.py +76 -0
- holder/generated/models/asset_import_job_status.py +13 -0
- holder/generated/models/asset_import_request.py +86 -0
- holder/generated/models/asset_placement.py +96 -0
- holder/generated/models/asset_placement_encoding.py +9 -0
- holder/generated/models/bye_response.py +62 -0
- holder/generated/models/calendar_card_activity.py +86 -0
- holder/generated/models/card_context_breadcrumb.py +113 -0
- holder/generated/models/card_context_breadcrumb_type.py +9 -0
- holder/generated/models/card_context_card.py +161 -0
- holder/generated/models/card_context_data.py +124 -0
- holder/generated/models/card_context_data_project.py +70 -0
- holder/generated/models/card_context_response.py +76 -0
- holder/generated/models/card_create_request.py +147 -0
- holder/generated/models/card_create_response.py +78 -0
- holder/generated/models/card_create_response_data.py +70 -0
- holder/generated/models/card_data.py +160 -0
- holder/generated/models/card_get_response.py +76 -0
- holder/generated/models/card_history_author.py +70 -0
- holder/generated/models/card_history_comparison_data.py +110 -0
- holder/generated/models/card_history_comparison_response.py +78 -0
- holder/generated/models/card_history_diff_line.py +100 -0
- holder/generated/models/card_history_diff_line_origin.py +10 -0
- holder/generated/models/card_history_entry.py +172 -0
- holder/generated/models/card_history_entry_kind.py +16 -0
- holder/generated/models/card_history_entry_saves_item.py +94 -0
- holder/generated/models/card_history_page_data.py +114 -0
- holder/generated/models/card_history_page_response.py +76 -0
- holder/generated/models/card_history_restore_response.py +80 -0
- holder/generated/models/card_history_restore_response_data.py +108 -0
- holder/generated/models/card_history_snapshot_response.py +80 -0
- holder/generated/models/card_history_snapshot_response_data.py +76 -0
- holder/generated/models/card_history_version.py +86 -0
- holder/generated/models/card_link.py +116 -0
- holder/generated/models/card_link_create_request.py +111 -0
- holder/generated/models/card_link_create_response.py +76 -0
- holder/generated/models/card_link_delete_request.py +80 -0
- holder/generated/models/card_link_list_response.py +84 -0
- holder/generated/models/card_list_item.py +161 -0
- holder/generated/models/card_list_response.py +84 -0
- holder/generated/models/card_move_intent.py +15 -0
- holder/generated/models/card_move_request.py +134 -0
- holder/generated/models/card_move_response.py +76 -0
- holder/generated/models/card_move_result.py +114 -0
- holder/generated/models/card_page.py +90 -0
- holder/generated/models/card_page_item.py +170 -0
- holder/generated/models/card_page_response.py +76 -0
- holder/generated/models/card_patch_response.py +78 -0
- holder/generated/models/card_patch_response_data.py +62 -0
- holder/generated/models/card_reference_candidate.py +84 -0
- holder/generated/models/card_reference_resolve_data.py +129 -0
- holder/generated/models/card_reference_resolve_data_match_kind.py +10 -0
- holder/generated/models/card_reference_resolve_data_status.py +10 -0
- holder/generated/models/card_reference_resolve_request.py +82 -0
- holder/generated/models/card_reference_resolve_request_scope.py +10 -0
- holder/generated/models/card_reference_resolve_response.py +78 -0
- holder/generated/models/card_tag_mutation_request.py +70 -0
- holder/generated/models/card_tag_mutation_response.py +78 -0
- holder/generated/models/card_tag_mutation_result.py +88 -0
- holder/generated/models/card_tag_mutation_result_outcome.py +12 -0
- holder/generated/models/card_update_request.py +122 -0
- holder/generated/models/caste_info.py +85 -0
- holder/generated/models/caste_info_name.py +12 -0
- holder/generated/models/caste_recommended_model.py +143 -0
- holder/generated/models/caste_recommended_model_required_caste.py +12 -0
- holder/generated/models/change_event_data.py +109 -0
- holder/generated/models/change_event_data_entity.py +14 -0
- holder/generated/models/delete_cards_card_id_children.py +9 -0
- holder/generated/models/delete_cards_card_id_resources_body.py +70 -0
- holder/generated/models/error.py +70 -0
- holder/generated/models/error_response.py +76 -0
- holder/generated/models/event_checkpoint.py +92 -0
- holder/generated/models/event_checkpoint_git_revisions.py +66 -0
- holder/generated/models/event_checkpoint_history_urls.py +48 -0
- holder/generated/models/event_checkpoint_response.py +76 -0
- holder/generated/models/event_resync_data.py +116 -0
- holder/generated/models/event_resync_data_reason.py +9 -0
- holder/generated/models/get_ai_catalog_json_response_200.py +46 -0
- holder/generated/models/get_ai_messages_message_id_backlinks_include_deleted.py +9 -0
- holder/generated/models/get_ai_messages_message_id_links_include_deleted.py +9 -0
- holder/generated/models/get_cards_card_id_backlinks_include_deleted.py +9 -0
- holder/generated/models/get_cards_card_id_links_include_deleted.py +9 -0
- holder/generated/models/get_cards_context_order.py +10 -0
- holder/generated/models/get_cards_order.py +10 -0
- holder/generated/models/get_cards_view.py +9 -0
- holder/generated/models/get_git_providers_json_response_200.py +46 -0
- holder/generated/models/get_locations_location_id_response_200.py +46 -0
- holder/generated/models/get_ping_response_200.py +8 -0
- holder/generated/models/get_projects_order.py +13 -0
- holder/generated/models/get_projects_project_id_cards_order.py +9 -0
- holder/generated/models/get_projects_project_id_cards_parent_type_0.py +8 -0
- holder/generated/models/get_projects_project_id_history_cards_card_id_compare_mode.py +9 -0
- holder/generated/models/get_projects_project_id_history_kind.py +13 -0
- holder/generated/models/health_data.py +110 -0
- holder/generated/models/health_data_crypt_filter.py +78 -0
- holder/generated/models/health_response.py +76 -0
- holder/generated/models/hierarchy_repair.py +100 -0
- holder/generated/models/hierarchy_repair_move.py +76 -0
- holder/generated/models/hierarchy_repair_request.py +62 -0
- holder/generated/models/hierarchy_repair_response.py +76 -0
- holder/generated/models/local_model.py +91 -0
- holder/generated/models/location_binding_request.py +81 -0
- holder/generated/models/location_binding_request_values.py +46 -0
- holder/generated/models/milestone.py +177 -0
- holder/generated/models/milestone_create_request.py +135 -0
- holder/generated/models/milestone_list_response.py +84 -0
- holder/generated/models/milestone_remove_response.py +78 -0
- holder/generated/models/milestone_remove_result.py +79 -0
- holder/generated/models/milestone_response.py +76 -0
- holder/generated/models/milestone_update_request.py +116 -0
- holder/generated/models/patch_locations_location_id_body.py +90 -0
- holder/generated/models/patch_locations_location_id_body_configuration.py +46 -0
- holder/generated/models/post_ai_runner_pull_body.py +62 -0
- holder/generated/models/post_cards_card_id_resources_body.py +70 -0
- holder/generated/models/post_locations_response_201.py +46 -0
- holder/generated/models/project.py +197 -0
- holder/generated/models/project_calendar_response.py +78 -0
- holder/generated/models/project_calendar_response_data.py +136 -0
- holder/generated/models/project_create_request.py +177 -0
- holder/generated/models/project_create_request_privacy_mode.py +9 -0
- holder/generated/models/project_create_response.py +78 -0
- holder/generated/models/project_create_response_data.py +62 -0
- holder/generated/models/project_delete_response.py +78 -0
- holder/generated/models/project_delete_response_data.py +62 -0
- holder/generated/models/project_encryption_check.py +94 -0
- holder/generated/models/project_encryption_check_response.py +80 -0
- holder/generated/models/project_encryption_check_response_data.py +92 -0
- holder/generated/models/project_encryption_check_response_data_privacy_mode.py +9 -0
- holder/generated/models/project_get_response.py +76 -0
- holder/generated/models/project_git_push_request.py +95 -0
- holder/generated/models/project_git_push_response.py +78 -0
- holder/generated/models/project_git_push_response_data.py +199 -0
- holder/generated/models/project_git_push_response_data_status.py +15 -0
- holder/generated/models/project_git_sync_operation_response.py +80 -0
- holder/generated/models/project_git_sync_operation_response_data.py +163 -0
- holder/generated/models/project_git_sync_operation_response_data_pull.py +96 -0
- holder/generated/models/project_git_sync_operation_response_data_pull_status.py +11 -0
- holder/generated/models/project_git_sync_operation_response_data_push.py +118 -0
- holder/generated/models/project_git_sync_operation_response_data_push_status.py +15 -0
- holder/generated/models/project_git_sync_operation_response_data_status.py +9 -0
- holder/generated/models/project_git_sync_request.py +94 -0
- holder/generated/models/project_git_sync_status_response.py +80 -0
- holder/generated/models/project_git_sync_status_response_data.py +76 -0
- holder/generated/models/project_git_test_remote_request.py +96 -0
- holder/generated/models/project_git_test_remote_response.py +80 -0
- holder/generated/models/project_git_test_remote_response_data.py +151 -0
- holder/generated/models/project_git_test_remote_response_data_status.py +14 -0
- holder/generated/models/project_history_activity.py +138 -0
- holder/generated/models/project_history_affected_object.py +74 -0
- holder/generated/models/project_history_affected_object_kind.py +13 -0
- holder/generated/models/project_history_page_data.py +115 -0
- holder/generated/models/project_history_page_response.py +78 -0
- holder/generated/models/project_list_response.py +84 -0
- holder/generated/models/project_privacy_mode.py +9 -0
- holder/generated/models/project_recovery_token_export_request.py +62 -0
- holder/generated/models/project_recovery_token_export_response.py +80 -0
- holder/generated/models/project_recovery_token_export_response_data.py +78 -0
- holder/generated/models/project_recovery_token_import_request.py +70 -0
- holder/generated/models/project_recovery_token_import_response.py +80 -0
- holder/generated/models/project_recovery_token_import_response_data.py +62 -0
- holder/generated/models/project_sync.py +227 -0
- holder/generated/models/project_tag.py +70 -0
- holder/generated/models/project_tag_list_response.py +84 -0
- holder/generated/models/project_update_request.py +181 -0
- holder/generated/models/project_update_request_privacy_mode.py +9 -0
- holder/generated/models/project_update_response.py +78 -0
- holder/generated/models/project_update_response_data.py +75 -0
- holder/generated/models/put_locations_preferred_body.py +70 -0
- holder/generated/models/rebuild_data.py +94 -0
- holder/generated/models/rebuild_request.py +62 -0
- holder/generated/models/rebuild_response.py +76 -0
- holder/generated/models/recovery_token_import_auto_response.py +80 -0
- holder/generated/models/recovery_token_import_auto_response_data.py +141 -0
- holder/generated/models/recovery_token_import_auto_response_data_pull_status.py +10 -0
- holder/generated/models/reindex_data.py +62 -0
- holder/generated/models/reindex_response.py +76 -0
- holder/generated/models/resource.py +134 -0
- holder/generated/models/resource_asset.py +140 -0
- holder/generated/models/resource_attachment_response.py +80 -0
- holder/generated/models/resource_attachment_response_data.py +90 -0
- holder/generated/models/resource_attachment_response_data_outcome.py +11 -0
- holder/generated/models/resource_create_request.py +129 -0
- holder/generated/models/resource_delete_response.py +78 -0
- holder/generated/models/resource_delete_response_data.py +62 -0
- holder/generated/models/resource_list_response.py +134 -0
- holder/generated/models/resource_metadata.py +55 -0
- holder/generated/models/resource_response.py +76 -0
- holder/generated/models/resource_update_request.py +102 -0
- holder/generated/models/runner_capabilities_data.py +220 -0
- holder/generated/models/runner_capabilities_response.py +78 -0
- holder/generated/models/runner_pull_job.py +122 -0
- holder/generated/models/runner_pull_progress.py +86 -0
- holder/generated/models/runner_pull_start_response.py +78 -0
- holder/generated/models/runner_pull_start_response_data.py +78 -0
- holder/generated/models/runner_pull_status_response.py +76 -0
- holder/generated/models/search_card_item.py +102 -0
- holder/generated/models/search_cards_response.py +84 -0
- holder/generated/models/search_message_item.py +86 -0
- holder/generated/models/search_messages_response.py +84 -0
- holder/generated/models/storage_location.py +149 -0
- holder/generated/models/storage_location_configuration.py +48 -0
- holder/generated/models/storage_location_list_response.py +100 -0
- holder/generated/models/storage_location_provider.py +9 -0
- holder/generated/models/storage_location_request.py +118 -0
- holder/generated/models/storage_location_request_configuration.py +46 -0
- holder/generated/models/storage_location_request_provider.py +9 -0
- holder/generated/models/trash_delete_response.py +78 -0
- holder/generated/models/trash_delete_response_data.py +46 -0
- holder/generated/models/trash_item.py +211 -0
- holder/generated/models/trash_list_response.py +84 -0
- holder/generated/types.py +54 -0
- holder/library.py +15 -0
- holder/models/__init__.py +15 -0
- holder/models/card.py +272 -0
- holder/models/project.py +207 -0
- holder/models/search.py +66 -0
- holder/models/tag.py +117 -0
- holder/py.typed +0 -0
- holder_python-0.2.5.dist-info/METADATA +120 -0
- holder_python-0.2.5.dist-info/RECORD +391 -0
- holder_python-0.2.5.dist-info/WHEEL +4 -0
holder/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Holder Python Library - control your knowledge using Python."""
|
|
2
|
+
|
|
3
|
+
from .client import Holder
|
|
4
|
+
from .models import (
|
|
5
|
+
Card,
|
|
6
|
+
CardSearchResult,
|
|
7
|
+
HierarchyRepair,
|
|
8
|
+
Project,
|
|
9
|
+
RepairedCard,
|
|
10
|
+
Tag,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Card",
|
|
15
|
+
"CardSearchResult",
|
|
16
|
+
"HierarchyRepair",
|
|
17
|
+
"Holder",
|
|
18
|
+
"Project",
|
|
19
|
+
"RepairedCard",
|
|
20
|
+
"Tag",
|
|
21
|
+
]
|
holder/client.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Connection and request handling for the synchronous Holder client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from typing import Any, Self
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from .exceptions import (
|
|
13
|
+
APIError,
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
ClosedError,
|
|
16
|
+
ConnectionError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
ProtocolError,
|
|
19
|
+
)
|
|
20
|
+
from .generated import AuthenticatedClient
|
|
21
|
+
from .generated.models import ErrorResponse
|
|
22
|
+
from .models.project import Project, ProjectCollection
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Holder:
|
|
26
|
+
"""Control your local knowledge, or connect with an explicit URL/token pair.
|
|
27
|
+
|
|
28
|
+
Use as a context manager. Collection queries run only when consumed.
|
|
29
|
+
Local discovery requires holderctl on PATH (or the holderctl argument).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*,
|
|
35
|
+
url: str | None = None,
|
|
36
|
+
token: str | None = None,
|
|
37
|
+
holderctl: str = "holderctl",
|
|
38
|
+
timeout: float | httpx.Timeout = 10.0,
|
|
39
|
+
transport: httpx.BaseTransport | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
if (url is None) != (token is None):
|
|
42
|
+
raise ValueError("Supply url and token together, or omit both")
|
|
43
|
+
self._url = url
|
|
44
|
+
self._token = token
|
|
45
|
+
self._local = url is None
|
|
46
|
+
self._holderctl = holderctl
|
|
47
|
+
self._timeout = timeout
|
|
48
|
+
self._transport = transport
|
|
49
|
+
self._client: AuthenticatedClient | None = None
|
|
50
|
+
self._closed = False
|
|
51
|
+
self.projects = ProjectCollection(self)
|
|
52
|
+
|
|
53
|
+
def _command(self, *args: str) -> str:
|
|
54
|
+
try:
|
|
55
|
+
result = subprocess.run(
|
|
56
|
+
[self._holderctl, *args],
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
timeout=65,
|
|
60
|
+
check=False,
|
|
61
|
+
)
|
|
62
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
63
|
+
raise ConnectionError(
|
|
64
|
+
"Could not run holderctl; install Holder or supply url and token"
|
|
65
|
+
) from None
|
|
66
|
+
if result.returncode:
|
|
67
|
+
# CLI output and stderr may contain local credentials; never echo them.
|
|
68
|
+
raise ConnectionError(
|
|
69
|
+
f"holderctl {args[0]} failed (exit {result.returncode})"
|
|
70
|
+
)
|
|
71
|
+
return result.stdout
|
|
72
|
+
|
|
73
|
+
def _discover(self) -> None:
|
|
74
|
+
try:
|
|
75
|
+
result = json.loads(
|
|
76
|
+
self._command(
|
|
77
|
+
"ensure",
|
|
78
|
+
"--json",
|
|
79
|
+
"--api-min",
|
|
80
|
+
"0.1",
|
|
81
|
+
"--api-max-exclusive",
|
|
82
|
+
"1.0",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
url = result["daemon"]["url"]
|
|
86
|
+
if result.get("ok") is not True or not isinstance(url, str) or not url:
|
|
87
|
+
raise ValueError
|
|
88
|
+
token = self._command("token").strip()
|
|
89
|
+
if not token:
|
|
90
|
+
raise ValueError
|
|
91
|
+
except (ValueError, KeyError, TypeError):
|
|
92
|
+
raise ConnectionError(
|
|
93
|
+
"holderctl did not return valid daemon connection details"
|
|
94
|
+
) from None
|
|
95
|
+
self._url, self._token = url, token
|
|
96
|
+
|
|
97
|
+
def _connect(self) -> None:
|
|
98
|
+
if self._local:
|
|
99
|
+
self._discover()
|
|
100
|
+
self._client = AuthenticatedClient(
|
|
101
|
+
base_url=self._url,
|
|
102
|
+
token=self._token,
|
|
103
|
+
timeout=httpx.Timeout(self._timeout),
|
|
104
|
+
httpx_args={"transport": self._transport} if self._transport else {},
|
|
105
|
+
)
|
|
106
|
+
self._client.__enter__()
|
|
107
|
+
|
|
108
|
+
def __enter__(self) -> Self:
|
|
109
|
+
if self._closed or self._client is not None:
|
|
110
|
+
raise ClosedError("Holder contexts cannot be reused")
|
|
111
|
+
self._connect()
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def __exit__(self, *args: object) -> None:
|
|
115
|
+
self._closed = True
|
|
116
|
+
if self._client is not None:
|
|
117
|
+
self._client.__exit__(*args)
|
|
118
|
+
|
|
119
|
+
def _active(self) -> AuthenticatedClient:
|
|
120
|
+
if self._closed or self._client is None:
|
|
121
|
+
raise ClosedError("Use Holder inside with Holder(...) as h")
|
|
122
|
+
return self._client
|
|
123
|
+
|
|
124
|
+
def __iter__(self) -> Iterator[Project]:
|
|
125
|
+
return iter(self.projects)
|
|
126
|
+
|
|
127
|
+
def _call(
|
|
128
|
+
self, endpoint: Any, *args: Any, write: bool = False, **kwargs: Any
|
|
129
|
+
) -> Any:
|
|
130
|
+
client = self._active()
|
|
131
|
+
for attempt in range(2):
|
|
132
|
+
try:
|
|
133
|
+
response = endpoint.sync_detailed(*args, client=client, **kwargs)
|
|
134
|
+
except httpx.TransportError:
|
|
135
|
+
raise ConnectionError(
|
|
136
|
+
"Could not communicate with the Holder daemon"
|
|
137
|
+
) from None
|
|
138
|
+
except (ValueError, KeyError, TypeError):
|
|
139
|
+
raise ProtocolError("Invalid daemon response") from None
|
|
140
|
+
if (
|
|
141
|
+
response.status_code == 401
|
|
142
|
+
and self._local
|
|
143
|
+
and not write
|
|
144
|
+
and attempt == 0
|
|
145
|
+
):
|
|
146
|
+
self._discover()
|
|
147
|
+
# Preserve the pool and custom transport while updating a restarted daemon.
|
|
148
|
+
http_client = client.get_httpx_client()
|
|
149
|
+
http_client.base_url = self._url
|
|
150
|
+
http_client.headers["Authorization"] = f"Bearer {self._token}"
|
|
151
|
+
continue
|
|
152
|
+
if isinstance(response.parsed, ErrorResponse):
|
|
153
|
+
error = response.parsed.error
|
|
154
|
+
error_type = {401: AuthenticationError, 404: NotFoundError}.get(
|
|
155
|
+
response.status_code, APIError
|
|
156
|
+
)
|
|
157
|
+
raise error_type(int(response.status_code), error.code, error.message)
|
|
158
|
+
if response.status_code >= 400:
|
|
159
|
+
error_type = {401: AuthenticationError, 404: NotFoundError}.get(
|
|
160
|
+
response.status_code, APIError
|
|
161
|
+
)
|
|
162
|
+
# Undocumented statuses are not parsed by generated endpoints.
|
|
163
|
+
try:
|
|
164
|
+
error = ErrorResponse.from_dict(json.loads(response.content)).error
|
|
165
|
+
except (ValueError, KeyError, TypeError, AttributeError):
|
|
166
|
+
error = None
|
|
167
|
+
if error is not None:
|
|
168
|
+
raise error_type(
|
|
169
|
+
int(response.status_code), error.code, error.message
|
|
170
|
+
)
|
|
171
|
+
raise error_type(
|
|
172
|
+
int(response.status_code), "http_error", "Daemon request failed"
|
|
173
|
+
)
|
|
174
|
+
if (
|
|
175
|
+
response.parsed is None
|
|
176
|
+
or getattr(response.parsed, "ok", None) is not True
|
|
177
|
+
):
|
|
178
|
+
raise ProtocolError("Invalid daemon success envelope")
|
|
179
|
+
return response.parsed.data
|
|
180
|
+
raise AuthenticationError(
|
|
181
|
+
401, "unauthorized", "Daemon rejected refreshed credentials"
|
|
182
|
+
)
|
holder/collections.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Shared lazy iteration and caching for Holder collections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, Self, TypeVar
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .client import Holder
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Collection(Generic[T]):
|
|
15
|
+
"""Lazy query with a progressively populated cache and independent iterators."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, holder: Holder, query: dict[str, Any] | None = None) -> None:
|
|
18
|
+
self._holder = holder
|
|
19
|
+
self._query = dict(query or {})
|
|
20
|
+
self._cache: list[T] = []
|
|
21
|
+
self._next: int | str | None = None
|
|
22
|
+
self._done = False
|
|
23
|
+
self._generation = 0
|
|
24
|
+
|
|
25
|
+
def __iter__(self) -> Iterator[T]:
|
|
26
|
+
self._holder._active()
|
|
27
|
+
generation = self._generation
|
|
28
|
+
index = 0
|
|
29
|
+
while True:
|
|
30
|
+
self._holder._active()
|
|
31
|
+
if generation != self._generation:
|
|
32
|
+
raise RuntimeError("Collection was refreshed during iteration")
|
|
33
|
+
if index == len(self._cache):
|
|
34
|
+
if self._done:
|
|
35
|
+
return
|
|
36
|
+
rows, next_cursor = self._fetch_page()
|
|
37
|
+
self._cache.extend(rows)
|
|
38
|
+
self._next = next_cursor
|
|
39
|
+
self._done = next_cursor is None
|
|
40
|
+
if not rows:
|
|
41
|
+
return
|
|
42
|
+
yield self._cache[index]
|
|
43
|
+
index += 1
|
|
44
|
+
|
|
45
|
+
def __getitem__(self, index: int) -> T:
|
|
46
|
+
if not isinstance(index, int):
|
|
47
|
+
raise TypeError("Collection indices must be integers")
|
|
48
|
+
if index < 0:
|
|
49
|
+
raise IndexError("Collection indices must be nonnegative")
|
|
50
|
+
for position, item in enumerate(self):
|
|
51
|
+
if position == index:
|
|
52
|
+
return item
|
|
53
|
+
raise IndexError("Collection index out of range")
|
|
54
|
+
|
|
55
|
+
def all(self) -> Self:
|
|
56
|
+
return self._clone(self._query)
|
|
57
|
+
|
|
58
|
+
def refresh(self) -> Self:
|
|
59
|
+
self._holder._active()
|
|
60
|
+
self._cache.clear()
|
|
61
|
+
self._next = None
|
|
62
|
+
self._done = False
|
|
63
|
+
self._generation += 1
|
|
64
|
+
return self
|
|
65
|
+
|
|
66
|
+
def _clone(self, query: dict[str, Any]) -> Self:
|
|
67
|
+
raise NotImplementedError
|
|
68
|
+
|
|
69
|
+
def _fetch_page(self) -> tuple[list[T], int | str | None]:
|
|
70
|
+
raise NotImplementedError
|
holder/events.py
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""Incremental asyncio transport for GET /events; no entity or recovery policy."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import AsyncIterator
|
|
7
|
+
from contextlib import asynccontextmanager
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Self
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from .generated import AuthenticatedClient, Client
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Event:
|
|
18
|
+
"""A complete SSE frame. IDs are opaque; data is retained as UTF-8 text."""
|
|
19
|
+
|
|
20
|
+
event: str
|
|
21
|
+
data: str
|
|
22
|
+
id: str | None = None
|
|
23
|
+
|
|
24
|
+
def json(self) -> Any:
|
|
25
|
+
return json.loads(self.data)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _lines(response: httpx.Response) -> AsyncIterator[str]:
|
|
29
|
+
# SSE recognizes only CR and LF, not Unicode separators inside JSON strings.
|
|
30
|
+
response.encoding = "utf-8"
|
|
31
|
+
line: list[str] = []
|
|
32
|
+
after_cr = False
|
|
33
|
+
async for chunk in response.aiter_text():
|
|
34
|
+
for character in chunk:
|
|
35
|
+
if after_cr and character == "\n":
|
|
36
|
+
after_cr = False
|
|
37
|
+
continue
|
|
38
|
+
after_cr = character == "\r"
|
|
39
|
+
if character in "\r\n":
|
|
40
|
+
yield "".join(line)
|
|
41
|
+
line.clear()
|
|
42
|
+
else:
|
|
43
|
+
line.append(character)
|
|
44
|
+
# No synthetic newline at EOF: partial frames must replay after reconnect.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _frames(response: httpx.Response) -> AsyncIterator[Event]:
|
|
48
|
+
data: list[str] = []
|
|
49
|
+
name = "message"
|
|
50
|
+
event_id = None
|
|
51
|
+
first = True
|
|
52
|
+
async for line in _lines(response):
|
|
53
|
+
if first:
|
|
54
|
+
line = line.removeprefix("\ufeff")
|
|
55
|
+
first = False
|
|
56
|
+
if not line:
|
|
57
|
+
if data:
|
|
58
|
+
yield Event(name, "\n".join(data), event_id)
|
|
59
|
+
data, name, event_id = [], "message", None
|
|
60
|
+
continue
|
|
61
|
+
if line.startswith(":"):
|
|
62
|
+
continue
|
|
63
|
+
field, _, value = line.partition(":")
|
|
64
|
+
value = value.removeprefix(" ")
|
|
65
|
+
if field == "data":
|
|
66
|
+
data.append(value)
|
|
67
|
+
elif field == "event":
|
|
68
|
+
name = value or "message"
|
|
69
|
+
elif field == "id" and "\x00" not in value:
|
|
70
|
+
event_id = value
|
|
71
|
+
# An unterminated frame is not delivered or acknowledged on disconnect.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _ClosingStream(httpx.AsyncByteStream):
|
|
75
|
+
"""Protect cleanup both at HTTPX's automatic EOF close and context exit."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, stream: httpx.AsyncByteStream) -> None:
|
|
78
|
+
self._stream = stream
|
|
79
|
+
self._cleanup: asyncio.Task[None] | None = None
|
|
80
|
+
|
|
81
|
+
async def __aiter__(self) -> AsyncIterator[bytes]:
|
|
82
|
+
async for chunk in self._stream:
|
|
83
|
+
yield chunk
|
|
84
|
+
|
|
85
|
+
async def aclose(self) -> None:
|
|
86
|
+
if self._cleanup is None:
|
|
87
|
+
self._cleanup = asyncio.create_task(self._stream.aclose())
|
|
88
|
+
cleanup = self._cleanup
|
|
89
|
+
# A second task cancellation must not interrupt socket/pool cleanup.
|
|
90
|
+
cancelled = False
|
|
91
|
+
while not cleanup.done():
|
|
92
|
+
try:
|
|
93
|
+
await asyncio.shield(cleanup)
|
|
94
|
+
except asyncio.CancelledError:
|
|
95
|
+
cancelled = True
|
|
96
|
+
cleanup.result()
|
|
97
|
+
if cancelled:
|
|
98
|
+
raise asyncio.CancelledError
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@asynccontextmanager
|
|
102
|
+
async def _connection(
|
|
103
|
+
transport: httpx.AsyncClient, **kwargs: Any
|
|
104
|
+
) -> AsyncIterator[httpx.Response]:
|
|
105
|
+
request = transport.build_request("GET", "/events", **kwargs)
|
|
106
|
+
response = await transport.send(request, stream=True)
|
|
107
|
+
response.stream = _ClosingStream(response.stream)
|
|
108
|
+
try:
|
|
109
|
+
yield response
|
|
110
|
+
finally:
|
|
111
|
+
await response.aclose()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ChangeStream:
|
|
115
|
+
"""Context-managed asyncio iterator over daemon invalidations.
|
|
116
|
+
|
|
117
|
+
Pass the same project filter used for /events/cursor. cursor advances when
|
|
118
|
+
the consumer requests the next event (after processing the previous one).
|
|
119
|
+
Persist cursor for a later subscription. A ready frame uses its ID, never
|
|
120
|
+
the newer checkpoint in its data. resync_required is yielded once and stops
|
|
121
|
+
iteration without advancing cursor: refresh state before subscribing again.
|
|
122
|
+
|
|
123
|
+
EOF and HTTPX transport errors reconnect after reconnect_delay seconds.
|
|
124
|
+
HTTP errors and invalid content types propagate without retry. Read timeout
|
|
125
|
+
defaults to disabled; connection/write/pool timeouts remain bounded.
|
|
126
|
+
Cancellation, aclose(), and context exit close the active response. The
|
|
127
|
+
supplied generated client remains caller-owned. Use asyncio task cancellation
|
|
128
|
+
or aclose() to interrupt a pending read; only one reader is supported.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
client: Client | AuthenticatedClient,
|
|
134
|
+
*,
|
|
135
|
+
project_id: str | None = None,
|
|
136
|
+
last_revision: str | None = None,
|
|
137
|
+
last_event_id: str | None = None,
|
|
138
|
+
reconnect_delay: float = 1.0,
|
|
139
|
+
timeout: httpx.Timeout | None = None,
|
|
140
|
+
) -> None:
|
|
141
|
+
if not math.isfinite(reconnect_delay) or reconnect_delay < 0:
|
|
142
|
+
raise ValueError("reconnect_delay must be finite and nonnegative")
|
|
143
|
+
self.cursor = last_event_id
|
|
144
|
+
self._client = client
|
|
145
|
+
self._params = {
|
|
146
|
+
key: value
|
|
147
|
+
for key, value in {
|
|
148
|
+
"project_id": project_id,
|
|
149
|
+
"last_revision": last_revision,
|
|
150
|
+
}.items()
|
|
151
|
+
if value is not None
|
|
152
|
+
}
|
|
153
|
+
self._delay = reconnect_delay
|
|
154
|
+
self._timeout = timeout if timeout is not None else httpx.Timeout(10, read=None)
|
|
155
|
+
self._iterator = self._events()
|
|
156
|
+
self._pending: asyncio.Task[Event] | None = None
|
|
157
|
+
self._entered = False
|
|
158
|
+
self._closed = False
|
|
159
|
+
|
|
160
|
+
async def __aenter__(self) -> Self:
|
|
161
|
+
if self._entered or self._closed:
|
|
162
|
+
raise RuntimeError("ChangeStream contexts cannot be reused")
|
|
163
|
+
self._entered = True
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
async def __aexit__(self, *args: object) -> None:
|
|
167
|
+
await self.aclose()
|
|
168
|
+
|
|
169
|
+
def __aiter__(self) -> Self:
|
|
170
|
+
return self
|
|
171
|
+
|
|
172
|
+
async def __anext__(self) -> Event:
|
|
173
|
+
if not self._entered:
|
|
174
|
+
raise RuntimeError("Use ChangeStream inside async with")
|
|
175
|
+
if self._closed:
|
|
176
|
+
raise StopAsyncIteration
|
|
177
|
+
if self._pending is not None:
|
|
178
|
+
raise RuntimeError("ChangeStream supports only one reader")
|
|
179
|
+
pending = asyncio.create_task(anext(self._iterator))
|
|
180
|
+
self._pending = pending
|
|
181
|
+
try:
|
|
182
|
+
return await pending
|
|
183
|
+
except BaseException:
|
|
184
|
+
self._closed = True
|
|
185
|
+
await self._iterator.aclose()
|
|
186
|
+
raise
|
|
187
|
+
finally:
|
|
188
|
+
self._pending = None
|
|
189
|
+
|
|
190
|
+
async def aclose(self) -> None:
|
|
191
|
+
"""Stop iteration, including a blocked read or reconnect delay."""
|
|
192
|
+
self._closed = True
|
|
193
|
+
pending = self._pending
|
|
194
|
+
if pending is not None:
|
|
195
|
+
pending.cancel()
|
|
196
|
+
try:
|
|
197
|
+
await pending
|
|
198
|
+
except (asyncio.CancelledError, StopAsyncIteration):
|
|
199
|
+
pass
|
|
200
|
+
await self._iterator.aclose()
|
|
201
|
+
|
|
202
|
+
async def _events(self) -> AsyncIterator[Event]:
|
|
203
|
+
transport = self._client.get_async_httpx_client()
|
|
204
|
+
while not self._closed:
|
|
205
|
+
headers = {
|
|
206
|
+
"Accept": "text/event-stream",
|
|
207
|
+
"Last-Event-ID": self.cursor or "",
|
|
208
|
+
}
|
|
209
|
+
terminal = None
|
|
210
|
+
try:
|
|
211
|
+
async with _connection(
|
|
212
|
+
transport,
|
|
213
|
+
params=self._params,
|
|
214
|
+
headers=headers,
|
|
215
|
+
timeout=self._timeout,
|
|
216
|
+
) as response:
|
|
217
|
+
response.raise_for_status()
|
|
218
|
+
if (
|
|
219
|
+
response.headers.get("content-type", "")
|
|
220
|
+
.split(";")[0]
|
|
221
|
+
.strip()
|
|
222
|
+
.lower()
|
|
223
|
+
!= "text/event-stream"
|
|
224
|
+
):
|
|
225
|
+
raise ValueError("Expected text/event-stream response")
|
|
226
|
+
async for event in _frames(response):
|
|
227
|
+
if event.event == "resync_required":
|
|
228
|
+
terminal = event
|
|
229
|
+
break
|
|
230
|
+
yield event
|
|
231
|
+
if event.id is not None:
|
|
232
|
+
self.cursor = event.id or None
|
|
233
|
+
except httpx.TransportError:
|
|
234
|
+
pass
|
|
235
|
+
if terminal is not None:
|
|
236
|
+
yield terminal
|
|
237
|
+
return
|
|
238
|
+
await asyncio.sleep(self._delay)
|
holder/exceptions.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Errors raised by the high-level Holder API."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HolderError(Exception):
|
|
5
|
+
"""Base error for the high-level client."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectionError(HolderError):
|
|
9
|
+
"""Daemon discovery, startup or transport failed."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ClosedError(HolderError):
|
|
13
|
+
"""The client is outside its active context."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProtocolError(HolderError):
|
|
17
|
+
"""The daemon returned an invalid or incompatible response."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class APIError(HolderError):
|
|
21
|
+
"""A failed daemon request, with its status and error envelope."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, status: int, code: str, message: str) -> None:
|
|
24
|
+
self.status = status
|
|
25
|
+
self.code = code
|
|
26
|
+
self.message = message
|
|
27
|
+
super().__init__(f"{status} {code}: {message}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AuthenticationError(APIError):
|
|
31
|
+
"""The daemon rejected the credentials."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NotFoundError(APIError):
|
|
35
|
+
"""The requested object was not found within its scope."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains methods for accessing the API"""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Contains endpoint functions for accessing the API"""
|