spatialflow 0.1.0__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.
- spatialflow/__init__.py +91 -0
- spatialflow/_generated/.github/workflows/python.yml +31 -0
- spatialflow/_generated/.gitignore +66 -0
- spatialflow/_generated/.gitlab-ci.yml +31 -0
- spatialflow/_generated/.openapi-generator/FILES +390 -0
- spatialflow/_generated/.openapi-generator/VERSION +1 -0
- spatialflow/_generated/.openapi-generator-ignore +23 -0
- spatialflow/_generated/.spec-hash +1 -0
- spatialflow/_generated/.travis.yml +17 -0
- spatialflow/_generated/README.md +537 -0
- spatialflow/_generated/__init__.py +1 -0
- spatialflow/_generated/docs/APIUsageStats.md +32 -0
- spatialflow/_generated/docs/AccountApi.md +1751 -0
- spatialflow/_generated/docs/ActionDeliverySuccessMetrics.md +32 -0
- spatialflow/_generated/docs/ActionResponse.md +32 -0
- spatialflow/_generated/docs/ActionRetryConfigSchema.md +32 -0
- spatialflow/_generated/docs/ActivitySummary.md +30 -0
- spatialflow/_generated/docs/AdminApi.md +1787 -0
- spatialflow/_generated/docs/ApiKeyCreateRequest.md +31 -0
- spatialflow/_generated/docs/ApiKeyCreateResponse.md +31 -0
- spatialflow/_generated/docs/ApiKeyResponse.md +38 -0
- spatialflow/_generated/docs/ApiKeyUpdateRequest.md +33 -0
- spatialflow/_generated/docs/AsyncUploadGeofencesResponse.md +33 -0
- spatialflow/_generated/docs/AuthTypeEnum.md +17 -0
- spatialflow/_generated/docs/AuthenticationApi.md +1289 -0
- spatialflow/_generated/docs/BatchLocationUpdateIn.md +30 -0
- spatialflow/_generated/docs/BillingApi.md +925 -0
- spatialflow/_generated/docs/BulkGeofenceRequest.md +29 -0
- spatialflow/_generated/docs/ChangePasswordSchema.md +30 -0
- spatialflow/_generated/docs/CheckoutSessionRequest.md +32 -0
- spatialflow/_generated/docs/CheckoutSessionResponse.md +31 -0
- spatialflow/_generated/docs/CircuitBreakerSchema.md +33 -0
- spatialflow/_generated/docs/ConfigFieldDefinitionRequest.md +41 -0
- spatialflow/_generated/docs/ConfigFieldDefinitionResponse.md +43 -0
- spatialflow/_generated/docs/ConfirmPasswordResetSchema.md +31 -0
- spatialflow/_generated/docs/CreateFromTemplateIn.md +30 -0
- spatialflow/_generated/docs/CreateGeofenceRequest.md +36 -0
- spatialflow/_generated/docs/CreateIntegrationSchema.md +33 -0
- spatialflow/_generated/docs/CreateUserSchema.md +35 -0
- spatialflow/_generated/docs/CreateWebhookRequest.md +43 -0
- spatialflow/_generated/docs/DashboardComparisonMetrics.md +32 -0
- spatialflow/_generated/docs/DashboardMetricsResponse.md +36 -0
- spatialflow/_generated/docs/DefaultApi.md +585 -0
- spatialflow/_generated/docs/DeleteFileResponse.md +30 -0
- spatialflow/_generated/docs/DeliveryStatusEnum.md +15 -0
- spatialflow/_generated/docs/DeviceIn.md +32 -0
- spatialflow/_generated/docs/DeviceOut.md +37 -0
- spatialflow/_generated/docs/DevicesApi.md +1213 -0
- spatialflow/_generated/docs/E2ETestApi.md +271 -0
- spatialflow/_generated/docs/EmailApi.md +541 -0
- spatialflow/_generated/docs/EmailHealthResponse.md +31 -0
- spatialflow/_generated/docs/EmailQueueStats.md +34 -0
- spatialflow/_generated/docs/EmailStats.md +31 -0
- spatialflow/_generated/docs/EmailStatusResponse.md +36 -0
- spatialflow/_generated/docs/ErrorResponse.md +32 -0
- spatialflow/_generated/docs/ExecutionOut.md +38 -0
- spatialflow/_generated/docs/ExportIntegrationSchema.md +35 -0
- spatialflow/_generated/docs/FileListResponse.md +31 -0
- spatialflow/_generated/docs/ForgotPasswordSchema.md +29 -0
- spatialflow/_generated/docs/GPXPlaybackOut.md +42 -0
- spatialflow/_generated/docs/GPXRouteOut.md +40 -0
- spatialflow/_generated/docs/GPXSimulatorApi.md +883 -0
- spatialflow/_generated/docs/GeoJSONPoint.md +31 -0
- spatialflow/_generated/docs/GeofenceListResponse.md +31 -0
- spatialflow/_generated/docs/GeofenceResponse.md +43 -0
- spatialflow/_generated/docs/GeofenceStats.md +30 -0
- spatialflow/_generated/docs/GeofenceTestResult.md +33 -0
- spatialflow/_generated/docs/GeofencesApi.md +1524 -0
- spatialflow/_generated/docs/HealthCheckResponse.md +31 -0
- spatialflow/_generated/docs/HealthResponse.md +32 -0
- spatialflow/_generated/docs/ImportIntegrationSchema.md +32 -0
- spatialflow/_generated/docs/ImportResultSchema.md +33 -0
- spatialflow/_generated/docs/IntegrationDetailSchema.md +44 -0
- spatialflow/_generated/docs/IntegrationResponseSchema.md +42 -0
- spatialflow/_generated/docs/IntegrationStatsSchema.md +36 -0
- spatialflow/_generated/docs/IntegrationTypeListResponse.md +34 -0
- spatialflow/_generated/docs/IntegrationTypeRequest.md +40 -0
- spatialflow/_generated/docs/IntegrationTypeResponse.md +45 -0
- spatialflow/_generated/docs/IntegrationsApi.md +2008 -0
- spatialflow/_generated/docs/InvoiceLineItem.md +33 -0
- spatialflow/_generated/docs/InvoiceListResponse.md +31 -0
- spatialflow/_generated/docs/InvoiceResponse.md +40 -0
- spatialflow/_generated/docs/LocationBatchIn.md +31 -0
- spatialflow/_generated/docs/LocationImportResponse.md +41 -0
- spatialflow/_generated/docs/LocationIngestResponse.md +35 -0
- spatialflow/_generated/docs/LocationPointIn.md +38 -0
- spatialflow/_generated/docs/LocationUpdateIn.md +36 -0
- spatialflow/_generated/docs/LocationUpdateOut.md +32 -0
- spatialflow/_generated/docs/LoginResponse.md +33 -0
- spatialflow/_generated/docs/LoginSchema.md +30 -0
- spatialflow/_generated/docs/MemberActionResponse.md +33 -0
- spatialflow/_generated/docs/MemberSummary.md +36 -0
- spatialflow/_generated/docs/MethodEnum.md +17 -0
- spatialflow/_generated/docs/OAuthAuthorizeResponse.md +29 -0
- spatialflow/_generated/docs/OAuthCallbackQuery.md +32 -0
- spatialflow/_generated/docs/OAuthLinkResponse.md +31 -0
- spatialflow/_generated/docs/OAuthProvidersResponse.md +29 -0
- spatialflow/_generated/docs/OnboardingProgressResponse.md +37 -0
- spatialflow/_generated/docs/PaymentMethodResponse.md +37 -0
- spatialflow/_generated/docs/PingResponse.md +31 -0
- spatialflow/_generated/docs/PlanChangePreviewResponse.md +35 -0
- spatialflow/_generated/docs/PlanFeatures.md +36 -0
- spatialflow/_generated/docs/PlanLimits.md +34 -0
- spatialflow/_generated/docs/PlanResponse.md +37 -0
- spatialflow/_generated/docs/PortalSessionRequest.md +30 -0
- spatialflow/_generated/docs/PortalSessionResponse.md +30 -0
- spatialflow/_generated/docs/PresignedUrlRequest.md +31 -0
- spatialflow/_generated/docs/PresignedUrlResponse.md +35 -0
- spatialflow/_generated/docs/PrivacyErasureRequest.md +35 -0
- spatialflow/_generated/docs/PrivacyErasureResponse.md +36 -0
- spatialflow/_generated/docs/PublicApi.md +389 -0
- spatialflow/_generated/docs/PublicLocationIngestApi.md +249 -0
- spatialflow/_generated/docs/RateLimitResponse.md +32 -0
- spatialflow/_generated/docs/RecentActivity.md +31 -0
- spatialflow/_generated/docs/RefreshTokenSchema.md +29 -0
- spatialflow/_generated/docs/RegisterSchema.md +37 -0
- spatialflow/_generated/docs/ResendVerificationSchema.md +29 -0
- spatialflow/_generated/docs/ResetPasswordSchema.md +31 -0
- spatialflow/_generated/docs/RetryPolicyResponseSchema.md +32 -0
- spatialflow/_generated/docs/RetryPolicySchema.md +35 -0
- spatialflow/_generated/docs/RetryStrategyEnum.md +14 -0
- spatialflow/_generated/docs/SeedDataResponseSchema.md +31 -0
- spatialflow/_generated/docs/SendEmailRequest.md +33 -0
- spatialflow/_generated/docs/SetupIntentResponse.md +30 -0
- spatialflow/_generated/docs/SignupRequest.md +40 -0
- spatialflow/_generated/docs/StartPlaybackRequest.md +30 -0
- spatialflow/_generated/docs/StorageApi.md +494 -0
- spatialflow/_generated/docs/SubscriptionActionResponse.md +32 -0
- spatialflow/_generated/docs/SubscriptionResponse.md +36 -0
- spatialflow/_generated/docs/SubscriptionsApi.md +677 -0
- spatialflow/_generated/docs/SuccessResponse.md +31 -0
- spatialflow/_generated/docs/SystemApi.md +137 -0
- spatialflow/_generated/docs/TemplateOut.md +35 -0
- spatialflow/_generated/docs/TestEventRequest.md +29 -0
- spatialflow/_generated/docs/TestIntegrationResponseSchema.md +31 -0
- spatialflow/_generated/docs/TestPointRequest.md +34 -0
- spatialflow/_generated/docs/TestPointResponse.md +33 -0
- spatialflow/_generated/docs/TestWebhookRequest.md +32 -0
- spatialflow/_generated/docs/TestWorkflowIn.md +29 -0
- spatialflow/_generated/docs/TileMetadata.md +36 -0
- spatialflow/_generated/docs/TilesApi.md +462 -0
- spatialflow/_generated/docs/UnsubscribeRequest.md +29 -0
- spatialflow/_generated/docs/UnsubscribeResponse.md +31 -0
- spatialflow/_generated/docs/UpdateGeofenceRequest.md +37 -0
- spatialflow/_generated/docs/UpdateIntegrationSchema.md +33 -0
- spatialflow/_generated/docs/UpdateMemberRoleRequest.md +30 -0
- spatialflow/_generated/docs/UpdateOnboardingProgressRequest.md +30 -0
- spatialflow/_generated/docs/UpdateProfileRequest.md +45 -0
- spatialflow/_generated/docs/UpdateUserWorkspaceRequest.md +31 -0
- spatialflow/_generated/docs/UpdateWebhookRequest.md +43 -0
- spatialflow/_generated/docs/UploadGeofencesRequest.md +31 -0
- spatialflow/_generated/docs/UploadJobStatus.md +42 -0
- spatialflow/_generated/docs/UsageMetrics.md +33 -0
- spatialflow/_generated/docs/UsageResponse.md +36 -0
- spatialflow/_generated/docs/UsageStats.md +32 -0
- spatialflow/_generated/docs/UserActionResponse.md +35 -0
- spatialflow/_generated/docs/UserApprovalRequest.md +30 -0
- spatialflow/_generated/docs/UserInviteRequest.md +33 -0
- spatialflow/_generated/docs/UserInviteResponse.md +36 -0
- spatialflow/_generated/docs/UserListResponse.md +34 -0
- spatialflow/_generated/docs/UserProfileResponse.md +55 -0
- spatialflow/_generated/docs/UserRejectionRequest.md +30 -0
- spatialflow/_generated/docs/UserResponse.md +44 -0
- spatialflow/_generated/docs/UserSummary.md +41 -0
- spatialflow/_generated/docs/UserUsageResponse.md +38 -0
- spatialflow/_generated/docs/UserWorkspaceResponse.md +34 -0
- spatialflow/_generated/docs/WebhookDeliveryDetailResponse.md +49 -0
- spatialflow/_generated/docs/WebhookDeliveryListResponse.md +31 -0
- spatialflow/_generated/docs/WebhookDeliveryResponse.md +46 -0
- spatialflow/_generated/docs/WebhookListResponse.md +31 -0
- spatialflow/_generated/docs/WebhookMetricsResponse.md +31 -0
- spatialflow/_generated/docs/WebhookResponse.md +50 -0
- spatialflow/_generated/docs/WebhookTestResponse.md +36 -0
- spatialflow/_generated/docs/WebhooksApi.md +1384 -0
- spatialflow/_generated/docs/WorkflowImportSchema.md +30 -0
- spatialflow/_generated/docs/WorkflowIn.md +32 -0
- spatialflow/_generated/docs/WorkflowListOut.md +39 -0
- spatialflow/_generated/docs/WorkflowListResponse.md +32 -0
- spatialflow/_generated/docs/WorkflowOut.md +42 -0
- spatialflow/_generated/docs/WorkflowRetryPolicyUpdateSchema.md +32 -0
- spatialflow/_generated/docs/WorkflowStepRetrySchema.md +33 -0
- spatialflow/_generated/docs/WorkflowUpdate.md +33 -0
- spatialflow/_generated/docs/WorkflowsApi.md +2599 -0
- spatialflow/_generated/docs/WorkspaceDeleteResponse.md +33 -0
- spatialflow/_generated/docs/WorkspaceDetail.md +37 -0
- spatialflow/_generated/docs/WorkspaceDetailResponse.md +41 -0
- spatialflow/_generated/docs/WorkspaceIn.md +34 -0
- spatialflow/_generated/docs/WorkspaceListItem.md +39 -0
- spatialflow/_generated/docs/WorkspaceListResponse.md +34 -0
- spatialflow/_generated/docs/WorkspaceMembersResponse.md +35 -0
- spatialflow/_generated/docs/WorkspaceOut.md +38 -0
- spatialflow/_generated/docs/WorkspaceSummary.md +32 -0
- spatialflow/_generated/docs/WorkspaceUpdateRequest.md +34 -0
- spatialflow/_generated/docs/WorkspaceUpdateResponse.md +33 -0
- spatialflow/_generated/docs/WorkspacesApi.md +241 -0
- spatialflow/_generated/git_push.sh +57 -0
- spatialflow/_generated/pyproject.toml +91 -0
- spatialflow/_generated/requirements.txt +6 -0
- spatialflow/_generated/setup.cfg +2 -0
- spatialflow/_generated/setup.py +51 -0
- spatialflow/_generated/spatialflow_generated/__init__.py +216 -0
- spatialflow/_generated/spatialflow_generated/api/__init__.py +24 -0
- spatialflow/_generated/spatialflow_generated/api/account_api.py +5675 -0
- spatialflow/_generated/spatialflow_generated/api/admin_api.py +6173 -0
- spatialflow/_generated/spatialflow_generated/api/authentication_api.py +4753 -0
- spatialflow/_generated/spatialflow_generated/api/billing_api.py +3151 -0
- spatialflow/_generated/spatialflow_generated/api/default_api.py +2157 -0
- spatialflow/_generated/spatialflow_generated/api/devices_api.py +3965 -0
- spatialflow/_generated/spatialflow_generated/api/e2_e_test_api.py +1049 -0
- spatialflow/_generated/spatialflow_generated/api/email_api.py +1879 -0
- spatialflow/_generated/spatialflow_generated/api/geofences_api.py +4899 -0
- spatialflow/_generated/spatialflow_generated/api/gpx_simulator_api.py +2824 -0
- spatialflow/_generated/spatialflow_generated/api/integrations_api.py +6952 -0
- spatialflow/_generated/spatialflow_generated/api/public_api.py +1506 -0
- spatialflow/_generated/spatialflow_generated/api/public_location_ingest_api.py +845 -0
- spatialflow/_generated/spatialflow_generated/api/storage_api.py +1642 -0
- spatialflow/_generated/spatialflow_generated/api/subscriptions_api.py +2356 -0
- spatialflow/_generated/spatialflow_generated/api/system_api.py +529 -0
- spatialflow/_generated/spatialflow_generated/api/tiles_api.py +1626 -0
- spatialflow/_generated/spatialflow_generated/api/webhooks_api.py +4579 -0
- spatialflow/_generated/spatialflow_generated/api/workflows_api.py +8334 -0
- spatialflow/_generated/spatialflow_generated/api/workspaces_api.py +813 -0
- spatialflow/_generated/spatialflow_generated/api_client.py +800 -0
- spatialflow/_generated/spatialflow_generated/api_response.py +21 -0
- spatialflow/_generated/spatialflow_generated/configuration.py +599 -0
- spatialflow/_generated/spatialflow_generated/exceptions.py +199 -0
- spatialflow/_generated/spatialflow_generated/models/__init__.py +180 -0
- spatialflow/_generated/spatialflow_generated/models/action_delivery_success_metrics.py +96 -0
- spatialflow/_generated/spatialflow_generated/models/action_response.py +96 -0
- spatialflow/_generated/spatialflow_generated/models/action_retry_config_schema.py +114 -0
- spatialflow/_generated/spatialflow_generated/models/activity_summary.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/api_key_create_request.py +95 -0
- spatialflow/_generated/spatialflow_generated/models/api_key_create_response.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/api_key_response.py +109 -0
- spatialflow/_generated/spatialflow_generated/models/api_key_update_request.py +114 -0
- spatialflow/_generated/spatialflow_generated/models/api_usage_stats.py +98 -0
- spatialflow/_generated/spatialflow_generated/models/async_upload_geofences_response.py +93 -0
- spatialflow/_generated/spatialflow_generated/models/auth_type_enum.py +39 -0
- spatialflow/_generated/spatialflow_generated/models/batch_location_update_in.py +97 -0
- spatialflow/_generated/spatialflow_generated/models/bulk_geofence_request.py +95 -0
- spatialflow/_generated/spatialflow_generated/models/change_password_schema.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/checkout_session_request.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/checkout_session_response.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/circuit_breaker_schema.py +94 -0
- spatialflow/_generated/spatialflow_generated/models/config_field_definition_request.py +134 -0
- spatialflow/_generated/spatialflow_generated/models/config_field_definition_response.py +138 -0
- spatialflow/_generated/spatialflow_generated/models/confirm_password_reset_schema.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/create_from_template_in.py +94 -0
- spatialflow/_generated/spatialflow_generated/models/create_geofence_request.py +125 -0
- spatialflow/_generated/spatialflow_generated/models/create_integration_schema.py +106 -0
- spatialflow/_generated/spatialflow_generated/models/create_user_schema.py +99 -0
- spatialflow/_generated/spatialflow_generated/models/create_webhook_request.py +136 -0
- spatialflow/_generated/spatialflow_generated/models/dashboard_comparison_metrics.py +96 -0
- spatialflow/_generated/spatialflow_generated/models/dashboard_metrics_response.py +108 -0
- spatialflow/_generated/spatialflow_generated/models/delete_file_response.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/delivery_status_enum.py +38 -0
- spatialflow/_generated/spatialflow_generated/models/device_in.py +98 -0
- spatialflow/_generated/spatialflow_generated/models/device_out.py +114 -0
- spatialflow/_generated/spatialflow_generated/models/email_health_response.py +92 -0
- spatialflow/_generated/spatialflow_generated/models/email_queue_stats.py +97 -0
- spatialflow/_generated/spatialflow_generated/models/email_stats.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/email_status_response.py +112 -0
- spatialflow/_generated/spatialflow_generated/models/error_response.py +101 -0
- spatialflow/_generated/spatialflow_generated/models/execution_out.py +126 -0
- spatialflow/_generated/spatialflow_generated/models/export_integration_schema.py +97 -0
- spatialflow/_generated/spatialflow_generated/models/file_list_response.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/forgot_password_schema.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/geo_json_point.py +97 -0
- spatialflow/_generated/spatialflow_generated/models/geofence_list_response.py +97 -0
- spatialflow/_generated/spatialflow_generated/models/geofence_response.py +139 -0
- spatialflow/_generated/spatialflow_generated/models/geofence_stats.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/geofence_test_result.py +98 -0
- spatialflow/_generated/spatialflow_generated/models/gpx_playback_out.py +128 -0
- spatialflow/_generated/spatialflow_generated/models/gpx_route_out.py +119 -0
- spatialflow/_generated/spatialflow_generated/models/health_check_response.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/health_response.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/import_integration_schema.py +100 -0
- spatialflow/_generated/spatialflow_generated/models/import_result_schema.py +103 -0
- spatialflow/_generated/spatialflow_generated/models/integration_detail_schema.py +125 -0
- spatialflow/_generated/spatialflow_generated/models/integration_response_schema.py +123 -0
- spatialflow/_generated/spatialflow_generated/models/integration_stats_schema.py +116 -0
- spatialflow/_generated/spatialflow_generated/models/integration_type_list_response.py +103 -0
- spatialflow/_generated/spatialflow_generated/models/integration_type_request.py +117 -0
- spatialflow/_generated/spatialflow_generated/models/integration_type_response.py +128 -0
- spatialflow/_generated/spatialflow_generated/models/invoice_line_item.py +93 -0
- spatialflow/_generated/spatialflow_generated/models/invoice_list_response.py +97 -0
- spatialflow/_generated/spatialflow_generated/models/invoice_response.py +120 -0
- spatialflow/_generated/spatialflow_generated/models/location_batch_in.py +102 -0
- spatialflow/_generated/spatialflow_generated/models/location_import_response.py +120 -0
- spatialflow/_generated/spatialflow_generated/models/location_ingest_response.py +117 -0
- spatialflow/_generated/spatialflow_generated/models/location_point_in.py +129 -0
- spatialflow/_generated/spatialflow_generated/models/location_update_in.py +132 -0
- spatialflow/_generated/spatialflow_generated/models/location_update_out.py +93 -0
- spatialflow/_generated/spatialflow_generated/models/login_response.py +95 -0
- spatialflow/_generated/spatialflow_generated/models/login_schema.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/member_action_response.py +98 -0
- spatialflow/_generated/spatialflow_generated/models/member_summary.py +114 -0
- spatialflow/_generated/spatialflow_generated/models/method_enum.py +39 -0
- spatialflow/_generated/spatialflow_generated/models/o_auth_authorize_response.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/o_auth_callback_query.py +103 -0
- spatialflow/_generated/spatialflow_generated/models/o_auth_link_response.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/o_auth_providers_response.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/onboarding_progress_response.py +112 -0
- spatialflow/_generated/spatialflow_generated/models/payment_method_response.py +116 -0
- spatialflow/_generated/spatialflow_generated/models/ping_response.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/plan_change_preview_response.py +104 -0
- spatialflow/_generated/spatialflow_generated/models/plan_features.py +109 -0
- spatialflow/_generated/spatialflow_generated/models/plan_limits.py +95 -0
- spatialflow/_generated/spatialflow_generated/models/plan_response.py +114 -0
- spatialflow/_generated/spatialflow_generated/models/portal_session_request.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/portal_session_response.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/presigned_url_request.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/presigned_url_response.py +99 -0
- spatialflow/_generated/spatialflow_generated/models/privacy_erasure_request.py +118 -0
- spatialflow/_generated/spatialflow_generated/models/privacy_erasure_response.py +110 -0
- spatialflow/_generated/spatialflow_generated/models/rate_limit_response.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/recent_activity.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/refresh_token_schema.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/register_schema.py +133 -0
- spatialflow/_generated/spatialflow_generated/models/resend_verification_schema.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/reset_password_schema.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/retry_policy_response_schema.py +95 -0
- spatialflow/_generated/spatialflow_generated/models/retry_policy_schema.py +99 -0
- spatialflow/_generated/spatialflow_generated/models/retry_strategy_enum.py +38 -0
- spatialflow/_generated/spatialflow_generated/models/seed_data_response_schema.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/send_email_request.py +100 -0
- spatialflow/_generated/spatialflow_generated/models/setup_intent_response.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/signup_request.py +164 -0
- spatialflow/_generated/spatialflow_generated/models/start_playback_request.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/subscription_action_response.py +100 -0
- spatialflow/_generated/spatialflow_generated/models/subscription_response.py +113 -0
- spatialflow/_generated/spatialflow_generated/models/success_response.py +94 -0
- spatialflow/_generated/spatialflow_generated/models/template_out.py +104 -0
- spatialflow/_generated/spatialflow_generated/models/test_event_request.py +92 -0
- spatialflow/_generated/spatialflow_generated/models/test_integration_response_schema.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/test_point_request.py +125 -0
- spatialflow/_generated/spatialflow_generated/models/test_point_response.py +101 -0
- spatialflow/_generated/spatialflow_generated/models/test_webhook_request.py +106 -0
- spatialflow/_generated/spatialflow_generated/models/test_workflow_in.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/tile_metadata.py +99 -0
- spatialflow/_generated/spatialflow_generated/models/unsubscribe_request.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/unsubscribe_response.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/update_geofence_request.py +142 -0
- spatialflow/_generated/spatialflow_generated/models/update_integration_schema.py +121 -0
- spatialflow/_generated/spatialflow_generated/models/update_member_role_request.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/update_onboarding_progress_request.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/update_profile_request.py +208 -0
- spatialflow/_generated/spatialflow_generated/models/update_user_workspace_request.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/update_webhook_request.py +186 -0
- spatialflow/_generated/spatialflow_generated/models/upload_geofences_request.py +95 -0
- spatialflow/_generated/spatialflow_generated/models/upload_job_status.py +137 -0
- spatialflow/_generated/spatialflow_generated/models/usage_metrics.py +93 -0
- spatialflow/_generated/spatialflow_generated/models/usage_response.py +105 -0
- spatialflow/_generated/spatialflow_generated/models/usage_stats.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/user_action_response.py +119 -0
- spatialflow/_generated/spatialflow_generated/models/user_approval_request.py +92 -0
- spatialflow/_generated/spatialflow_generated/models/user_invite_request.py +110 -0
- spatialflow/_generated/spatialflow_generated/models/user_invite_response.py +99 -0
- spatialflow/_generated/spatialflow_generated/models/user_list_response.py +110 -0
- spatialflow/_generated/spatialflow_generated/models/user_profile_response.py +158 -0
- spatialflow/_generated/spatialflow_generated/models/user_rejection_request.py +87 -0
- spatialflow/_generated/spatialflow_generated/models/user_response.py +142 -0
- spatialflow/_generated/spatialflow_generated/models/user_summary.py +140 -0
- spatialflow/_generated/spatialflow_generated/models/user_usage_response.py +152 -0
- spatialflow/_generated/spatialflow_generated/models/user_workspace_response.py +110 -0
- spatialflow/_generated/spatialflow_generated/models/webhook_delivery_detail_response.py +182 -0
- spatialflow/_generated/spatialflow_generated/models/webhook_delivery_list_response.py +97 -0
- spatialflow/_generated/spatialflow_generated/models/webhook_delivery_response.py +161 -0
- spatialflow/_generated/spatialflow_generated/models/webhook_list_response.py +97 -0
- spatialflow/_generated/spatialflow_generated/models/webhook_metrics_response.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/webhook_response.py +148 -0
- spatialflow/_generated/spatialflow_generated/models/webhook_test_response.py +114 -0
- spatialflow/_generated/spatialflow_generated/models/workflow_import_schema.py +89 -0
- spatialflow/_generated/spatialflow_generated/models/workflow_in.py +98 -0
- spatialflow/_generated/spatialflow_generated/models/workflow_list_out.py +118 -0
- spatialflow/_generated/spatialflow_generated/models/workflow_list_response.py +101 -0
- spatialflow/_generated/spatialflow_generated/models/workflow_out.py +129 -0
- spatialflow/_generated/spatialflow_generated/models/workflow_retry_policy_update_schema.py +109 -0
- spatialflow/_generated/spatialflow_generated/models/workflow_step_retry_schema.py +103 -0
- spatialflow/_generated/spatialflow_generated/models/workflow_update.py +120 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_delete_response.py +93 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_detail.py +116 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_detail_response.py +143 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_in.py +122 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_list_item.py +130 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_list_response.py +103 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_members_response.py +109 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_out.py +132 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_summary.py +91 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_update_request.py +120 -0
- spatialflow/_generated/spatialflow_generated/models/workspace_update_response.py +93 -0
- spatialflow/_generated/spatialflow_generated/py.typed +0 -0
- spatialflow/_generated/spatialflow_generated/rest.py +215 -0
- spatialflow/_generated/test-requirements.txt +6 -0
- spatialflow/_generated/tox.ini +9 -0
- spatialflow/client.py +137 -0
- spatialflow/exceptions.py +234 -0
- spatialflow/jobs.py +204 -0
- spatialflow/pagination.py +142 -0
- spatialflow/uploads.py +166 -0
- spatialflow/webhooks.py +126 -0
- spatialflow-0.1.0.dist-info/METADATA +249 -0
- spatialflow-0.1.0.dist-info/RECORD +404 -0
- spatialflow-0.1.0.dist-info/WHEEL +4 -0
spatialflow/jobs.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Async job polling helpers for SpatialFlow SDK.
|
|
3
|
+
|
|
4
|
+
Provides utilities for polling long-running jobs like file uploads.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from typing import Any, Callable, Dict, Optional, TypeVar
|
|
9
|
+
|
|
10
|
+
from .exceptions import SpatialFlowError, TimeoutError
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class JobTimeoutError(TimeoutError):
|
|
16
|
+
"""Raised when a job polling operation times out."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, job_id: str, timeout: float, last_status: Optional[str] = None):
|
|
19
|
+
self.job_id = job_id
|
|
20
|
+
self.timeout = timeout
|
|
21
|
+
self.last_status = last_status
|
|
22
|
+
message = f"Job {job_id} did not complete within {timeout} seconds"
|
|
23
|
+
if last_status:
|
|
24
|
+
message += f" (last status: {last_status})"
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JobFailedError(SpatialFlowError):
|
|
29
|
+
"""Raised when a polled job fails."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
job_id: str,
|
|
34
|
+
error_message: Optional[str] = None,
|
|
35
|
+
results: Optional[Dict[str, Any]] = None,
|
|
36
|
+
):
|
|
37
|
+
self.job_id = job_id
|
|
38
|
+
self.error_message = error_message
|
|
39
|
+
self.results = results
|
|
40
|
+
message = f"Job {job_id} failed"
|
|
41
|
+
if error_message:
|
|
42
|
+
message += f": {error_message}"
|
|
43
|
+
super().__init__(message)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class JobResult:
|
|
47
|
+
"""Result of a completed job."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
job_id: str,
|
|
52
|
+
status: str,
|
|
53
|
+
created_count: int = 0,
|
|
54
|
+
failed_count: int = 0,
|
|
55
|
+
total_features: int = 0,
|
|
56
|
+
results: Optional[Dict[str, Any]] = None,
|
|
57
|
+
duration: Optional[float] = None,
|
|
58
|
+
raw_response: Optional[Any] = None,
|
|
59
|
+
):
|
|
60
|
+
self.job_id = job_id
|
|
61
|
+
self.status = status
|
|
62
|
+
self.created_count = created_count
|
|
63
|
+
self.failed_count = failed_count
|
|
64
|
+
self.total_features = total_features
|
|
65
|
+
self.results = results or {}
|
|
66
|
+
self.duration = duration
|
|
67
|
+
self.raw_response = raw_response
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def created_geofences(self) -> list:
|
|
71
|
+
"""List of created geofence info (id, name)."""
|
|
72
|
+
return self.results.get("created_geofences", [])
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def errors(self) -> list:
|
|
76
|
+
"""List of errors that occurred during processing."""
|
|
77
|
+
return self.results.get("errors", [])
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def warnings(self) -> list:
|
|
81
|
+
"""List of warnings from processing."""
|
|
82
|
+
return self.results.get("warnings", [])
|
|
83
|
+
|
|
84
|
+
def __repr__(self) -> str:
|
|
85
|
+
return (
|
|
86
|
+
f"JobResult(job_id={self.job_id!r}, status={self.status!r}, "
|
|
87
|
+
f"created={self.created_count}, failed={self.failed_count})"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
async def poll_job(
|
|
92
|
+
fetch_status: Callable[[], Any],
|
|
93
|
+
*,
|
|
94
|
+
timeout: float = 300,
|
|
95
|
+
poll_interval: float = 2.0,
|
|
96
|
+
terminal_statuses: tuple = ("completed", "failed"),
|
|
97
|
+
on_status: Optional[Callable[[str, Any], None]] = None,
|
|
98
|
+
extract_job_id: Optional[Callable[[Any], str]] = None,
|
|
99
|
+
extract_status: Optional[Callable[[Any], str]] = None,
|
|
100
|
+
) -> JobResult:
|
|
101
|
+
"""
|
|
102
|
+
Poll a job until it reaches a terminal status.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
fetch_status: Async function that fetches the current job status.
|
|
106
|
+
Should return a response object with status information.
|
|
107
|
+
timeout: Maximum time to wait in seconds. Default: 300 (5 minutes).
|
|
108
|
+
poll_interval: Time between polls in seconds. Default: 2.0.
|
|
109
|
+
terminal_statuses: Tuple of statuses that indicate the job is done.
|
|
110
|
+
Default: ("completed", "failed").
|
|
111
|
+
on_status: Optional callback called on each poll with (status, response).
|
|
112
|
+
extract_job_id: Function to extract job_id from response.
|
|
113
|
+
Default: looks for job_id attribute or dict key.
|
|
114
|
+
extract_status: Function to extract status from response.
|
|
115
|
+
Default: looks for status attribute or dict key.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
JobResult with the final job state.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
JobTimeoutError: If the job doesn't complete within the timeout.
|
|
122
|
+
JobFailedError: If the job fails.
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
>>> async def get_status():
|
|
126
|
+
... return await client.geofences.apps_geofences_api_get_upload_job_status(
|
|
127
|
+
... job_id=job_id
|
|
128
|
+
... )
|
|
129
|
+
...
|
|
130
|
+
>>> result = await poll_job(get_status, timeout=120)
|
|
131
|
+
>>> print(f"Created {result.created_count} geofences")
|
|
132
|
+
"""
|
|
133
|
+
extract_job_id = extract_job_id or _default_extract_job_id
|
|
134
|
+
extract_status = extract_status or _default_extract_status
|
|
135
|
+
|
|
136
|
+
elapsed = 0.0
|
|
137
|
+
last_status: Optional[str] = None
|
|
138
|
+
last_response: Optional[Any] = None
|
|
139
|
+
|
|
140
|
+
while elapsed < timeout:
|
|
141
|
+
response = await fetch_status()
|
|
142
|
+
last_response = response
|
|
143
|
+
status = extract_status(response)
|
|
144
|
+
last_status = status
|
|
145
|
+
|
|
146
|
+
if on_status:
|
|
147
|
+
on_status(status, response)
|
|
148
|
+
|
|
149
|
+
if status in terminal_statuses:
|
|
150
|
+
job_id = extract_job_id(response)
|
|
151
|
+
return _build_job_result(job_id, status, response)
|
|
152
|
+
|
|
153
|
+
await asyncio.sleep(poll_interval)
|
|
154
|
+
elapsed += poll_interval
|
|
155
|
+
|
|
156
|
+
# Timeout
|
|
157
|
+
job_id = extract_job_id(last_response) if last_response else "unknown"
|
|
158
|
+
raise JobTimeoutError(job_id, timeout, last_status)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _default_extract_job_id(response: Any) -> str:
|
|
162
|
+
"""Extract job_id from response."""
|
|
163
|
+
if hasattr(response, "job_id"):
|
|
164
|
+
return str(response.job_id)
|
|
165
|
+
if isinstance(response, dict):
|
|
166
|
+
return str(response.get("job_id", "unknown"))
|
|
167
|
+
return "unknown"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _default_extract_status(response: Any) -> str:
|
|
171
|
+
"""Extract status from response."""
|
|
172
|
+
if hasattr(response, "status"):
|
|
173
|
+
return str(response.status)
|
|
174
|
+
if isinstance(response, dict):
|
|
175
|
+
return str(response.get("status", "unknown"))
|
|
176
|
+
return "unknown"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _build_job_result(job_id: str, status: str, response: Any) -> JobResult:
|
|
180
|
+
"""Build a JobResult from the response."""
|
|
181
|
+
# Handle both object attributes and dict access
|
|
182
|
+
def get_field(name: str, default: Any = None) -> Any:
|
|
183
|
+
if hasattr(response, name):
|
|
184
|
+
return getattr(response, name)
|
|
185
|
+
if isinstance(response, dict):
|
|
186
|
+
return response.get(name, default)
|
|
187
|
+
return default
|
|
188
|
+
|
|
189
|
+
# Check for failure
|
|
190
|
+
if status == "failed":
|
|
191
|
+
error_message = get_field("error_message")
|
|
192
|
+
results = get_field("results")
|
|
193
|
+
raise JobFailedError(job_id, error_message, results)
|
|
194
|
+
|
|
195
|
+
return JobResult(
|
|
196
|
+
job_id=job_id,
|
|
197
|
+
status=status,
|
|
198
|
+
created_count=get_field("created_count", 0),
|
|
199
|
+
failed_count=get_field("failed_count", 0),
|
|
200
|
+
total_features=get_field("total_features", 0),
|
|
201
|
+
results=get_field("results"),
|
|
202
|
+
duration=get_field("duration"),
|
|
203
|
+
raw_response=response,
|
|
204
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pagination helpers for SpatialFlow SDK.
|
|
3
|
+
|
|
4
|
+
Provides async iterators for paginated API responses.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TypeVar, Generic, AsyncIterator, Callable, Awaitable, Any, List
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PaginatedResponse(Generic[T]):
|
|
13
|
+
"""
|
|
14
|
+
A paginated response with helper methods for navigation.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
items: The items in the current page
|
|
18
|
+
count: Total number of items across all pages
|
|
19
|
+
next_url: URL for the next page (if any)
|
|
20
|
+
previous_url: URL for the previous page (if any)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
items: List[T],
|
|
26
|
+
count: int,
|
|
27
|
+
next_url: str | None = None,
|
|
28
|
+
previous_url: str | None = None,
|
|
29
|
+
):
|
|
30
|
+
self.items = items
|
|
31
|
+
self.count = count
|
|
32
|
+
self.next_url = next_url
|
|
33
|
+
self.previous_url = previous_url
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def has_more(self) -> bool:
|
|
37
|
+
"""Returns True if there are more pages available."""
|
|
38
|
+
return self.next_url is not None
|
|
39
|
+
|
|
40
|
+
def __iter__(self):
|
|
41
|
+
"""Iterate over items in the current page."""
|
|
42
|
+
return iter(self.items)
|
|
43
|
+
|
|
44
|
+
def __len__(self) -> int:
|
|
45
|
+
"""Number of items in the current page."""
|
|
46
|
+
return len(self.items)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AsyncPaginator(Generic[T]):
|
|
50
|
+
"""
|
|
51
|
+
Async iterator for paginated API responses.
|
|
52
|
+
|
|
53
|
+
Automatically fetches subsequent pages as you iterate.
|
|
54
|
+
|
|
55
|
+
Example:
|
|
56
|
+
>>> async for geofence in client.geofences.list_all():
|
|
57
|
+
... print(geofence.name)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
fetch_page: Callable[[int, int], Awaitable[Any]],
|
|
63
|
+
extract_items: Callable[[Any], List[T]],
|
|
64
|
+
extract_count: Callable[[Any], int],
|
|
65
|
+
extract_next: Callable[[Any], str | None],
|
|
66
|
+
limit: int = 100,
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Initialize the paginator.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
fetch_page: Async function that fetches a page (offset, limit) -> response
|
|
73
|
+
extract_items: Function to extract items list from response
|
|
74
|
+
extract_count: Function to extract total count from response
|
|
75
|
+
extract_next: Function to extract next page URL from response
|
|
76
|
+
limit: Number of items per page
|
|
77
|
+
"""
|
|
78
|
+
self._fetch_page = fetch_page
|
|
79
|
+
self._extract_items = extract_items
|
|
80
|
+
self._extract_count = extract_count
|
|
81
|
+
self._extract_next = extract_next
|
|
82
|
+
self._limit = limit
|
|
83
|
+
self._offset = 0
|
|
84
|
+
self._exhausted = False
|
|
85
|
+
self._total_count: int | None = None
|
|
86
|
+
|
|
87
|
+
async def __aiter__(self) -> AsyncIterator[T]:
|
|
88
|
+
"""Async iterate over all items across all pages."""
|
|
89
|
+
while not self._exhausted:
|
|
90
|
+
response = await self._fetch_page(self._offset, self._limit)
|
|
91
|
+
items = self._extract_items(response)
|
|
92
|
+
self._total_count = self._extract_count(response)
|
|
93
|
+
next_url = self._extract_next(response)
|
|
94
|
+
|
|
95
|
+
for item in items:
|
|
96
|
+
yield item
|
|
97
|
+
|
|
98
|
+
if next_url is None or len(items) < self._limit:
|
|
99
|
+
self._exhausted = True
|
|
100
|
+
else:
|
|
101
|
+
self._offset += self._limit
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def total_count(self) -> int | None:
|
|
105
|
+
"""
|
|
106
|
+
Total count of items (available after first page is fetched).
|
|
107
|
+
"""
|
|
108
|
+
return self._total_count
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def paginate(
|
|
112
|
+
fetch_page: Callable[[int, int], Awaitable[Any]],
|
|
113
|
+
limit: int = 100,
|
|
114
|
+
) -> AsyncPaginator:
|
|
115
|
+
"""
|
|
116
|
+
Create a paginator for a list endpoint.
|
|
117
|
+
|
|
118
|
+
This is a convenience function for the common case where the response
|
|
119
|
+
has 'results', 'count', and 'next' fields.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
fetch_page: Async function that takes (offset, limit) and returns response
|
|
123
|
+
limit: Number of items per page (default 100)
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
AsyncPaginator that yields items from all pages
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
>>> async def fetch(offset, limit):
|
|
130
|
+
... return await client.geofences.apps_geofences_api_list_geofences(
|
|
131
|
+
... offset=offset, limit=limit
|
|
132
|
+
... )
|
|
133
|
+
>>> async for geofence in paginate(fetch):
|
|
134
|
+
... print(geofence.name)
|
|
135
|
+
"""
|
|
136
|
+
return AsyncPaginator(
|
|
137
|
+
fetch_page=fetch_page,
|
|
138
|
+
extract_items=lambda r: r.results if hasattr(r, "results") else [],
|
|
139
|
+
extract_count=lambda r: r.count if hasattr(r, "count") else 0,
|
|
140
|
+
extract_next=lambda r: r.next if hasattr(r, "next") else None,
|
|
141
|
+
limit=limit,
|
|
142
|
+
)
|
spatialflow/uploads.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File upload helpers for SpatialFlow SDK.
|
|
3
|
+
|
|
4
|
+
Provides utilities for uploading files (GeoJSON, KML, GPX) and processing them.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Callable, Optional, Union
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
|
|
13
|
+
from .jobs import JobResult, poll_job
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def upload_geofences(
|
|
17
|
+
client: Any,
|
|
18
|
+
file_path: Union[str, Path],
|
|
19
|
+
*,
|
|
20
|
+
group_name: Optional[str] = None,
|
|
21
|
+
timeout: float = 300,
|
|
22
|
+
poll_interval: float = 2.0,
|
|
23
|
+
on_status: Optional[Callable[[str, Any], None]] = None,
|
|
24
|
+
) -> JobResult:
|
|
25
|
+
"""
|
|
26
|
+
Upload a geofence file and wait for processing to complete.
|
|
27
|
+
|
|
28
|
+
This is a convenience method that:
|
|
29
|
+
1. Requests a presigned upload URL
|
|
30
|
+
2. Uploads the file to S3
|
|
31
|
+
3. Starts the geofence import job
|
|
32
|
+
4. Polls until completion
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
client: SpatialFlow client instance.
|
|
36
|
+
file_path: Path to the file to upload (GeoJSON, KML, or GPX).
|
|
37
|
+
group_name: Optional name for the geofence group.
|
|
38
|
+
timeout: Maximum time to wait for processing in seconds. Default: 300.
|
|
39
|
+
poll_interval: Time between status polls in seconds. Default: 2.0.
|
|
40
|
+
on_status: Optional callback called on each poll with (status, response).
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
JobResult with the final job state including created geofences.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
FileNotFoundError: If the file doesn't exist.
|
|
47
|
+
ValueError: If the file type is not supported.
|
|
48
|
+
JobTimeoutError: If processing doesn't complete within timeout.
|
|
49
|
+
JobFailedError: If the import job fails.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
>>> result = await upload_geofences(
|
|
53
|
+
... client,
|
|
54
|
+
... "boundaries.geojson",
|
|
55
|
+
... group_name="my-region",
|
|
56
|
+
... timeout=120,
|
|
57
|
+
... )
|
|
58
|
+
>>> print(f"Created {result.created_count} geofences")
|
|
59
|
+
"""
|
|
60
|
+
file_path = Path(file_path)
|
|
61
|
+
|
|
62
|
+
if not file_path.exists():
|
|
63
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
64
|
+
|
|
65
|
+
# Get file info
|
|
66
|
+
filename = file_path.name
|
|
67
|
+
file_size = file_path.stat().st_size
|
|
68
|
+
|
|
69
|
+
# Determine content type
|
|
70
|
+
ext = file_path.suffix.lower()
|
|
71
|
+
content_types = {
|
|
72
|
+
".geojson": "application/geo+json",
|
|
73
|
+
".json": "application/json",
|
|
74
|
+
".kml": "application/vnd.google-earth.kml+xml",
|
|
75
|
+
".gpx": "application/gpx+xml",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ext not in content_types:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"Unsupported file type: {ext}. "
|
|
81
|
+
f"Supported types: {', '.join(content_types.keys())}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
content_type = content_types[ext]
|
|
85
|
+
|
|
86
|
+
# Step 1: Get presigned upload URL
|
|
87
|
+
presigned_response = await client.storage.apps_storage_api_create_presigned_url(
|
|
88
|
+
presigned_url_request={
|
|
89
|
+
"file_type": "geofences",
|
|
90
|
+
"filename": filename,
|
|
91
|
+
"file_size": file_size,
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Extract from response (handle both direct and axios-style responses)
|
|
96
|
+
presigned_data = _extract_data(presigned_response)
|
|
97
|
+
upload_url = presigned_data.get("upload_url")
|
|
98
|
+
file_id = presigned_data.get("file_id")
|
|
99
|
+
|
|
100
|
+
if not upload_url:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
"Failed to get presigned upload URL from storage API. "
|
|
103
|
+
"Response missing 'upload_url' field."
|
|
104
|
+
)
|
|
105
|
+
if not file_id:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
"Failed to get file ID from storage API. "
|
|
108
|
+
"Response missing 'file_id' field."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Step 2: Upload file to S3
|
|
112
|
+
# Note: For large files, consider using aiohttp's streaming upload.
|
|
113
|
+
# This implementation reads the entire file into memory for simplicity.
|
|
114
|
+
async with aiohttp.ClientSession() as session:
|
|
115
|
+
with open(file_path, "rb") as f:
|
|
116
|
+
file_content = f.read()
|
|
117
|
+
|
|
118
|
+
async with session.put(
|
|
119
|
+
upload_url,
|
|
120
|
+
data=file_content,
|
|
121
|
+
headers={"Content-Type": content_type},
|
|
122
|
+
) as resp:
|
|
123
|
+
if resp.status not in (200, 204):
|
|
124
|
+
text = await resp.text()
|
|
125
|
+
raise ValueError(f"Failed to upload file to S3: {resp.status} - {text}")
|
|
126
|
+
|
|
127
|
+
# Step 3: Start the geofence import job
|
|
128
|
+
upload_request = {"file_id": file_id}
|
|
129
|
+
if group_name:
|
|
130
|
+
upload_request["group_name"] = group_name
|
|
131
|
+
|
|
132
|
+
job_response = await client.geofences.apps_geofences_api_upload_geofences_async(
|
|
133
|
+
upload_geofences_request=upload_request
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
job_data = _extract_data(job_response)
|
|
137
|
+
job_id = job_data.get("job_id")
|
|
138
|
+
|
|
139
|
+
if not job_id:
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"Failed to start upload job. Response missing 'job_id' field."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Step 4: Poll for completion
|
|
145
|
+
async def fetch_status():
|
|
146
|
+
return await client.geofences.apps_geofences_api_get_upload_job_status(
|
|
147
|
+
job_id=job_id
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return await poll_job(
|
|
151
|
+
fetch_status,
|
|
152
|
+
timeout=timeout,
|
|
153
|
+
poll_interval=poll_interval,
|
|
154
|
+
on_status=on_status,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _extract_data(response: Any) -> dict:
|
|
159
|
+
"""Extract data from response, handling axios-style responses."""
|
|
160
|
+
if hasattr(response, "data"):
|
|
161
|
+
return response.data if isinstance(response.data, dict) else response.data.__dict__
|
|
162
|
+
if isinstance(response, dict):
|
|
163
|
+
return response
|
|
164
|
+
if hasattr(response, "__dict__"):
|
|
165
|
+
return response.__dict__
|
|
166
|
+
return {}
|
spatialflow/webhooks.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Webhook signature verification for SpatialFlow SDK.
|
|
3
|
+
|
|
4
|
+
Provides HMAC-SHA256 signature verification for webhook payloads.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import hashlib
|
|
8
|
+
import hmac
|
|
9
|
+
import time
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
from .exceptions import SpatialFlowError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WebhookSignatureError(SpatialFlowError):
|
|
16
|
+
"""Raised when webhook signature verification fails."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def verify_webhook_signature(
|
|
22
|
+
payload: Union[str, bytes],
|
|
23
|
+
signature: str,
|
|
24
|
+
secret: str,
|
|
25
|
+
tolerance: int = 300,
|
|
26
|
+
) -> dict:
|
|
27
|
+
"""
|
|
28
|
+
Verify a webhook signature and return the parsed payload.
|
|
29
|
+
|
|
30
|
+
SpatialFlow webhooks include an HMAC-SHA256 signature in the
|
|
31
|
+
`X-SF-Signature` header. This function verifies that signature
|
|
32
|
+
and optionally checks the timestamp to prevent replay attacks.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
payload: The raw webhook payload (request body as string or bytes)
|
|
36
|
+
signature: The signature from X-SF-Signature header
|
|
37
|
+
secret: Your webhook signing secret
|
|
38
|
+
tolerance: Maximum age of the webhook in seconds (default 300 = 5 minutes).
|
|
39
|
+
Set to 0 to disable timestamp checking.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The parsed webhook payload as a dict
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
WebhookSignatureError: If signature is invalid or timestamp is too old
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> from spatialflow import verify_webhook_signature
|
|
49
|
+
>>>
|
|
50
|
+
>>> # In your webhook handler (e.g., Flask/FastAPI)
|
|
51
|
+
>>> @app.post("/webhook")
|
|
52
|
+
>>> async def handle_webhook(request):
|
|
53
|
+
... payload = await request.body()
|
|
54
|
+
... signature = request.headers.get("X-SF-Signature")
|
|
55
|
+
...
|
|
56
|
+
... try:
|
|
57
|
+
... event = verify_webhook_signature(
|
|
58
|
+
... payload=payload,
|
|
59
|
+
... signature=signature,
|
|
60
|
+
... secret=WEBHOOK_SECRET,
|
|
61
|
+
... )
|
|
62
|
+
... # Process the verified event
|
|
63
|
+
... print(f"Event type: {event['type']}")
|
|
64
|
+
... return {"status": "ok"}
|
|
65
|
+
... except WebhookSignatureError as e:
|
|
66
|
+
... return {"error": str(e)}, 400
|
|
67
|
+
"""
|
|
68
|
+
import json
|
|
69
|
+
|
|
70
|
+
# Normalize payload to bytes
|
|
71
|
+
if isinstance(payload, str):
|
|
72
|
+
payload_bytes = payload.encode("utf-8")
|
|
73
|
+
else:
|
|
74
|
+
payload_bytes = payload
|
|
75
|
+
|
|
76
|
+
# Parse the signature header
|
|
77
|
+
# Format: t=<timestamp>,v1=<signature>
|
|
78
|
+
try:
|
|
79
|
+
parts = dict(part.split("=", 1) for part in signature.split(","))
|
|
80
|
+
timestamp_str = parts.get("t")
|
|
81
|
+
sig_hash = parts.get("v1")
|
|
82
|
+
|
|
83
|
+
if not timestamp_str or not sig_hash:
|
|
84
|
+
raise WebhookSignatureError(
|
|
85
|
+
"Invalid signature format. Expected: t=<timestamp>,v1=<signature>"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
timestamp = int(timestamp_str)
|
|
89
|
+
except (ValueError, AttributeError) as e:
|
|
90
|
+
raise WebhookSignatureError(f"Failed to parse signature header: {e}")
|
|
91
|
+
|
|
92
|
+
# Check timestamp tolerance (replay attack prevention)
|
|
93
|
+
if tolerance > 0:
|
|
94
|
+
now = int(time.time())
|
|
95
|
+
age = now - timestamp
|
|
96
|
+
if age > tolerance:
|
|
97
|
+
raise WebhookSignatureError(
|
|
98
|
+
f"Webhook timestamp too old: {age}s (tolerance: {tolerance}s)"
|
|
99
|
+
)
|
|
100
|
+
if age < -tolerance:
|
|
101
|
+
raise WebhookSignatureError(
|
|
102
|
+
f"Webhook timestamp in future: {-age}s (tolerance: {tolerance}s)"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Compute expected signature
|
|
106
|
+
# The signed payload is: timestamp.payload
|
|
107
|
+
signed_payload = f"{timestamp}.".encode("utf-8") + payload_bytes
|
|
108
|
+
expected_sig = hmac.new(
|
|
109
|
+
secret.encode("utf-8"),
|
|
110
|
+
signed_payload,
|
|
111
|
+
hashlib.sha256,
|
|
112
|
+
).hexdigest()
|
|
113
|
+
|
|
114
|
+
# Constant-time comparison to prevent timing attacks
|
|
115
|
+
if not hmac.compare_digest(expected_sig, sig_hash):
|
|
116
|
+
raise WebhookSignatureError("Signature verification failed")
|
|
117
|
+
|
|
118
|
+
# Parse and return the payload
|
|
119
|
+
try:
|
|
120
|
+
return json.loads(payload_bytes.decode("utf-8"))
|
|
121
|
+
except json.JSONDecodeError as e:
|
|
122
|
+
raise WebhookSignatureError(f"Failed to parse webhook payload as JSON: {e}")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Convenience alias
|
|
126
|
+
verify_signature = verify_webhook_signature
|