temperkb-py 0.5.1__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.
- temper/__init__.py +93 -0
- temper/_validate.py +147 -0
- temper/act.py +59 -0
- temper/client.py +137 -0
- temper/connection.py +353 -0
- temper/credentials.py +359 -0
- temper/errors.py +243 -0
- temper/generated/__init__.py +770 -0
- temper/generated/api/__init__.py +29 -0
- temper/generated/api/access_api.py +1420 -0
- temper/generated/api/auditor_api.py +888 -0
- temper/generated/api/blobs_api.py +3134 -0
- temper/generated/api/cognitive_maps_api.py +3980 -0
- temper/generated/api/contexts_api.py +4146 -0
- temper/generated/api/data_artifact_shapes_api.py +914 -0
- temper/generated/api/data_artifacts_api.py +998 -0
- temper/generated/api/events_api.py +607 -0
- temper/generated/api/facets_api.py +1627 -0
- temper/generated/api/graph_api.py +2506 -0
- temper/generated/api/health_api.py +295 -0
- temper/generated/api/ingest_api.py +1526 -0
- temper/generated/api/invitations_api.py +2036 -0
- temper/generated/api/invocations_api.py +1200 -0
- temper/generated/api/meta_api.py +629 -0
- temper/generated/api/profile_api.py +852 -0
- temper/generated/api/query_api.py +337 -0
- temper/generated/api/reblocking_api.py +337 -0
- temper/generated/api/relationships_api.py +1280 -0
- temper/generated/api/resources_api.py +5430 -0
- temper/generated/api/schema_api.py +847 -0
- temper/generated/api/search_api.py +334 -0
- temper/generated/api/slack_link_api.py +598 -0
- temper/generated/api/steward_api.py +1463 -0
- temper/generated/api/teams_api.py +2692 -0
- temper/generated/api_client.py +804 -0
- temper/generated/api_response.py +21 -0
- temper/generated/configuration.py +606 -0
- temper/generated/exceptions.py +218 -0
- temper/generated/models/__init__.py +353 -0
- temper/generated/models/accept_invitation_response.py +94 -0
- temper/generated/models/act_input.py +137 -0
- temper/generated/models/act_invocation.py +160 -0
- temper/generated/models/act_name.py +262 -0
- temper/generated/models/act_quantity.py +96 -0
- temper/generated/models/act_refusal.py +94 -0
- temper/generated/models/actor_authority.py +38 -0
- temper/generated/models/add_member_request.py +92 -0
- temper/generated/models/advance_watermark_ack.py +103 -0
- temper/generated/models/advance_watermark_request.py +101 -0
- temper/generated/models/agent_authorship.py +117 -0
- temper/generated/models/anchor_address_resolution.py +151 -0
- temper/generated/models/anchor_address_resolution_one_of.py +95 -0
- temper/generated/models/anchor_address_resolution_one_of1.py +104 -0
- temper/generated/models/anchor_address_resolution_one_of2.py +95 -0
- temper/generated/models/anchor_shape.py +114 -0
- temper/generated/models/anchor_verdict.py +38 -0
- temper/generated/models/append_block_payload.py +110 -0
- temper/generated/models/artifact_commit_request.py +163 -0
- temper/generated/models/artifact_commit_response.py +95 -0
- temper/generated/models/artifact_view.py +119 -0
- temper/generated/models/assert_relationship_request.py +154 -0
- temper/generated/models/atlas_edge.py +108 -0
- temper/generated/models/atlas_entry.py +112 -0
- temper/generated/models/atlas_home.py +106 -0
- temper/generated/models/atlas_node.py +139 -0
- temper/generated/models/atlas_subgraph.py +106 -0
- temper/generated/models/audit_citation.py +93 -0
- temper/generated/models/audit_sweep_row.py +93 -0
- temper/generated/models/auditor_dispatch_tick_request.py +93 -0
- temper/generated/models/auditor_dispatch_tick_response.py +104 -0
- temper/generated/models/auditor_job_complete_ack.py +96 -0
- temper/generated/models/bind_team_outcome.py +93 -0
- temper/generated/models/bind_team_request.py +89 -0
- temper/generated/models/blob_commit_response.py +104 -0
- temper/generated/models/blob_delete_ack.py +91 -0
- temper/generated/models/blob_relation_ack.py +89 -0
- temper/generated/models/blob_relation_assert_request.py +154 -0
- temper/generated/models/blob_relation_direction.py +37 -0
- temper/generated/models/blob_relation_edge_direction.py +37 -0
- temper/generated/models/blob_relation_row.py +121 -0
- temper/generated/models/blob_summary.py +103 -0
- temper/generated/models/blob_upload_begin_request.py +93 -0
- temper/generated/models/blob_upload_begin_response.py +89 -0
- temper/generated/models/blob_upload_finalize_request.py +98 -0
- temper/generated/models/blob_upload_progress.py +101 -0
- temper/generated/models/blob_upload_segment_info.py +93 -0
- temper/generated/models/block_chunk_ref.py +93 -0
- temper/generated/models/block_fold_disposition.py +151 -0
- temper/generated/models/block_fold_disposition_one_of.py +114 -0
- temper/generated/models/block_fold_disposition_one_of1.py +95 -0
- temper/generated/models/block_fold_disposition_one_of2.py +95 -0
- temper/generated/models/block_provenance_row.py +111 -0
- temper/generated/models/block_read.py +151 -0
- temper/generated/models/block_read_one_of.py +127 -0
- temper/generated/models/block_read_one_of1.py +116 -0
- temper/generated/models/block_read_one_of2.py +98 -0
- temper/generated/models/block_successor.py +96 -0
- temper/generated/models/blocks_response.py +98 -0
- temper/generated/models/body_storage.py +37 -0
- temper/generated/models/bridge.py +93 -0
- temper/generated/models/bulk_reassign_ack.py +89 -0
- temper/generated/models/bulk_reassign_request.py +91 -0
- temper/generated/models/change_role_request.py +89 -0
- temper/generated/models/charter_block.py +92 -0
- temper/generated/models/charter_disposition.py +39 -0
- temper/generated/models/citation_audit_request.py +104 -0
- temper/generated/models/citation_audit_row.py +111 -0
- temper/generated/models/claimed_audit_job.py +103 -0
- temper/generated/models/claimed_job.py +93 -0
- temper/generated/models/close_invocation_request.py +96 -0
- temper/generated/models/cogmap_analytics_row.py +105 -0
- temper/generated/models/cogmap_detail.py +112 -0
- temper/generated/models/cogmap_foundation_row.py +95 -0
- temper/generated/models/cogmap_grant_body.py +99 -0
- temper/generated/models/cogmap_region_metrics_row.py +126 -0
- temper/generated/models/cogmap_region_row.py +109 -0
- temper/generated/models/cogmap_regulation_row.py +100 -0
- temper/generated/models/cogmap_revoke_body.py +91 -0
- temper/generated/models/cogmap_row.py +108 -0
- temper/generated/models/cogmap_staleness.py +103 -0
- temper/generated/models/combine_node.py +104 -0
- temper/generated/models/combine_op.py +38 -0
- temper/generated/models/composition.py +103 -0
- temper/generated/models/composition_trace.py +96 -0
- temper/generated/models/confidence_band.py +38 -0
- temper/generated/models/content_response.py +109 -0
- temper/generated/models/context_create_request.py +99 -0
- temper/generated/models/context_owner_ref.py +154 -0
- temper/generated/models/context_owner_ref_one_of.py +88 -0
- temper/generated/models/context_owner_ref_one_of1.py +88 -0
- temper/generated/models/context_panorama.py +112 -0
- temper/generated/models/context_row.py +108 -0
- temper/generated/models/context_row_with_counts.py +110 -0
- temper/generated/models/create_cogmap_outcome.py +93 -0
- temper/generated/models/create_cogmap_request.py +116 -0
- temper/generated/models/create_invitation_request.py +91 -0
- temper/generated/models/create_request_body.py +102 -0
- temper/generated/models/create_review_body.py +93 -0
- temper/generated/models/deactivated.py +95 -0
- temper/generated/models/delete_response.py +88 -0
- temper/generated/models/denied.py +95 -0
- temper/generated/models/device_overrides.py +108 -0
- temper/generated/models/discouraged_open_meta_key.py +90 -0
- temper/generated/models/dispatch_tick_request.py +100 -0
- temper/generated/models/dispatch_tick_response.py +104 -0
- temper/generated/models/disposition.py +38 -0
- temper/generated/models/doc_type_description.py +106 -0
- temper/generated/models/doc_type_summary.py +92 -0
- temper/generated/models/drift_sweep_row.py +102 -0
- temper/generated/models/edge_facet_row.py +142 -0
- temper/generated/models/edge_facet_set_request.py +148 -0
- temper/generated/models/edge_facets_response.py +99 -0
- temper/generated/models/edge_filter.py +102 -0
- temper/generated/models/edge_kind.py +39 -0
- temper/generated/models/element_event.py +118 -0
- temper/generated/models/element_kind.py +37 -0
- temper/generated/models/embed_dispatch_summary.py +99 -0
- temper/generated/models/enforcement_mode.py +37 -0
- temper/generated/models/entitlements.py +106 -0
- temper/generated/models/entry_bounds.py +94 -0
- temper/generated/models/error_body.py +92 -0
- temper/generated/models/error_detail.py +101 -0
- temper/generated/models/error_details.py +137 -0
- temper/generated/models/event_cursor_response.py +94 -0
- temper/generated/models/event_trail.py +102 -0
- temper/generated/models/exact_arm.py +106 -0
- temper/generated/models/exact_hit.py +94 -0
- temper/generated/models/extent.py +151 -0
- temper/generated/models/extent_one_of.py +95 -0
- temper/generated/models/extent_one_of1.py +95 -0
- temper/generated/models/extent_one_of2.py +97 -0
- temper/generated/models/facet_ack.py +89 -0
- temper/generated/models/facet_predicate.py +90 -0
- temper/generated/models/facet_retract_ack.py +89 -0
- temper/generated/models/facet_set_request.py +143 -0
- temper/generated/models/finalize_payload.py +98 -0
- temper/generated/models/fold_relationship_request.py +144 -0
- temper/generated/models/grant_outcome.py +88 -0
- temper/generated/models/graph_edge_row.py +123 -0
- temper/generated/models/group_key_meta.py +92 -0
- temper/generated/models/health_response.py +97 -0
- temper/generated/models/home_cogmap.py +99 -0
- temper/generated/models/home_context.py +105 -0
- temper/generated/models/id_kind.py +194 -0
- temper/generated/models/id_provenance.py +137 -0
- temper/generated/models/id_provenance_one_of.py +98 -0
- temper/generated/models/id_provenance_one_of1.py +98 -0
- temper/generated/models/id_set.py +107 -0
- temper/generated/models/idp_revocation.py +38 -0
- temper/generated/models/illegal_transition.py +105 -0
- temper/generated/models/ingest_create_response.py +137 -0
- temper/generated/models/ingest_delta.py +120 -0
- temper/generated/models/ingest_payload.py +224 -0
- temper/generated/models/ingest_state.py +37 -0
- temper/generated/models/inherited_read_grant.py +93 -0
- temper/generated/models/inherited_share.py +91 -0
- temper/generated/models/input_source.py +151 -0
- temper/generated/models/input_source_one_of.py +108 -0
- temper/generated/models/input_source_one_of1.py +95 -0
- temper/generated/models/input_source_one_of2.py +95 -0
- temper/generated/models/insufficient_authority.py +100 -0
- temper/generated/models/intention.py +96 -0
- temper/generated/models/invitation_status.py +39 -0
- temper/generated/models/invitation_token_request.py +88 -0
- temper/generated/models/invitee_invitation.py +112 -0
- temper/generated/models/invocation_ack.py +91 -0
- temper/generated/models/invocation_act_row.py +114 -0
- temper/generated/models/invocation_close_ack.py +92 -0
- temper/generated/models/invocation_summary.py +112 -0
- temper/generated/models/invocation_view.py +148 -0
- temper/generated/models/join_request.py +145 -0
- temper/generated/models/join_request_status.py +39 -0
- temper/generated/models/kind_owner_input.py +137 -0
- temper/generated/models/kind_owner_input_one_of.py +89 -0
- temper/generated/models/kind_owner_input_one_of1.py +89 -0
- temper/generated/models/lineage_node.py +101 -0
- temper/generated/models/managed_meta.py +156 -0
- temper/generated/models/match_location.py +103 -0
- temper/generated/models/materialize_ack.py +118 -0
- temper/generated/models/materialize_delta.py +111 -0
- temper/generated/models/materialize_request.py +93 -0
- temper/generated/models/merge_policy.py +37 -0
- temper/generated/models/meta_update_payload.py +156 -0
- temper/generated/models/narrowed_by.py +94 -0
- temper/generated/models/no_prior_standing.py +95 -0
- temper/generated/models/no_standing.py +95 -0
- temper/generated/models/node_home.py +37 -0
- temper/generated/models/open_invocation_request.py +98 -0
- temper/generated/models/open_meta_convention.py +103 -0
- temper/generated/models/ord_op.py +39 -0
- temper/generated/models/orphan_node.py +109 -0
- temper/generated/models/outcome_declaration.py +97 -0
- temper/generated/models/pending_invitation_counts.py +95 -0
- temper/generated/models/plan_refusal.py +115 -0
- temper/generated/models/plan_refusal_details.py +96 -0
- temper/generated/models/polarity.py +37 -0
- temper/generated/models/profile.py +126 -0
- temper/generated/models/profile_auth_link.py +109 -0
- temper/generated/models/profile_update_request.py +111 -0
- temper/generated/models/profile_with_entitlements.py +122 -0
- temper/generated/models/property_op.py +151 -0
- temper/generated/models/property_op_one_of.py +95 -0
- temper/generated/models/property_op_one_of1.py +97 -0
- temper/generated/models/property_op_one_of2.py +105 -0
- temper/generated/models/property_predicate.py +94 -0
- temper/generated/models/provenance_source.py +151 -0
- temper/generated/models/provenance_source_one_of.py +98 -0
- temper/generated/models/provenance_source_one_of1.py +98 -0
- temper/generated/models/provenance_source_one_of2.py +97 -0
- temper/generated/models/public_system_settings.py +107 -0
- temper/generated/models/quantity_scale.py +151 -0
- temper/generated/models/quantity_scale_one_of.py +95 -0
- temper/generated/models/quantity_scale_one_of1.py +97 -0
- temper/generated/models/quantity_scale_one_of2.py +95 -0
- temper/generated/models/query_response.py +107 -0
- temper/generated/models/reassign_ack.py +91 -0
- temper/generated/models/reassign_context_outcome.py +113 -0
- temper/generated/models/reassign_context_request.py +89 -0
- temper/generated/models/reassign_resource_request.py +89 -0
- temper/generated/models/reblock_candidate.py +95 -0
- temper/generated/models/reblock_outcome.py +230 -0
- temper/generated/models/reblock_outcome_one_of.py +92 -0
- temper/generated/models/reblock_outcome_one_of1.py +92 -0
- temper/generated/models/reblock_outcome_one_of1_in_progress.py +88 -0
- temper/generated/models/reblock_outcome_one_of2.py +92 -0
- temper/generated/models/reblock_outcome_one_of3.py +92 -0
- temper/generated/models/reblock_outcome_one_of4.py +92 -0
- temper/generated/models/reblock_outcome_one_of4_error.py +88 -0
- temper/generated/models/reblock_outcome_one_of_reblocked.py +89 -0
- temper/generated/models/reblock_receipt.py +114 -0
- temper/generated/models/reblock_request.py +109 -0
- temper/generated/models/reblock_scope.py +154 -0
- temper/generated/models/reblock_scope_one_of.py +89 -0
- temper/generated/models/reblock_scope_one_of1.py +89 -0
- temper/generated/models/reblock_summary.py +99 -0
- temper/generated/models/reconcile_cogmap_request.py +127 -0
- temper/generated/models/reconcile_edge.py +102 -0
- temper/generated/models/reconcile_edge_tombstone.py +93 -0
- temper/generated/models/reconcile_entry.py +116 -0
- temper/generated/models/reconcile_outcome.py +98 -0
- temper/generated/models/reconcile_telos.py +96 -0
- temper/generated/models/reconcile_telos_block.py +90 -0
- temper/generated/models/reconcile_tombstone.py +89 -0
- temper/generated/models/refusal.py +235 -0
- temper/generated/models/refusal_reason.py +670 -0
- temper/generated/models/region_disclosure.py +91 -0
- temper/generated/models/region_hit.py +98 -0
- temper/generated/models/relationship_ack.py +89 -0
- temper/generated/models/relationship_target.py +37 -0
- temper/generated/models/remove_member_outcome.py +92 -0
- temper/generated/models/rename_context_outcome.py +99 -0
- temper/generated/models/rename_context_request.py +88 -0
- temper/generated/models/requested.py +95 -0
- temper/generated/models/residual_bucket.py +90 -0
- temper/generated/models/residual_context.py +91 -0
- temper/generated/models/residual_groups.py +98 -0
- temper/generated/models/residual_owned_reach.py +99 -0
- temper/generated/models/resource_annotate_request.py +154 -0
- temper/generated/models/resource_create_request.py +152 -0
- temper/generated/models/resource_facet_row.py +126 -0
- temper/generated/models/resource_facets.py +92 -0
- temper/generated/models/resource_facets_response.py +99 -0
- temper/generated/models/resource_filter.py +139 -0
- temper/generated/models/resource_grant_body.py +99 -0
- temper/generated/models/resource_hit.py +119 -0
- temper/generated/models/resource_lineage.py +108 -0
- temper/generated/models/resource_list_response.py +117 -0
- temper/generated/models/resource_revoke_body.py +91 -0
- temper/generated/models/resource_section.py +38 -0
- temper/generated/models/resource_sort_field.py +42 -0
- temper/generated/models/resource_update_request.py +235 -0
- temper/generated/models/resource_view.py +202 -0
- temper/generated/models/restore_context_outcome.py +97 -0
- temper/generated/models/retire_context_outcome.py +95 -0
- temper/generated/models/return_spec.py +102 -0
- temper/generated/models/retype_relationship_request.py +143 -0
- temper/generated/models/revoke_outcome.py +88 -0
- temper/generated/models/revoked.py +95 -0
- temper/generated/models/reweight_relationship_request.py +139 -0
- temper/generated/models/score_kind.py +194 -0
- temper/generated/models/scoring.py +94 -0
- temper/generated/models/search_params.py +152 -0
- temper/generated/models/search_reason.py +38 -0
- temper/generated/models/search_response.py +104 -0
- temper/generated/models/search_result_row.py +115 -0
- temper/generated/models/search_scope.py +38 -0
- temper/generated/models/search_scope_info.py +96 -0
- temper/generated/models/segment_info.py +91 -0
- temper/generated/models/segmented_begin.py +103 -0
- temper/generated/models/segmented_begin_response.py +103 -0
- temper/generated/models/shape_declare_request.py +160 -0
- temper/generated/models/shape_emptiness.py +39 -0
- temper/generated/models/shape_view.py +116 -0
- temper/generated/models/share_context_outcome.py +93 -0
- temper/generated/models/share_context_request.py +89 -0
- temper/generated/models/slack_disconnect_request.py +88 -0
- temper/generated/models/slack_disconnect_response.py +96 -0
- temper/generated/models/slack_disconnected_principal.py +95 -0
- temper/generated/models/slice_request.py +95 -0
- temper/generated/models/sort_order.py +37 -0
- temper/generated/models/stage_disposition.py +39 -0
- temper/generated/models/stage_input.py +137 -0
- temper/generated/models/stage_input_one_of.py +104 -0
- temper/generated/models/stage_input_one_of1.py +111 -0
- temper/generated/models/stage_input_trace.py +97 -0
- temper/generated/models/stage_node.py +137 -0
- temper/generated/models/stage_output.py +137 -0
- temper/generated/models/stage_output_one_of.py +105 -0
- temper/generated/models/stage_output_one_of1.py +105 -0
- temper/generated/models/stage_relation.py +37 -0
- temper/generated/models/stage_result.py +162 -0
- temper/generated/models/stage_trace.py +163 -0
- temper/generated/models/standing.py +40 -0
- temper/generated/models/standing_shape.py +103 -0
- temper/generated/models/subscription.py +118 -0
- temper/generated/models/subscription_override.py +108 -0
- temper/generated/models/system_access_details.py +120 -0
- temper/generated/models/team_create_request.py +110 -0
- temper/generated/models/team_detail.py +121 -0
- temper/generated/models/team_invitation.py +115 -0
- temper/generated/models/team_member_detail.py +97 -0
- temper/generated/models/team_member_row.py +97 -0
- temper/generated/models/team_member_source.py +37 -0
- temper/generated/models/team_role.py +39 -0
- temper/generated/models/team_row.py +111 -0
- temper/generated/models/team_update_request.py +100 -0
- temper/generated/models/territory.py +117 -0
- temper/generated/models/territory_kind.py +38 -0
- temper/generated/models/territory_overview.py +116 -0
- temper/generated/models/unbind_team_outcome.py +93 -0
- temper/generated/models/unrecognized_standing.py +97 -0
- temper/generated/models/unshare_context_outcome.py +93 -0
- temper/generated/models/vault_config.py +118 -0
- temper/generated/models/via_entry.py +106 -0
- temper/generated/models/wide_arm.py +108 -0
- temper/generated/models/wide_hit.py +94 -0
- temper/generated/py.typed +0 -0
- temper/generated/rest.py +263 -0
- temper/py.typed +0 -0
- temper/refs.py +42 -0
- temper/version.py +11 -0
- temperkb_py-0.5.1.dist-info/METADATA +250 -0
- temperkb_py-0.5.1.dist-info/RECORD +385 -0
- temperkb_py-0.5.1.dist-info/WHEEL +4 -0
- temperkb_py-0.5.1.dist-info/licenses/LICENSE +21 -0
temper/__init__.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""The Python client for the Temper knowledge base API.
|
|
2
|
+
|
|
3
|
+
Sibling of `temper-rb` and `temper-ts`; the three are pinned to the same wire
|
|
4
|
+
contracts. `temper/generated/` is emitted from the repo-root `openapi.json` — itself
|
|
5
|
+
a product of the Axum router — and is NEVER hand-edited: `cargo make openapi`
|
|
6
|
+
regenerates it, and `openapi-py-drift` (in `cargo make check`, and in CI) fails if the
|
|
7
|
+
committed copy has fallen behind the contract.
|
|
8
|
+
|
|
9
|
+
import temper
|
|
10
|
+
from temper.generated.api.resources_api import ResourcesApi
|
|
11
|
+
|
|
12
|
+
temper.configure(base_url="https://temperkb.io")
|
|
13
|
+
client = temper.Client(temper.ClientCredentials(
|
|
14
|
+
token_url="https://temperkb.io/oauth/token",
|
|
15
|
+
client_id="tmpr_...",
|
|
16
|
+
client_secret="...",
|
|
17
|
+
))
|
|
18
|
+
|
|
19
|
+
client.whoami()
|
|
20
|
+
client.call(lambda api: ResourcesApi(api).get_resource(uuid), idempotent=True)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from temper.act import Act
|
|
24
|
+
from temper.client import MAX_READ_ATTEMPTS, Client, default_backoff
|
|
25
|
+
from temper.connection import (
|
|
26
|
+
SURFACE,
|
|
27
|
+
Connection,
|
|
28
|
+
api_client,
|
|
29
|
+
configure,
|
|
30
|
+
current_connection,
|
|
31
|
+
current_token,
|
|
32
|
+
reset_connection,
|
|
33
|
+
with_token,
|
|
34
|
+
)
|
|
35
|
+
from temper.credentials import BearerToken, ClientCredentials, Credentials
|
|
36
|
+
from temper.errors import (
|
|
37
|
+
BadRequest,
|
|
38
|
+
Conflict,
|
|
39
|
+
Forbidden,
|
|
40
|
+
NotFound,
|
|
41
|
+
PermanentError,
|
|
42
|
+
RateLimited,
|
|
43
|
+
ServerError,
|
|
44
|
+
SystemAccessRequired,
|
|
45
|
+
TemperError,
|
|
46
|
+
TransientError,
|
|
47
|
+
TransportError,
|
|
48
|
+
Unauthorized,
|
|
49
|
+
map_error,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# The contract this package was generated against. `generate-temper-py.sh` passes
|
|
53
|
+
# openapi.json's info.version to the generator as `packageVersion`, so the generated
|
|
54
|
+
# tree already carries it — we alias it rather than reasserting it, and callers never
|
|
55
|
+
# reach into `temper.generated` for it. Independent of `__version__`, which is the
|
|
56
|
+
# SDK's own SemVer: a package version and an API version answer different questions.
|
|
57
|
+
from temper.generated import __version__ as CONTRACT_VERSION
|
|
58
|
+
from temper.refs import parse_ref
|
|
59
|
+
from temper.version import __version__
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"CONTRACT_VERSION",
|
|
63
|
+
"MAX_READ_ATTEMPTS",
|
|
64
|
+
"SURFACE",
|
|
65
|
+
"Act",
|
|
66
|
+
"BadRequest",
|
|
67
|
+
"BearerToken",
|
|
68
|
+
"Client",
|
|
69
|
+
"ClientCredentials",
|
|
70
|
+
"Conflict",
|
|
71
|
+
"Connection",
|
|
72
|
+
"Credentials",
|
|
73
|
+
"Forbidden",
|
|
74
|
+
"NotFound",
|
|
75
|
+
"PermanentError",
|
|
76
|
+
"RateLimited",
|
|
77
|
+
"ServerError",
|
|
78
|
+
"SystemAccessRequired",
|
|
79
|
+
"TemperError",
|
|
80
|
+
"TransientError",
|
|
81
|
+
"TransportError",
|
|
82
|
+
"Unauthorized",
|
|
83
|
+
"__version__",
|
|
84
|
+
"api_client",
|
|
85
|
+
"configure",
|
|
86
|
+
"current_connection",
|
|
87
|
+
"current_token",
|
|
88
|
+
"default_backoff",
|
|
89
|
+
"map_error",
|
|
90
|
+
"parse_ref",
|
|
91
|
+
"reset_connection",
|
|
92
|
+
"with_token",
|
|
93
|
+
]
|
temper/_validate.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""What a caller is allowed to hand this package, checked once, at the seam.
|
|
2
|
+
|
|
3
|
+
Three kinds of value arrive from a caller and then get put somewhere that cannot
|
|
4
|
+
defend itself:
|
|
5
|
+
|
|
6
|
+
* An ENDPOINT — `configure(base_url=...)`, `ClientCredentials(token_url=...)`. Every
|
|
7
|
+
API request puts a bearer token on the first; every mint puts the client_secret on
|
|
8
|
+
the second. So the scheme is not cosmetic, and neither is userinfo: `urlsplit` will
|
|
9
|
+
happily accept ``https://id:secret@host/`` and the secret then rides in every error
|
|
10
|
+
message that names the URL.
|
|
11
|
+
* An OPAQUE SECRET or IDENTIFIER — a token, a client id, a device id. These end up in
|
|
12
|
+
an HTTP header or a form field. The failure that actually happens in the field is
|
|
13
|
+
not an attack: it is ``TEMPER_M2M_CLIENT_SECRET=$(cat secret.txt)`` keeping the
|
|
14
|
+
trailing newline, which produces an `invalid_client` no amount of squinting at the
|
|
15
|
+
secret explains. Rejecting it here names it.
|
|
16
|
+
* A plain non-empty string.
|
|
17
|
+
|
|
18
|
+
Checked at CONSTRUCTION, not at first use. `Connection` builds its `ApiClient` lazily,
|
|
19
|
+
so a value validated in `_build()` would surface its error on the first API call —
|
|
20
|
+
several layers and possibly several minutes from the `configure()` that caused it.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import ipaddress
|
|
26
|
+
from urllib.parse import SplitResult, urlsplit
|
|
27
|
+
|
|
28
|
+
#: Hostnames that are the local machine by definition. `.localhost` is reserved for
|
|
29
|
+
#: exactly this by RFC 6761 §6.3, and Docker/CI setups do use `foo.localhost`.
|
|
30
|
+
_LOOPBACK_NAMES = frozenset({"localhost"})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def require_str(value: object, name: str) -> str:
|
|
34
|
+
"""A non-empty `str`, and nothing more."""
|
|
35
|
+
if not isinstance(value, str) or not value:
|
|
36
|
+
raise ValueError(f"{name} must be a non-empty str")
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def require_opaque(value: object, name: str) -> str:
|
|
41
|
+
"""A non-empty `str` safe to put in a header value or a form field, verbatim.
|
|
42
|
+
|
|
43
|
+
Rejects rather than strips. A stripped value is a guess about what the caller
|
|
44
|
+
meant, and the guess is wrong precisely when it matters: a secret with a stray
|
|
45
|
+
space in the MIDDLE is a different secret, not a formatting slip, and silently
|
|
46
|
+
sending a trimmed one would turn a clear error into a 401 with no cause attached.
|
|
47
|
+
|
|
48
|
+
The whitespace rule is also what keeps a `\\r\\n` out of an `Authorization` or
|
|
49
|
+
`X-Temper-Device-Id` header. urllib3 v2 rejects such a header itself — but it
|
|
50
|
+
does so on the request, wrapped in a transport error, with nothing pointing back
|
|
51
|
+
at the `configure()` or `BearerToken(...)` that introduced it.
|
|
52
|
+
"""
|
|
53
|
+
text = require_str(value, name)
|
|
54
|
+
if text != text.strip():
|
|
55
|
+
raise ValueError(
|
|
56
|
+
f"{name} has leading or trailing whitespace; "
|
|
57
|
+
f"strip it at the source (a secret read from a file keeps its newline)"
|
|
58
|
+
)
|
|
59
|
+
if any(ch.isspace() for ch in text):
|
|
60
|
+
raise ValueError(f"{name} must not contain whitespace")
|
|
61
|
+
# Cc/Cf and friends: never part of a real token, and a `\r\n` here is a header
|
|
62
|
+
# split. `str.isprintable()` is false for every control character AND for the
|
|
63
|
+
# separators the whitespace check above already caught.
|
|
64
|
+
if not text.isprintable():
|
|
65
|
+
raise ValueError(f"{name} must not contain control characters")
|
|
66
|
+
return text
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def require_endpoint(
|
|
70
|
+
value: object,
|
|
71
|
+
*,
|
|
72
|
+
name: str,
|
|
73
|
+
allow_insecure_http: bool = False,
|
|
74
|
+
) -> SplitResult:
|
|
75
|
+
"""An absolute http(s) origin this package is willing to put a secret on.
|
|
76
|
+
|
|
77
|
+
Returns the parsed URL so a caller can reuse the parts rather than re-splitting.
|
|
78
|
+
|
|
79
|
+
`http://` is refused off the loopback interface: a bearer token and a
|
|
80
|
+
client_secret both travel in the clear over it, to anything on the path. Loopback
|
|
81
|
+
is exempt because that is what a test server and a `temper serve` on your laptop
|
|
82
|
+
are, and `allow_insecure_http=True` is the deliberate opt-out for the case this
|
|
83
|
+
cannot see — a private network where TLS terminates elsewhere. It is a keyword a
|
|
84
|
+
caller has to write, which is the whole point: `verify_ssl=False` used to be a
|
|
85
|
+
typo away.
|
|
86
|
+
"""
|
|
87
|
+
text = require_str(value, name)
|
|
88
|
+
# `urlsplit` SILENTLY strips tab, CR and LF anywhere in the URL (CVE-2019-9740's
|
|
89
|
+
# fix). Silently is the problem: a `base_url` with an embedded newline would be
|
|
90
|
+
# accepted here and normalized into something the caller never wrote.
|
|
91
|
+
if not text.isprintable():
|
|
92
|
+
raise ValueError(f"{name} must not contain whitespace or control characters")
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
parts = urlsplit(text)
|
|
96
|
+
except ValueError as exc: # a malformed IPv6 literal, principally
|
|
97
|
+
raise ValueError(f"{name} is not a parseable URL: {text!r}") from exc
|
|
98
|
+
|
|
99
|
+
if parts.scheme not in ("http", "https") or not parts.netloc:
|
|
100
|
+
raise ValueError(f"{name} must be an absolute http(s) URL, got {text!r}")
|
|
101
|
+
|
|
102
|
+
# `parts.username` is None for `host:port`, so this catches ONLY a real userinfo
|
|
103
|
+
# section. Refused rather than dropped: a caller who wrote credentials into the
|
|
104
|
+
# URL meant them to authenticate something, and quietly discarding them would
|
|
105
|
+
# produce a 401 whose cause is invisible.
|
|
106
|
+
if parts.username is not None or parts.password is not None:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"{name} must not carry userinfo (user:password@); "
|
|
109
|
+
f"pass credentials to ClientCredentials or BearerToken instead"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Accessing it is the check: `port` raises for one out of range or not a number.
|
|
114
|
+
_ = parts.port
|
|
115
|
+
except ValueError as exc:
|
|
116
|
+
raise ValueError(f"{name} has an invalid port: {text!r}") from exc
|
|
117
|
+
|
|
118
|
+
if parts.query or parts.fragment:
|
|
119
|
+
raise ValueError(
|
|
120
|
+
f"{name} must be an origin (optionally with a path prefix), "
|
|
121
|
+
f"not a URL with a query or fragment: {text!r}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if parts.scheme == "http" and not (allow_insecure_http or is_loopback(parts.hostname)):
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"{name} is plaintext http to a non-loopback host, which would put the "
|
|
127
|
+
f"bearer token and client_secret on the wire in the clear; use https, or "
|
|
128
|
+
f"pass allow_insecure_http=True to accept that deliberately"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return parts
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def is_loopback(hostname: str | None) -> bool:
|
|
135
|
+
"""Whether `hostname` names this machine, by literal address or by reserved name.
|
|
136
|
+
|
|
137
|
+
`urlsplit.hostname` is already lowercased and already has the brackets stripped
|
|
138
|
+
off an IPv6 literal, which is what makes `ip_address` the right test here — and
|
|
139
|
+
it covers the whole 127.0.0.0/8 block, not just 127.0.0.1.
|
|
140
|
+
"""
|
|
141
|
+
if not hostname:
|
|
142
|
+
return False
|
|
143
|
+
try:
|
|
144
|
+
return ipaddress.ip_address(hostname).is_loopback
|
|
145
|
+
except ValueError:
|
|
146
|
+
pass
|
|
147
|
+
return hostname in _LOOPBACK_NAMES or hostname.endswith(".localhost")
|
temper/act.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Act context for a write. Optional on every call.
|
|
2
|
+
|
|
3
|
+
The constructor invariant mirrors `ActInput::into_act_context`: Rust's
|
|
4
|
+
`AgentAuthorship.confidence` is non-Option, so authorship without confidence is a 400.
|
|
5
|
+
Rejecting it here is the parse-don't-validate answer — an invalid `Act` cannot be
|
|
6
|
+
constructed, so no call site can send one.
|
|
7
|
+
|
|
8
|
+
`correlation` and `invocation` are exempt: correlation is provenance, never
|
|
9
|
+
authorship, and an act with no supplied correlation self-roots to its own event id.
|
|
10
|
+
Nothing gates on it, so a client may always omit it.
|
|
11
|
+
|
|
12
|
+
Ported from `clients/temper-rb/lib/temper/act.rb`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
_AUTHORSHIP_FIELDS = ("reasoning", "rationale", "persona", "model")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Act:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
*,
|
|
26
|
+
confidence: Any | None = None,
|
|
27
|
+
reasoning: str | None = None,
|
|
28
|
+
rationale: str | None = None,
|
|
29
|
+
persona: str | None = None,
|
|
30
|
+
model: str | None = None,
|
|
31
|
+
correlation: str | None = None,
|
|
32
|
+
invocation: str | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
authorship = {
|
|
35
|
+
"reasoning": reasoning,
|
|
36
|
+
"rationale": rationale,
|
|
37
|
+
"persona": persona,
|
|
38
|
+
"model": model,
|
|
39
|
+
}
|
|
40
|
+
if confidence is None:
|
|
41
|
+
supplied = [k for k in _AUTHORSHIP_FIELDS if authorship[k] is not None]
|
|
42
|
+
if supplied:
|
|
43
|
+
raise ValueError("Act requires `confidence` when supplying " + ", ".join(supplied))
|
|
44
|
+
|
|
45
|
+
fields: dict[str, Any] = dict(authorship)
|
|
46
|
+
fields["confidence"] = None if confidence is None else str(confidence)
|
|
47
|
+
fields["correlation_id"] = correlation
|
|
48
|
+
fields["invocation_id"] = invocation
|
|
49
|
+
# Nils omitted: the server distinguishes an absent key from null.
|
|
50
|
+
self._fields = {k: v for k, v in fields.items() if v is not None}
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict[str, Any]:
|
|
53
|
+
"""The seven `ActInput` wire keys.
|
|
54
|
+
|
|
55
|
+
They flatten into ~30 write bodies and onto the query string of
|
|
56
|
+
`DELETE /api/resources/{id}`, so this is a plain dict rather than a model:
|
|
57
|
+
both destinations take it as keyword arguments.
|
|
58
|
+
"""
|
|
59
|
+
return dict(self._fields)
|
temper/client.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""The call seam: retry policy, 401 repair, and error translation in one place.
|
|
2
|
+
|
|
3
|
+
A cheap façade over the process-global connection. Holds a credential and nothing
|
|
4
|
+
else; constructing one does no I/O, so a threaded server can build one per user and a
|
|
5
|
+
worker process can memoize one.
|
|
6
|
+
|
|
7
|
+
WHY THERE ARE NO PER-ENDPOINT METHODS HERE. The gem hand-writes `Resources`,
|
|
8
|
+
`Contexts`, `CognitiveMaps` because a Ruby caller otherwise passes an untyped hash.
|
|
9
|
+
The generated Python core already answers that: every operation is a typed method
|
|
10
|
+
taking pydantic models, so a hand-written `resources.create(...)` would be a second,
|
|
11
|
+
worse spelling of something already correct — and a place for the two to drift. This
|
|
12
|
+
is the same call temper-ts makes, for the same reason, in `src/client.ts`.
|
|
13
|
+
|
|
14
|
+
What the generated core does NOT answer is which failures are worth retrying and who
|
|
15
|
+
repairs a dead token. That is what `call()` is.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from typing import Any, TypeVar
|
|
23
|
+
|
|
24
|
+
import urllib3.exceptions
|
|
25
|
+
|
|
26
|
+
from temper import connection as _connection
|
|
27
|
+
from temper.credentials import Credentials
|
|
28
|
+
from temper.errors import TemperError, TransientError, Unauthorized, map_error
|
|
29
|
+
from temper.generated.api.profile_api import ProfileApi
|
|
30
|
+
from temper.generated.api.search_api import SearchApi
|
|
31
|
+
from temper.generated.api_client import ApiClient
|
|
32
|
+
from temper.generated.exceptions import ApiException
|
|
33
|
+
from temper.generated.models.profile_with_entitlements import ProfileWithEntitlements
|
|
34
|
+
from temper.generated.models.search_params import SearchParams
|
|
35
|
+
from temper.generated.models.search_response import SearchResponse
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
|
|
39
|
+
#: 200ms, 400ms — mirroring MAX_ATTEMPTS and the backoff in
|
|
40
|
+
#: crates/temper-client/src/http.rs, and the gem's Client::DEFAULT_BACKOFF.
|
|
41
|
+
MAX_READ_ATTEMPTS = 3
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def default_backoff(attempt: int) -> None:
|
|
45
|
+
time.sleep(0.2 * (2 ** (attempt - 1)))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Client:
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
credentials: Credentials,
|
|
52
|
+
*,
|
|
53
|
+
backoff: Callable[[int], None] = default_backoff,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._credentials = credentials
|
|
56
|
+
self._backoff = backoff
|
|
57
|
+
|
|
58
|
+
def call(self, fn: Callable[[ApiClient], T], *, idempotent: bool = False) -> T:
|
|
59
|
+
"""The one seam every call goes through.
|
|
60
|
+
|
|
61
|
+
``idempotent=True`` — a safe method. 5xx and transport failures retry.
|
|
62
|
+
``idempotent=False`` — a write. NEVER auto-retried.
|
|
63
|
+
|
|
64
|
+
A 401 is repaired once, for reads and writes alike: re-authenticating is not
|
|
65
|
+
re-submitting. A credential that cannot mint gets its 401 back untouched —
|
|
66
|
+
`BearerToken.refresh()` raises, and raising here would replace temper's real
|
|
67
|
+
answer with a message about the client's own plumbing.
|
|
68
|
+
|
|
69
|
+
Usage::
|
|
70
|
+
|
|
71
|
+
client.call(
|
|
72
|
+
lambda api: ResourcesApi(api).get_resource(resource_id),
|
|
73
|
+
idempotent=True,
|
|
74
|
+
)
|
|
75
|
+
"""
|
|
76
|
+
attempt = 0
|
|
77
|
+
reminted = False
|
|
78
|
+
api = _connection.api_client()
|
|
79
|
+
|
|
80
|
+
while True:
|
|
81
|
+
attempt += 1
|
|
82
|
+
try:
|
|
83
|
+
with _connection.with_token(self._credentials.token()):
|
|
84
|
+
return fn(api)
|
|
85
|
+
except (ApiException, urllib3.exceptions.HTTPError) as caught:
|
|
86
|
+
# Bound outside the `except` so the retry/repair decisions below read as
|
|
87
|
+
# straight-line code rather than as branches nested in a handler. The
|
|
88
|
+
# cost is that Python's implicit chaining does not apply out here — hence
|
|
89
|
+
# the explicit `from raw` on the raise, without which the caller's
|
|
90
|
+
# traceback would stop at the mapped error and lose the urllib3 or
|
|
91
|
+
# ApiException frame that says what actually happened.
|
|
92
|
+
error, raw = map_error(caught), caught
|
|
93
|
+
|
|
94
|
+
if self._repair_credentials(error, reminted):
|
|
95
|
+
reminted = True
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
if not self._retryable_read(error, idempotent, attempt):
|
|
99
|
+
raise error from raw
|
|
100
|
+
|
|
101
|
+
self._backoff(attempt)
|
|
102
|
+
|
|
103
|
+
# -- conveniences the gem also puts on Client itself ----------------------
|
|
104
|
+
|
|
105
|
+
def whoami(self) -> ProfileWithEntitlements:
|
|
106
|
+
"""Assert the machine profile resolved, and report what it can reach.
|
|
107
|
+
|
|
108
|
+
Authentication is not authorization. A minted M2M token does not even yield a
|
|
109
|
+
profile on its own: the client_id must already be registered (lookup-or-401 —
|
|
110
|
+
there is no JIT-create branch), and then, without a cogmap write grant and team
|
|
111
|
+
membership, every call authenticates cleanly and 403s. Discovering that here
|
|
112
|
+
beats discovering it on the first write.
|
|
113
|
+
"""
|
|
114
|
+
return self.call(lambda api: ProfileApi(api).get_profile(), idempotent=True)
|
|
115
|
+
|
|
116
|
+
def search(self, query: str, **opts: Any) -> SearchResponse:
|
|
117
|
+
"""`SearchParams` names the field `query`, not `q`."""
|
|
118
|
+
params = SearchParams(query=query, **opts)
|
|
119
|
+
return self.call(lambda api: SearchApi(api).search(params), idempotent=True)
|
|
120
|
+
|
|
121
|
+
# -- internals -----------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def _repair_credentials(self, error: TemperError, reminted: bool) -> bool:
|
|
124
|
+
if not isinstance(error, Unauthorized) or reminted:
|
|
125
|
+
return False
|
|
126
|
+
if not self._credentials.can_refresh:
|
|
127
|
+
# The gem reaches the same outcome by letting BearerToken#refresh! raise
|
|
128
|
+
# its own Unauthorized. Asking first is the same decision made where it
|
|
129
|
+
# can be read, and it keeps the SERVER's 401 — body and all — as the
|
|
130
|
+
# error the caller sees.
|
|
131
|
+
return False
|
|
132
|
+
self._credentials.refresh()
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _retryable_read(error: TemperError, idempotent: bool, attempt: int) -> bool:
|
|
137
|
+
return idempotent and isinstance(error, TransientError) and attempt < MAX_READ_ATTEMPTS
|