monkeybrain-runtime 1.0.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.
- monkeybrain_runtime-1.0.0.dist-info/METADATA +76 -0
- monkeybrain_runtime-1.0.0.dist-info/RECORD +838 -0
- monkeybrain_runtime-1.0.0.dist-info/WHEEL +5 -0
- monkeybrain_runtime-1.0.0.dist-info/entry_points.txt +3 -0
- monkeybrain_runtime-1.0.0.dist-info/top_level.txt +2 -0
- services/__init__.py +8 -0
- services/agentos/__init__.py +0 -0
- services/agentos/main.py +1 -0
- services/assets/helpers/__init__.py +12 -0
- services/assets/helpers/device.py +59 -0
- services/assets/helpers/equipment.py +179 -0
- services/assets/helpers/instruments.py +72 -0
- services/assets/helpers/machines.py +183 -0
- services/assets/helpers/materials.py +76 -0
- services/assets/helpers/parts.py +116 -0
- services/assets/helpers/plc.py +134 -0
- services/assets/helpers/tags.py +108 -0
- services/assets/helpers/tools.py +101 -0
- services/assets/main.py +75 -0
- services/assets/models/__init__.py +12 -0
- services/assets/models/device.py +79 -0
- services/assets/models/equipment.py +222 -0
- services/assets/models/instruments.py +85 -0
- services/assets/models/machines.py +230 -0
- services/assets/models/material.py +266 -0
- services/assets/models/parts.py +96 -0
- services/assets/models/plc.py +264 -0
- services/assets/models/tags.py +76 -0
- services/assets/models/tools.py +179 -0
- services/assets/routers/__init__.py +12 -0
- services/assets/routers/classes.py +65 -0
- services/assets/routers/device.py +86 -0
- services/assets/routers/equipment.py +145 -0
- services/assets/routers/families.py +61 -0
- services/assets/routers/instruments.py +70 -0
- services/assets/routers/machines.py +136 -0
- services/assets/routers/materials.py +105 -0
- services/assets/routers/parts.py +130 -0
- services/assets/routers/plc.py +94 -0
- services/assets/routers/subclasses.py +68 -0
- services/assets/routers/tags.py +138 -0
- services/assets/routers/tools.py +113 -0
- services/auth/helpers/__init__.py +13 -0
- services/auth/helpers/approval_decisions.py +261 -0
- services/auth/helpers/audit_elasticsearch_sync.py +350 -0
- services/auth/helpers/departments.py +53 -0
- services/auth/helpers/graph_store.py +848 -0
- services/auth/helpers/influx_store.py +280 -0
- services/auth/helpers/me.py +33 -0
- services/auth/helpers/nats_consumer.py +618 -0
- services/auth/helpers/nats_store.py +242 -0
- services/auth/helpers/permissions.py +62 -0
- services/auth/helpers/roles.py +87 -0
- services/auth/helpers/store.py +54 -0
- services/auth/helpers/team_members.py +155 -0
- services/auth/helpers/teams.py +87 -0
- services/auth/helpers/tokens.py +71 -0
- services/auth/helpers/users.py +119 -0
- services/auth/helpers/websocket_broadcast.py +41 -0
- services/auth/main.py +88 -0
- services/auth/models/__init__.py +12 -0
- services/auth/models/departments.py +55 -0
- services/auth/models/login.py +20 -0
- services/auth/models/permissions.py +61 -0
- services/auth/models/roles.py +53 -0
- services/auth/models/session.py +26 -0
- services/auth/models/teamMembers.py +59 -0
- services/auth/models/teams.py +56 -0
- services/auth/models/users.py +77 -0
- services/auth/routers/__init__.py +14 -0
- services/auth/routers/auth.py +3839 -0
- services/auth/routers/departments.py +84 -0
- services/auth/routers/integration_config.py +703 -0
- services/auth/routers/me.py +28 -0
- services/auth/routers/permissions.py +96 -0
- services/auth/routers/roles.py +139 -0
- services/auth/routers/session.py +224 -0
- services/auth/routers/team_members.py +152 -0
- services/auth/routers/teams.py +112 -0
- services/auth/routers/users.py +131 -0
- services/auth/routers/websocket_events.py +247 -0
- services/batch_execution/__init__.py +19 -0
- services/batch_execution/pipeline_triggers/__init__.py +53 -0
- services/batch_execution/pipeline_triggers/pipeline_parameter_adjuster.py +440 -0
- services/batch_execution/pipeline_triggers/pipeline_trigger_engine.py +445 -0
- services/batch_execution/pipeline_triggers/re_evaluation_queue.py +341 -0
- services/batch_execution/pipeline_triggers/test_phase3_implementation.py +553 -0
- services/batch_execution/pipeline_triggers/workflow_reevaluator.py +367 -0
- services/batch_execution/smoke_test_e2e_feedback_loop.py +704 -0
- services/batch_execution/workflow_executor.py +478 -0
- services/changeover/helpers/__init__.py +11 -0
- services/changeover/helpers/changeover.py +87 -0
- services/changeover/helpers/changeover_common.py +55 -0
- services/changeover/helpers/changeover_events.py +66 -0
- services/changeover/helpers/changeover_kpis.py +33 -0
- services/changeover/helpers/changeover_matrix.py +60 -0
- services/changeover/helpers/changeover_procedures.py +164 -0
- services/changeover/helpers/changeover_windows.py +96 -0
- services/changeover/main.py +52 -0
- services/changeover/models/__init__.py +11 -0
- services/changeover/models/changeover.py +73 -0
- services/changeover/models/changeover_events.py +142 -0
- services/changeover/models/changeover_kpis.py +75 -0
- services/changeover/models/changeover_matrix.py +63 -0
- services/changeover/models/changeover_procedures.py +108 -0
- services/changeover/models/changeover_tasks.py +87 -0
- services/changeover/models/changeover_windows.py +72 -0
- services/changeover/routers/__init__.py +9 -0
- services/changeover/routers/changeover_events.py +127 -0
- services/changeover/routers/changeover_kpis.py +80 -0
- services/changeover/routers/changeover_matrix.py +80 -0
- services/changeover/routers/changeover_procedures.py +118 -0
- services/changeover/routers/changeover_windows.py +98 -0
- services/common/__init__.py +2 -0
- services/common/approval_chains.py +648 -0
- services/common/auth.py +56 -0
- services/common/cdc.py +52 -0
- services/common/compat.py +217 -0
- services/common/compliance.py +562 -0
- services/common/config.py +134 -0
- services/common/cors.py +17 -0
- services/common/data_transformation.py +195 -0
- services/common/db.py +577 -0
- services/common/embeddings.py +97 -0
- services/common/event_reducers.py +194 -0
- services/common/event_types.py +51 -0
- services/common/integration_ingestion.py +169 -0
- services/common/logging.py +204 -0
- services/common/models/__init__.py +2 -0
- services/common/models/databricks.py +25 -0
- services/common/models/enums.py +64 -0
- services/common/module_control.py +422 -0
- services/common/mongo_cdc_watcher.py +106 -0
- services/common/n8n_auth.py +22 -0
- services/common/neo4j_mirror.py +1087 -0
- services/common/ontology_registry.py +110 -0
- services/common/reasoning_traces.py +52 -0
- services/common/supply_chain_cdc.py +555 -0
- services/common/tracing.py +159 -0
- services/common/utils.py +30 -0
- services/customers/helpers/__init__.py +8 -0
- services/customers/helpers/customer_details.py +64 -0
- services/customers/helpers/customer_metadata.py +64 -0
- services/customers/helpers/customer_order_metrics.py +67 -0
- services/customers/helpers/customer_payment_data.py +67 -0
- services/customers/main.py +50 -0
- services/customers/models/__init__.py +8 -0
- services/customers/models/customer_details.py +42 -0
- services/customers/models/customer_metadata.py +97 -0
- services/customers/models/customer_order_metrics.py +86 -0
- services/customers/models/customer_payment_data.py +60 -0
- services/customers/routers/__init__.py +8 -0
- services/customers/routers/customer_details.py +88 -0
- services/customers/routers/customer_metadata.py +88 -0
- services/customers/routers/customer_order_metrics.py +88 -0
- services/customers/routers/customer_payment_data.py +88 -0
- services/documents/__init__.py +1 -0
- services/documents/helpers/__init__.py +6 -0
- services/documents/helpers/document_metadata.py +569 -0
- services/documents/helpers/document_workflows.py +215 -0
- services/documents/helpers/report_templates.py +113 -0
- services/documents/main.py +49 -0
- services/documents/models/__init__.py +6 -0
- services/documents/models/document_metadata.py +215 -0
- services/documents/models/document_workflows.py +136 -0
- services/documents/models/report_templates.py +132 -0
- services/documents/routers/__init__.py +6 -0
- services/documents/routers/document_metadata.py +654 -0
- services/documents/routers/document_workflows.py +146 -0
- services/documents/routers/report_templates.py +86 -0
- services/events/helpers/__init__.py +5 -0
- services/events/helpers/events.py +394 -0
- services/events/main.py +40 -0
- services/events/models/__init__.py +5 -0
- services/events/models/events.py +50 -0
- services/events/routers/__init__.py +6 -0
- services/events/routers/count_events.py +109 -0
- services/events/routers/events.py +75 -0
- services/events/seed_events.py +196 -0
- services/facilities/helpers/__init__.py +8 -0
- services/facilities/helpers/lines.py +74 -0
- services/facilities/helpers/locations.py +231 -0
- services/facilities/helpers/plants.py +59 -0
- services/facilities/helpers/stages.py +110 -0
- services/facilities/helpers/workstation.py +213 -0
- services/facilities/main.py +60 -0
- services/facilities/models/__init__.py +10 -0
- services/facilities/models/industrialLine.py +72 -0
- services/facilities/models/industrialPlant.py +164 -0
- services/facilities/models/locations.py +74 -0
- services/facilities/models/stages.py +92 -0
- services/facilities/models/worker.py +73 -0
- services/facilities/models/workstation.py +117 -0
- services/facilities/models/workstation_live_state.py +59 -0
- services/facilities/routers/__init__.py +8 -0
- services/facilities/routers/bays.py +81 -0
- services/facilities/routers/buildings.py +92 -0
- services/facilities/routers/floors.py +81 -0
- services/facilities/routers/lines.py +154 -0
- services/facilities/routers/locations.py +208 -0
- services/facilities/routers/plant.py +203 -0
- services/facilities/routers/rooms.py +81 -0
- services/facilities/routers/stages.py +152 -0
- services/facilities/routers/workstation.py +173 -0
- services/file/backup.py +71 -0
- services/file/main.py +45 -0
- services/file/recieve.py +54 -0
- services/file/send.py +55 -0
- services/file/src/core/config.py +90 -0
- services/file/src/core/keycloak.py +152 -0
- services/file/src/core/logging_config.py +9 -0
- services/file/src/core/security.py +33 -0
- services/file/src/helpers/cad_conversion.py +331 -0
- services/file/src/helpers/helpers.py +825 -0
- services/file/src/routes/cad_conversion.py +26 -0
- services/file/src/routes/files.py +136 -0
- services/file/src/routes/presigned.py +154 -0
- services/file/src/services/websocket.py +293 -0
- services/floor_layout/helpers/__init__.py +8 -0
- services/floor_layout/helpers/bays.py +92 -0
- services/floor_layout/helpers/buildings.py +54 -0
- services/floor_layout/helpers/floors.py +65 -0
- services/floor_layout/helpers/rooms.py +76 -0
- services/floor_layout/main.py +52 -0
- services/floor_layout/models/__init__.py +8 -0
- services/floor_layout/models/bays.py +65 -0
- services/floor_layout/models/buildings.py +52 -0
- services/floor_layout/models/floors.py +45 -0
- services/floor_layout/models/rooms.py +61 -0
- services/floor_layout/routers/__init__.py +9 -0
- services/floor_layout/routers/bays.py +143 -0
- services/floor_layout/routers/buildings.py +116 -0
- services/floor_layout/routers/floors.py +89 -0
- services/floor_layout/routers/locations.py +80 -0
- services/floor_layout/routers/rooms.py +134 -0
- services/inventory/helpers/__init__.py +13 -0
- services/inventory/helpers/cycle_counts.py +124 -0
- services/inventory/helpers/inventory_allocations.py +134 -0
- services/inventory/helpers/inventory_item_counts.py +114 -0
- services/inventory/helpers/inventory_item_quantities.py +114 -0
- services/inventory/helpers/inventory_items.py +103 -0
- services/inventory/helpers/inventory_stage_outputs.py +134 -0
- services/inventory/helpers/inventory_transactions.py +112 -0
- services/inventory/helpers/stock_adjustment_requests.py +101 -0
- services/inventory/helpers/warehouse_cycle_counts.py +133 -0
- services/inventory/helpers/warehouse_locations.py +213 -0
- services/inventory/helpers/warehouse_regulated_records.py +123 -0
- services/inventory/main.py +62 -0
- services/inventory/models/__init__.py +17 -0
- services/inventory/models/cycle_counts.py +99 -0
- services/inventory/models/inventory_allocations.py +121 -0
- services/inventory/models/inventory_common.py +65 -0
- services/inventory/models/inventory_enums.py +21 -0
- services/inventory/models/inventory_item_count.py +65 -0
- services/inventory/models/inventory_item_quantity.py +82 -0
- services/inventory/models/inventory_items.py +168 -0
- services/inventory/models/inventory_responses.py +44 -0
- services/inventory/models/inventory_stage_outputs.py +96 -0
- services/inventory/models/inventory_state.py +15 -0
- services/inventory/models/inventory_transactions.py +80 -0
- services/inventory/models/stock_adjustment_requests.py +109 -0
- services/inventory/models/warehouse_cycle_counts.py +119 -0
- services/inventory/models/warehouse_location_models.py +708 -0
- services/inventory/models/warehouse_regulated_records.py +358 -0
- services/inventory/routers/__init__.py +13 -0
- services/inventory/routers/cycle_counts.py +106 -0
- services/inventory/routers/inventory_allocations.py +125 -0
- services/inventory/routers/inventory_item_counts.py +105 -0
- services/inventory/routers/inventory_item_quantities.py +105 -0
- services/inventory/routers/inventory_items.py +109 -0
- services/inventory/routers/inventory_stage_outputs.py +122 -0
- services/inventory/routers/inventory_transactions.py +96 -0
- services/inventory/routers/stock_adjustment_requests.py +124 -0
- services/inventory/routers/warehouse_cycle_counts.py +124 -0
- services/inventory/routers/warehouse_locations.py +426 -0
- services/inventory/routers/warehouse_regulated_records.py +273 -0
- services/iot/helpers/__init__.py +8 -0
- services/iot/helpers/ble_device.py +87 -0
- services/iot/helpers/mqtt_bridge.py +115 -0
- services/iot/helpers/sensor_readings.py +63 -0
- services/iot/helpers/sensors.py +77 -0
- services/iot/helpers/servers.py +72 -0
- services/iot/helpers/uwb_device.py +95 -0
- services/iot/main.py +53 -0
- services/iot/models/__init__.py +8 -0
- services/iot/models/ble_device.py +118 -0
- services/iot/models/sensors.py +256 -0
- services/iot/models/servers.py +206 -0
- services/iot/models/uwb_device.py +106 -0
- services/iot/routers/__init__.py +8 -0
- services/iot/routers/ble_device.py +110 -0
- services/iot/routers/sensors.py +144 -0
- services/iot/routers/servers.py +141 -0
- services/iot/routers/uwb_device.py +148 -0
- services/module_control/__init__.py +1 -0
- services/module_control/helpers/__init__.py +1 -0
- services/module_control/helpers/integration_config.py +243 -0
- services/module_control/helpers/security.py +104 -0
- services/module_control/main.py +44 -0
- services/module_control/models/__init__.py +1 -0
- services/module_control/models/module_control.py +65 -0
- services/module_control/routers/__init__.py +1 -0
- services/module_control/routers/module_control.py +219 -0
- services/orders/helpers/__init__.py +11 -0
- services/orders/helpers/invoices.py +123 -0
- services/orders/helpers/order_customer_metrics.py +61 -0
- services/orders/helpers/order_details.py +71 -0
- services/orders/helpers/order_metadata.py +61 -0
- services/orders/helpers/order_payment_metadata.py +74 -0
- services/orders/helpers/orders.py +119 -0
- services/orders/helpers/sales_orders.py +136 -0
- services/orders/main.py +56 -0
- services/orders/models/__init__.py +11 -0
- services/orders/models/invoices.py +415 -0
- services/orders/models/order_customer_metrics.py +78 -0
- services/orders/models/order_details.py +46 -0
- services/orders/models/order_metadata.py +60 -0
- services/orders/models/order_payment_metadata.py +63 -0
- services/orders/models/orders.py +64 -0
- services/orders/models/sales_orders.py +130 -0
- services/orders/routers/__init__.py +11 -0
- services/orders/routers/invoices.py +111 -0
- services/orders/routers/order_customer_metrics.py +87 -0
- services/orders/routers/order_details.py +87 -0
- services/orders/routers/order_metadata.py +87 -0
- services/orders/routers/order_payment_metadata.py +87 -0
- services/orders/routers/orders.py +74 -0
- services/orders/routers/sales_orders.py +111 -0
- services/pm/helpers/__init__.py +14 -0
- services/pm/helpers/calendar_bookings.py +114 -0
- services/pm/helpers/calibration_point.py +110 -0
- services/pm/helpers/calibrations.py +196 -0
- services/pm/helpers/checklists.py +318 -0
- services/pm/helpers/cleaning.py +333 -0
- services/pm/helpers/downtime.py +376 -0
- services/pm/helpers/kanban_boards.py +186 -0
- services/pm/helpers/maintainance.py +177 -0
- services/pm/helpers/sop.py +1155 -0
- services/pm/helpers/sop_cdc.py +324 -0
- services/pm/helpers/weekly_schedules.py +79 -0
- services/pm/main.py +62 -0
- services/pm/models/__init__.py +14 -0
- services/pm/models/calendar_booking.py +82 -0
- services/pm/models/calibration_point.py +44 -0
- services/pm/models/calibrations.py +167 -0
- services/pm/models/checklists.py +117 -0
- services/pm/models/cleaning.py +203 -0
- services/pm/models/downtime.py +109 -0
- services/pm/models/kanban_board.py +178 -0
- services/pm/models/maintainanceLog.py +148 -0
- services/pm/models/sop.py +152 -0
- services/pm/models/weekly_schedule.py +91 -0
- services/pm/routers/__init__.py +14 -0
- services/pm/routers/calendar_bookings.py +143 -0
- services/pm/routers/calibration_point.py +94 -0
- services/pm/routers/calibrations.py +232 -0
- services/pm/routers/checklists.py +188 -0
- services/pm/routers/cleaning.py +127 -0
- services/pm/routers/downtime.py +143 -0
- services/pm/routers/kanban_boards.py +283 -0
- services/pm/routers/maintainance.py +241 -0
- services/pm/routers/sop.py +437 -0
- services/pm/routers/weekly_schedules.py +108 -0
- services/process_definitions/helpers/__init__.py +11 -0
- services/process_definitions/helpers/cpp_cqa_registry.py +120 -0
- services/process_definitions/helpers/mbmr_templates.py +107 -0
- services/process_definitions/helpers/packing_instructions.py +113 -0
- services/process_definitions/helpers/process_constraints.py +495 -0
- services/process_definitions/helpers/process_corrections.py +279 -0
- services/process_definitions/helpers/process_definition.py +996 -0
- services/process_definitions/helpers/process_node_catalog.py +786 -0
- services/process_definitions/helpers/process_post_checks.py +441 -0
- services/process_definitions/helpers/process_pre_checks.py +351 -0
- services/process_definitions/helpers/process_steps.py +220 -0
- services/process_definitions/main.py +71 -0
- services/process_definitions/models/__init__.py +13 -0
- services/process_definitions/models/cpp_cqa_registry.py +145 -0
- services/process_definitions/models/gxp_change_controls.py +38 -0
- services/process_definitions/models/gxp_risk_assessments.py +30 -0
- services/process_definitions/models/gxp_validation_evidence.py +33 -0
- services/process_definitions/models/mbmr_templates.py +173 -0
- services/process_definitions/models/packing_instructions.py +176 -0
- services/process_definitions/models/process_constraints.py +159 -0
- services/process_definitions/models/process_corrections.py +118 -0
- services/process_definitions/models/process_definition.py +685 -0
- services/process_definitions/models/process_definition_common.py +48 -0
- services/process_definitions/models/process_node_catalog.py +25 -0
- services/process_definitions/models/process_post_checks.py +171 -0
- services/process_definitions/models/process_pre_checks.py +168 -0
- services/process_definitions/models/process_steps.py +170 -0
- services/process_definitions/node_services/__init__.py +8 -0
- services/process_definitions/node_services/common.py +95 -0
- services/process_definitions/node_services/executor.py +499 -0
- services/process_definitions/node_services/flow_simulator.py +733 -0
- services/process_definitions/node_services/functions.py +193 -0
- services/process_definitions/node_services/messaging.py +44 -0
- services/process_definitions/node_services/models.py +221 -0
- services/process_definitions/node_services/network.py +161 -0
- services/process_definitions/node_services/parsers.py +87 -0
- services/process_definitions/node_services/sequence.py +95 -0
- services/process_definitions/node_services/storage.py +50 -0
- services/process_definitions/node_services/webhooks.py +52 -0
- services/process_definitions/routers/__init__.py +10 -0
- services/process_definitions/routers/cpp_cqa_registry.py +86 -0
- services/process_definitions/routers/mbmr_templates.py +84 -0
- services/process_definitions/routers/packing_instructions.py +84 -0
- services/process_definitions/routers/process_constraints.py +564 -0
- services/process_definitions/routers/process_corrections.py +343 -0
- services/process_definitions/routers/process_definition.py +992 -0
- services/process_definitions/routers/process_post_checks.py +529 -0
- services/process_definitions/routers/process_pre_checks.py +435 -0
- services/process_definitions/routers/process_steps.py +274 -0
- services/procurement/helpers/__init__.py +9 -0
- services/procurement/helpers/goods_receipts.py +240 -0
- services/procurement/helpers/purchase_order_shipping_information.py +85 -0
- services/procurement/helpers/purchase_orders.py +68 -0
- services/procurement/helpers/quality_control.py +235 -0
- services/procurement/helpers/sampling.py +404 -0
- services/procurement/main.py +52 -0
- services/procurement/models/__init__.py +9 -0
- services/procurement/models/goods_receipts.py +165 -0
- services/procurement/models/purchase_orders.py +54 -0
- services/procurement/models/quality_control.py +464 -0
- services/procurement/models/reinspection_records.py +28 -0
- services/procurement/models/sampling.py +262 -0
- services/procurement/models/shipping_information.py +51 -0
- services/procurement/routers/__init__.py +9 -0
- services/procurement/routers/goods_receipts.py +201 -0
- services/procurement/routers/purchase_orders.py +106 -0
- services/procurement/routers/quality_control.py +386 -0
- services/procurement/routers/sampling.py +296 -0
- services/procurement/routers/shipping_information.py +97 -0
- services/production/__init__.py +1 -0
- services/production/agents/__init__.py +5 -0
- services/production/agents/batch_planning_agent.py +815 -0
- services/production/models/__init__.py +25 -0
- services/production/models/batch.py +253 -0
- services/products/helpers/__init__.py +10 -0
- services/products/helpers/boms.py +100 -0
- services/products/helpers/drug_research.py +644 -0
- services/products/helpers/product_component.py +168 -0
- services/products/helpers/product_inventory.py +221 -0
- services/products/helpers/product_pricing.py +123 -0
- services/products/helpers/product_utils.py +32 -0
- services/products/helpers/products.py +81 -0
- services/products/main.py +59 -0
- services/products/models/__init__.py +9 -0
- services/products/models/drug_research.py +138 -0
- services/products/models/product_common.py +60 -0
- services/products/models/product_component.py +1028 -0
- services/products/models/product_inventory.py +118 -0
- services/products/models/product_pricing.py +73 -0
- services/products/models/products.py +151 -0
- services/products/routers/__init__.py +9 -0
- services/products/routers/boms.py +116 -0
- services/products/routers/drug_research.py +115 -0
- services/products/routers/product_components.py +123 -0
- services/products/routers/product_inventory.py +185 -0
- services/products/routers/product_pricing.py +136 -0
- services/products/routers/products.py +165 -0
- services/replenishment/__init__.py +1 -0
- services/replenishment/main.py +46 -0
- services/replenishment/routers/__init__.py +1 -0
- services/replenishment/routers/replenishment.py +20 -0
- services/shifts/helpers/__init__.py +7 -0
- services/shifts/helpers/shift_templates.py +124 -0
- services/shifts/helpers/shifts.py +79 -0
- services/shifts/helpers/timesheets.py +137 -0
- services/shifts/main.py +48 -0
- services/shifts/models/__init__.py +8 -0
- services/shifts/models/shift.py +62 -0
- services/shifts/models/shift_template.py +82 -0
- services/shifts/models/time_range.py +31 -0
- services/shifts/models/timesheet.py +196 -0
- services/shifts/routers/__init__.py +7 -0
- services/shifts/routers/shift_templates.py +97 -0
- services/shifts/routers/shifts.py +117 -0
- services/shifts/routers/timesheets.py +117 -0
- services/shipping/helpers/__init__.py +15 -0
- services/shipping/helpers/carrier.py +78 -0
- services/shipping/helpers/customs_declaration.py +104 -0
- services/shipping/helpers/delivery_note.py +99 -0
- services/shipping/helpers/package.py +95 -0
- services/shipping/helpers/pallet.py +85 -0
- services/shipping/helpers/route.py +93 -0
- services/shipping/helpers/shipping_information.py +82 -0
- services/shipping/helpers/shipping_provider_details.py +59 -0
- services/shipping/helpers/shipping_provider_metadata.py +59 -0
- services/shipping/helpers/vehicle.py +85 -0
- services/shipping/helpers/waybill.py +86 -0
- services/shipping/main.py +64 -0
- services/shipping/models/__init__.py +15 -0
- services/shipping/models/carrier.py +97 -0
- services/shipping/models/customs_declaration.py +138 -0
- services/shipping/models/delivery_note.py +163 -0
- services/shipping/models/package.py +152 -0
- services/shipping/models/pallet.py +137 -0
- services/shipping/models/route.py +120 -0
- services/shipping/models/shipping_information.py +55 -0
- services/shipping/models/shipping_provider_details.py +42 -0
- services/shipping/models/shipping_provider_metadata.py +54 -0
- services/shipping/models/vehicle.py +129 -0
- services/shipping/models/waybill.py +189 -0
- services/shipping/routers/__init__.py +15 -0
- services/shipping/routers/carrier.py +99 -0
- services/shipping/routers/customs_declaration.py +132 -0
- services/shipping/routers/delivery_note.py +150 -0
- services/shipping/routers/package.py +141 -0
- services/shipping/routers/pallet.py +108 -0
- services/shipping/routers/route.py +128 -0
- services/shipping/routers/shipping_information.py +97 -0
- services/shipping/routers/shipping_provider_details.py +80 -0
- services/shipping/routers/shipping_provider_metadata.py +80 -0
- services/shipping/routers/vehicle.py +117 -0
- services/shipping/routers/waybill.py +119 -0
- services/suppliers/helpers/__init__.py +13 -0
- services/suppliers/helpers/supplier_capabilities.py +58 -0
- services/suppliers/helpers/supplier_certifications.py +67 -0
- services/suppliers/helpers/supplier_details.py +58 -0
- services/suppliers/helpers/supplier_financials.py +58 -0
- services/suppliers/helpers/supplier_inventory.py +74 -0
- services/suppliers/helpers/supplier_locations.py +60 -0
- services/suppliers/helpers/supplier_pricing.py +69 -0
- services/suppliers/helpers/supplier_quality.py +69 -0
- services/suppliers/helpers/supplier_shipping.py +69 -0
- services/suppliers/main.py +60 -0
- services/suppliers/models/__init__.py +13 -0
- services/suppliers/models/supplier_capabilities.py +70 -0
- services/suppliers/models/supplier_certifications.py +64 -0
- services/suppliers/models/supplier_details.py +75 -0
- services/suppliers/models/supplier_financials.py +69 -0
- services/suppliers/models/supplier_inventory.py +76 -0
- services/suppliers/models/supplier_locations.py +70 -0
- services/suppliers/models/supplier_pricing.py +74 -0
- services/suppliers/models/supplier_quality.py +74 -0
- services/suppliers/models/supplier_shipping.py +76 -0
- services/suppliers/routers/__init__.py +13 -0
- services/suppliers/routers/supplier_capabilities.py +88 -0
- services/suppliers/routers/supplier_certifications.py +87 -0
- services/suppliers/routers/supplier_details.py +83 -0
- services/suppliers/routers/supplier_financials.py +83 -0
- services/suppliers/routers/supplier_inventory.py +105 -0
- services/suppliers/routers/supplier_locations.py +89 -0
- services/suppliers/routers/supplier_pricing.py +96 -0
- services/suppliers/routers/supplier_quality.py +96 -0
- services/suppliers/routers/supplier_shipping.py +96 -0
- services/supply_allocation/main.py +46 -0
- services/supply_allocation/routers/__init__.py +1 -0
- services/supply_allocation/routers/allocation.py +20 -0
- services/taxonomy/helpers/__init__.py +7 -0
- services/taxonomy/helpers/classes.py +48 -0
- services/taxonomy/helpers/family.py +53 -0
- services/taxonomy/helpers/subclass.py +58 -0
- services/taxonomy/main.py +48 -0
- services/taxonomy/models/__init__.py +7 -0
- services/taxonomy/models/classes.py +52 -0
- services/taxonomy/models/family.py +60 -0
- services/taxonomy/models/subclass.py +50 -0
- services/taxonomy/routers/__init__.py +7 -0
- services/taxonomy/routers/classes.py +78 -0
- services/taxonomy/routers/family.py +77 -0
- services/taxonomy/routers/subclass.py +82 -0
- services/warehouse_execution/__init__.py +1 -0
- services/warehouse_execution/main.py +46 -0
- services/warehouse_execution/routers/__init__.py +1 -0
- services/warehouse_execution/routers/execution.py +21 -0
- services/work_order_agent/__init__.py +17 -0
- services/work_order_agent/agent/__init__.py +17 -0
- services/work_order_agent/agent/work_order_agent.py +658 -0
- services/work_order_agent/tracking/__init__.py +101 -0
- services/work_order_agent/tracking/event_system.py +182 -0
- services/work_order_agent/tracking/state_machine.py +163 -0
- services/work_order_agent/tracking/state_machine_integrator.py +295 -0
- services/work_order_agent/tracking/test_phase2_implementation.py +302 -0
- services/work_order_agent/tracking/time_analysis.py +301 -0
- services/work_order_agent/tracking/tracked_work_order.py +255 -0
- services/work_order_agent/tracking/work_order_adapter.py +367 -0
- services/work_order_agent/tracking/work_order_batch_manager.py +406 -0
- services/work_order_agent/tracking/work_order_repository.py +431 -0
- services/workorders/helpers/__init__.py +5 -0
- services/workorders/helpers/area_room_usage_ledger.py +139 -0
- services/workorders/helpers/batch_execution_records.py +265 -0
- services/workorders/helpers/batch_release_workflows.py +158 -0
- services/workorders/helpers/batch_step_executions.py +145 -0
- services/workorders/helpers/equipment_usage_ledger.py +209 -0
- services/workorders/helpers/executed_bmr_records.py +170 -0
- services/workorders/helpers/executed_bpr_records.py +170 -0
- services/workorders/helpers/executed_instruction_evidence.py +155 -0
- services/workorders/helpers/ipc_result_records.py +134 -0
- services/workorders/helpers/production_batches.py +117 -0
- services/workorders/helpers/work_orders.py +367 -0
- services/workorders/helpers/yield_reconciliation_records.py +158 -0
- services/workorders/main.py +110 -0
- services/workorders/models/__init__.py +5 -0
- services/workorders/models/area_room_usage_ledger.py +154 -0
- services/workorders/models/batch_execution_records.py +575 -0
- services/workorders/models/batch_release_workflows.py +190 -0
- services/workorders/models/batch_step_executions.py +142 -0
- services/workorders/models/equipment_usage_ledger.py +144 -0
- services/workorders/models/executed_bmr_records.py +220 -0
- services/workorders/models/executed_bpr_records.py +220 -0
- services/workorders/models/executed_instruction_evidence.py +128 -0
- services/workorders/models/ipc_result_records.py +164 -0
- services/workorders/models/production_batches.py +181 -0
- services/workorders/models/work_orders.py +255 -0
- services/workorders/models/yield_reconciliation_records.py +175 -0
- services/workorders/routers/__init__.py +5 -0
- services/workorders/routers/area_room_usage_ledger.py +117 -0
- services/workorders/routers/batch_execution_records.py +103 -0
- services/workorders/routers/batch_release_workflows.py +86 -0
- services/workorders/routers/batch_step_executions.py +88 -0
- services/workorders/routers/equipment_usage_ledger.py +115 -0
- services/workorders/routers/executed_bmr_records.py +86 -0
- services/workorders/routers/executed_bpr_records.py +86 -0
- services/workorders/routers/executed_instruction_evidence.py +86 -0
- services/workorders/routers/ipc_result_records.py +86 -0
- services/workorders/routers/production_batches.py +86 -0
- services/workorders/routers/work_orders.py +257 -0
- services/workorders/routers/yield_reconciliation_records.py +86 -0
- src/broca/__init__.py +5 -0
- src/broca/agent.py +201 -0
- src/cerebellum/__init__.py +0 -0
- src/cerebellum/adapter.py +84 -0
- src/cerebellum/capabilities/__init__.py +0 -0
- src/cerebellum/capabilities/agent/__init__.py +0 -0
- src/cerebellum/capabilities/agent/agents.py +65 -0
- src/cerebellum/capabilities/ai/__init__.py +0 -0
- src/cerebellum/capabilities/ai/providers.py +106 -0
- src/cerebellum/capabilities/api/__init__.py +0 -0
- src/cerebellum/capabilities/api/graphql.py +35 -0
- src/cerebellum/capabilities/api/rest_api.py +45 -0
- src/cerebellum/capabilities/api/webhook.py +30 -0
- src/cerebellum/capabilities/browser/__init__.py +0 -0
- src/cerebellum/capabilities/browser/browsers.py +27 -0
- src/cerebellum/capabilities/cloud/__init__.py +0 -0
- src/cerebellum/capabilities/cloud/cloud.py +46 -0
- src/cerebellum/capabilities/communication/__init__.py +0 -0
- src/cerebellum/capabilities/communication/communication.py +62 -0
- src/cerebellum/capabilities/database/__init__.py +0 -0
- src/cerebellum/capabilities/database/elasticsearch.py +40 -0
- src/cerebellum/capabilities/database/influxdb.py +37 -0
- src/cerebellum/capabilities/database/mongodb.py +50 -0
- src/cerebellum/capabilities/database/neo4j.py +32 -0
- src/cerebellum/capabilities/database/redis.py +44 -0
- src/cerebellum/capabilities/enterprise/__init__.py +0 -0
- src/cerebellum/capabilities/enterprise/opensource.py +180 -0
- src/cerebellum/capabilities/enterprise/proprietary.py +313 -0
- src/cerebellum/capabilities/event_streaming/__init__.py +0 -0
- src/cerebellum/capabilities/event_streaming/streaming.py +38 -0
- src/cerebellum/capabilities/infrastructure/__init__.py +0 -0
- src/cerebellum/capabilities/infrastructure/infrastructure.py +30 -0
- src/cerebellum/capabilities/productivity/__init__.py +0 -0
- src/cerebellum/capabilities/productivity/productivity.py +158 -0
- src/cerebellum/capabilities/robotics/__init__.py +0 -0
- src/cerebellum/capabilities/robotics/robotics.py +124 -0
- src/cerebellum/capabilities/runtime/__init__.py +0 -0
- src/cerebellum/capabilities/runtime/runtimes.py +92 -0
- src/cerebellum/capabilities/search/__init__.py +0 -0
- src/cerebellum/capabilities/search/search.py +63 -0
- src/cerebellum/capabilities/source_control/__init__.py +0 -0
- src/cerebellum/capabilities/source_control/source_control.py +113 -0
- src/cerebellum/capabilities/storage/__init__.py +0 -0
- src/cerebellum/capabilities/storage/storage.py +94 -0
- src/cerebellum/capabilities/workflow/__init__.py +0 -0
- src/cerebellum/capabilities/workflow/workflows.py +49 -0
- src/cerebellum/capability.py +108 -0
- src/cerebellum/config.py +157 -0
- src/cerebellum/fallback.py +147 -0
- src/cerebellum/fallback_engine.py +121 -0
- src/cerebellum/key_manager.py +129 -0
- src/cerebellum/keystore.py +179 -0
- src/cerebellum/lifecycle.py +54 -0
- src/cerebellum/metadata.py +61 -0
- src/cerebellum/operator/base.py +25 -0
- src/cerebellum/peripheral.py +92 -0
- src/cerebellum/registry.py +98 -0
- src/cerebellum/resolve_entity_capability.py +259 -0
- src/cingulate/benchmark/__init__.py +23 -0
- src/cingulate/benchmark/reporter.py +102 -0
- src/cingulate/benchmark/runner.py +159 -0
- src/cingulate/benchmark/scenario_runner.py +150 -0
- src/cingulate/benchmark/validator.py +102 -0
- src/cingulate/governance/__init__.py +21 -0
- src/cingulate/governance/architecture_validator.py +194 -0
- src/cingulate/governance/compliance.py +104 -0
- src/cingulate/governance/governance.py +77 -0
- src/cingulate/governance/policy_registry.py +91 -0
- src/cortex/__init__.py +33 -0
- src/cortex/cost.py +71 -0
- src/cortex/counterfactual.py +162 -0
- src/cortex/digital_twin.py +90 -0
- src/cortex/experience.py +83 -0
- src/cortex/feedback.py +144 -0
- src/cortex/loss.py +116 -0
- src/cortex/prediction.py +142 -0
- src/cortex/replay.py +130 -0
- src/cortex/reward.py +113 -0
- src/cortex/simulator.py +102 -0
- src/cortex/world_model.py +180 -0
- src/cortex/world_model_simulation.py +1591 -0
- src/cortex/world_state.py +121 -0
- src/cortex/xavier.py +250 -0
- src/deepdive/__init__.py +29 -0
- src/deepdive/aggregation.py +113 -0
- src/deepdive/digital_twin_aggregator.py +128 -0
- src/deepdive/elasticsearch_adapter.py +110 -0
- src/deepdive/fleet_analytics.py +131 -0
- src/deepdive/knowledge_aggregator.py +130 -0
- src/homeostasis/__init__.py +19 -0
- src/homeostasis/control_plane.py +159 -0
- src/introspection/__init__.py +38 -0
- src/introspection/alerting.py +142 -0
- src/introspection/health.py +101 -0
- src/introspection/lemon.py +243 -0
- src/introspection/logging.py +147 -0
- src/introspection/metrics.py +106 -0
- src/introspection/tracing.py +162 -0
- src/monkey_brain/__init__.py +1 -0
- src/monkey_brain/api/main.py +148 -0
- src/monkey_brain/api/models.py +81 -0
- src/monkey_brain/api/routes/routes/keys.py +106 -0
- src/monkey_brain/api/routes/routes/run.py +169 -0
- src/monkey_brain/api/routes/routes/simulate.py +485 -0
- src/monkey_brain/dlm/__init__.py +44 -0
- src/monkey_brain/dlm/dlm.py +139 -0
- src/monkey_brain/dlm/gc.py +115 -0
- src/monkey_brain/dlm/lifecycle.py +149 -0
- src/monkey_brain/dlm/orphans.py +99 -0
- src/monkey_brain/dlm/storage.py +149 -0
- src/monkey_brain/dlm/ttl.py +140 -0
- src/monkey_brain/documents/__init__.py +0 -0
- src/monkey_brain/documents/document_ocr.py +6 -0
- src/monkey_brain/kernel/__init__.py +53 -0
- src/monkey_brain/kernel/capability_interface.py +144 -0
- src/monkey_brain/kernel/classifier/__init__.py +1 -0
- src/monkey_brain/kernel/classifier/embed_classifier.py +125 -0
- src/monkey_brain/kernel/classifier/intent_examples.py +106 -0
- src/monkey_brain/kernel/dag.py +23 -0
- src/monkey_brain/kernel/execution_state.py +257 -0
- src/monkey_brain/kernel/goal_planner.py +85 -0
- src/monkey_brain/kernel/goal_router.py +20 -0
- src/monkey_brain/kernel/goals/__init__.py +1 -0
- src/monkey_brain/kernel/goals/goal.py +130 -0
- src/monkey_brain/kernel/goals/goal_bootstrap.py +38 -0
- src/monkey_brain/kernel/goals/goal_classifier.py +132 -0
- src/monkey_brain/kernel/goals/goal_registry.py +75 -0
- src/monkey_brain/kernel/intents/__init__.py +1 -0
- src/monkey_brain/kernel/intents/event_adapter.py +246 -0
- src/monkey_brain/kernel/intents/helpers.py +13 -0
- src/monkey_brain/kernel/intents/intent_registry.py +705 -0
- src/monkey_brain/kernel/intents/intent_router.py +102 -0
- src/monkey_brain/kernel/intents/predicates/approval_create.py +9 -0
- src/monkey_brain/kernel/intents/predicates/approval_decision.py +9 -0
- src/monkey_brain/kernel/intents/predicates/approval_hold.py +9 -0
- src/monkey_brain/kernel/intents/predicates/approval_query.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_close.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_creation.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_delete.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_hold.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_record.py +9 -0
- src/monkey_brain/kernel/intents/predicates/batch_update.py +9 -0
- src/monkey_brain/kernel/intents/predicates/change_control.py +49 -0
- src/monkey_brain/kernel/intents/predicates/compliance_audit.py +14 -0
- src/monkey_brain/kernel/intents/predicates/decision_intelligence.py +9 -0
- src/monkey_brain/kernel/intents/predicates/drug_research.py +9 -0
- src/monkey_brain/kernel/intents/predicates/fuzzy_match.py +19 -0
- src/monkey_brain/kernel/intents/predicates/production_kpi.py +9 -0
- src/monkey_brain/kernel/intents/predicates/sop_create.py +9 -0
- src/monkey_brain/kernel/intents/predicates/sop_query.py +9 -0
- src/monkey_brain/kernel/intents/predicates/sop_update.py +9 -0
- src/monkey_brain/kernel/intents/predicates/warehouse_shipping.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_create.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_delete.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_hold.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_query.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_status.py +9 -0
- src/monkey_brain/kernel/intents/predicates/work_order_update.py +9 -0
- src/monkey_brain/kernel/intents/predicates/worker.py +9 -0
- src/monkey_brain/kernel/intents/telemetry_adapter.py +274 -0
- src/monkey_brain/kernel/intents/utils.py +68 -0
- src/monkey_brain/kernel/learning.py +98 -0
- src/monkey_brain/kernel/llm_explorer.py +188 -0
- src/monkey_brain/kernel/loss.py +81 -0
- src/monkey_brain/kernel/nlp/__init__.py +1 -0
- src/monkey_brain/kernel/nlp/compat.py +23 -0
- src/monkey_brain/kernel/nlp/models.py +10 -0
- src/monkey_brain/kernel/nlp/question_analyzer.py +203 -0
- src/monkey_brain/kernel/nlp/spacy_parser.py +53 -0
- src/monkey_brain/kernel/observer.py +97 -0
- src/monkey_brain/kernel/parser/__init__.py +3 -0
- src/monkey_brain/kernel/parser/ast.py +28 -0
- src/monkey_brain/kernel/parser/extractors/__init__.py +11 -0
- src/monkey_brain/kernel/parser/extractors/entities.py +21 -0
- src/monkey_brain/kernel/parser/extractors/filters.py +16 -0
- src/monkey_brain/kernel/parser/extractors/projections.py +36 -0
- src/monkey_brain/kernel/parser/extractors/verbs.py +31 -0
- src/monkey_brain/kernel/parser/parser.py +57 -0
- src/monkey_brain/kernel/parser/rules.py +75 -0
- src/monkey_brain/kernel/pipeline.py +44 -0
- src/monkey_brain/kernel/planner.py +57 -0
- src/monkey_brain/kernel/rl/__init__.py +33 -0
- src/monkey_brain/kernel/rl/learner.py +98 -0
- src/monkey_brain/kernel/rl/policy.py +254 -0
- src/monkey_brain/kernel/rl/reward.py +117 -0
- src/monkey_brain/kernel/rl/transition.py +112 -0
- src/monkey_brain/persistence/__init__.py +47 -0
- src/monkey_brain/persistence/adapters.py +49 -0
- src/monkey_brain/persistence/events.py +105 -0
- src/monkey_brain/persistence/manager.py +124 -0
- src/monkey_brain/persistence/mongodb_adapter.py +91 -0
- src/monkey_brain/persistence/redis_adapter.py +93 -0
- src/monkey_brain/persistence/reducer.py +111 -0
- src/monkey_brain/runtime/__init__.py +49 -0
- src/monkey_brain/runtime/depedencies.py +8 -0
- src/monkey_brain/runtime/engine.py +183 -0
- src/monkey_brain/runtime/message_bus.py +82 -0
- src/monkey_brain/runtime/process.py +144 -0
- src/monkey_brain/runtime/resource_manager.py +100 -0
- src/monkey_brain/runtime/routers.py +8 -0
- src/monkey_brain/runtime/runtime.py +199 -0
- src/monkey_brain/runtime/scheduler.py +165 -0
- src/monkey_brain/runtime/supervisor.py +133 -0
- src/monkey_brain/runtime/worker_pool.py +111 -0
- src/plasticity/seed/__init__.py +30 -0
- src/plasticity/seed/benchmark_generator.py +105 -0
- src/plasticity/seed/event_generator.py +122 -0
- src/plasticity/seed/scenario_builder.py +134 -0
- src/plasticity/seed/seed_data.py +206 -0
- src/plasticity/seed/seeder.py +122 -0
- src/plasticity/testing/__init__.py +28 -0
- src/plasticity/testing/performance.py +131 -0
- src/plasticity/testing/profiler.py +255 -0
- src/plasticity/testing/reporter.py +84 -0
- src/plasticity/testing/runner.py +209 -0
- src/sync/__init__.py +12 -0
- src/sync/cloud_aggregator.py +63 -0
- src/sync/edge_node.py +51 -0
- src/sync/sync_manager.py +74 -0
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
from mimetypes import guess_type
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import shutil
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
from zoneinfo import ZoneInfo
|
|
8
|
+
import boto3
|
|
9
|
+
from fastapi import FastAPI, File, Response, UploadFile, HTTPException, BackgroundTasks
|
|
10
|
+
from dotenv import load_dotenv
|
|
11
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
|
12
|
+
from uuid import uuid4
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
import asyncio
|
|
15
|
+
import httpx
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import base64
|
|
20
|
+
|
|
21
|
+
import websockets
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Load environment variables
|
|
26
|
+
load_dotenv()
|
|
27
|
+
|
|
28
|
+
# env variables for s3
|
|
29
|
+
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") or os.getenv("ACCESS_KEY_ID")
|
|
30
|
+
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") or os.getenv("SECRET_ACCESS_KEY")
|
|
31
|
+
AWS_REGION = os.getenv("AWS_REGION", os.getenv("REGION", "ap-south-1"))
|
|
32
|
+
S3_BUCKET = os.getenv("AWS_S3_BUCKET") or os.getenv("S3_BUCKET")
|
|
33
|
+
S3_BACKUP_BUCKET = os.getenv("AWS_S3_BACKUP_BUCKET") or os.getenv("S3_BACKUP_BUCKET")
|
|
34
|
+
LOCAL_UPLOAD_DIR = Path(os.getenv("FILE_UPLOAD_DIR", "uploads/files"))
|
|
35
|
+
|
|
36
|
+
# env variabls for websocket
|
|
37
|
+
FORWARD_URL = os.getenv("WEBSOCKET_URL") or os.getenv("FORWARD_URL") or ""
|
|
38
|
+
FORWARD_TIMEOUT = int(os.getenv("WEBSOCKET_TIMEOUT", "120"))
|
|
39
|
+
FORWARD_ENABLED = bool(FORWARD_URL and FORWARD_URL.strip())
|
|
40
|
+
FORWARD_EXPECT_JSON = os.getenv("WEBSOCKET_EXPECT_JSON", "false").lower() == "true"
|
|
41
|
+
|
|
42
|
+
# Validate required config
|
|
43
|
+
if not S3_BUCKET:
|
|
44
|
+
logger.warning("S3_BUCKET is not set. Set AWS_S3_BUCKET or S3_BUCKET env var.")
|
|
45
|
+
|
|
46
|
+
# Initialize FastAPI app
|
|
47
|
+
app = FastAPI(
|
|
48
|
+
title="Document Upload API with Backup",
|
|
49
|
+
description="FastAPI application for Azure Container Apps",
|
|
50
|
+
version="1.0.0"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Initialize S3 client (credentials optional if IAM role present)
|
|
54
|
+
s3_client_kwargs = {"region_name": AWS_REGION}
|
|
55
|
+
if AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY:
|
|
56
|
+
s3_client_kwargs.update({
|
|
57
|
+
"aws_access_key_id": AWS_ACCESS_KEY_ID,
|
|
58
|
+
"aws_secret_access_key": AWS_SECRET_ACCESS_KEY,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
s3_client = boto3.client("s3", **s3_client_kwargs)
|
|
62
|
+
|
|
63
|
+
logger.info(f"Forward URL: {FORWARD_URL}")
|
|
64
|
+
logger.info(f"Forward Enabled: {FORWARD_ENABLED}")
|
|
65
|
+
logger.info(f"Forward Timeout: {FORWARD_TIMEOUT}s")
|
|
66
|
+
logger.info(f"Forward Expect JSON: {FORWARD_EXPECT_JSON}")
|
|
67
|
+
logger.info(f"S3 Bucket: {S3_BUCKET}")
|
|
68
|
+
logger.info(f"S3 Backup Bucket: {S3_BACKUP_BUCKET or (S3_BUCKET + '-backup' if S3_BUCKET else 'N/A')}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _local_upload_path(date: str, filename: str) -> Path:
|
|
72
|
+
return LOCAL_UPLOAD_DIR / date / filename
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _local_backup_path(date: str, filename: str) -> Path:
|
|
76
|
+
return LOCAL_UPLOAD_DIR / "backups" / date / filename
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _save_local_file(date: str, filename: str, file_content: bytes) -> Path:
|
|
80
|
+
local_path = _local_upload_path(date, filename)
|
|
81
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
local_path.write_bytes(file_content)
|
|
83
|
+
return local_path
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _local_file_info(date: str, path: Path, backup_exists: bool = False) -> dict:
|
|
87
|
+
stat = path.stat()
|
|
88
|
+
return {
|
|
89
|
+
"filename": path.name,
|
|
90
|
+
"key": f"uploads/{date}/{path.name}",
|
|
91
|
+
"size": stat.st_size,
|
|
92
|
+
"last_modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
|
|
93
|
+
"url": str(path),
|
|
94
|
+
"storage_backend": "local",
|
|
95
|
+
"backup_exists": backup_exists,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _list_local_files(date: str, create_backup: bool = True) -> dict:
|
|
100
|
+
folder = LOCAL_UPLOAD_DIR / date
|
|
101
|
+
if not folder.exists():
|
|
102
|
+
return {"date": date, "file_count": 0, "files": [], "message": "No files found for this date"}
|
|
103
|
+
|
|
104
|
+
files = []
|
|
105
|
+
for path in sorted(item for item in folder.iterdir() if item.is_file()):
|
|
106
|
+
backup_path = _local_backup_path(date, path.name)
|
|
107
|
+
backup_exists = backup_path.exists()
|
|
108
|
+
if create_backup and not backup_exists:
|
|
109
|
+
backup_path.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
shutil.copy2(path, backup_path)
|
|
111
|
+
backup_exists = True
|
|
112
|
+
files.append(_local_file_info(date, path, backup_exists=backup_exists))
|
|
113
|
+
|
|
114
|
+
return {"date": date, "file_count": len(files), "files": files, "storage_backend": "local"}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _download_headers(filename: str) -> dict[str, str]:
|
|
118
|
+
safe_name = Path(filename or "download").name.replace('"', "")
|
|
119
|
+
encoded_name = quote(safe_name)
|
|
120
|
+
return {
|
|
121
|
+
"Content-Disposition": f"attachment; filename=\"{safe_name}\"; filename*=UTF-8''{encoded_name}",
|
|
122
|
+
"X-Content-Type-Options": "nosniff",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _read_local_file(date: str, filename: str, *, as_attachment: bool = True) -> Response:
|
|
127
|
+
local_path = _local_upload_path(date, filename)
|
|
128
|
+
if not local_path.exists():
|
|
129
|
+
raise HTTPException(status_code=404, detail="File not found locally")
|
|
130
|
+
|
|
131
|
+
content_type, _ = guess_type(filename)
|
|
132
|
+
if not content_type:
|
|
133
|
+
content_type = "application/octet-stream"
|
|
134
|
+
|
|
135
|
+
headers = _download_headers(filename) if as_attachment else {"X-Content-Type-Options": "nosniff"}
|
|
136
|
+
return Response(content=local_path.read_bytes(), media_type=content_type, headers=headers)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _delete_local_file(date: str, filename: str, delete_backup: bool = True) -> dict:
|
|
140
|
+
local_path = _local_upload_path(date, filename)
|
|
141
|
+
backup_path = _local_backup_path(date, filename)
|
|
142
|
+
if not local_path.exists():
|
|
143
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
144
|
+
|
|
145
|
+
local_path.unlink()
|
|
146
|
+
backup_deleted = False
|
|
147
|
+
if delete_backup and backup_path.exists():
|
|
148
|
+
backup_path.unlink()
|
|
149
|
+
backup_deleted = True
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
"message": "File deleted successfully",
|
|
153
|
+
"filename": filename,
|
|
154
|
+
"date": date,
|
|
155
|
+
"storage_backend": "local",
|
|
156
|
+
"backup_deleted": backup_deleted,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _delete_local_folder(date: str, delete_backup: bool = True) -> dict:
|
|
161
|
+
folder = LOCAL_UPLOAD_DIR / date
|
|
162
|
+
backup_folder = LOCAL_UPLOAD_DIR / "backups" / date
|
|
163
|
+
if not folder.exists():
|
|
164
|
+
raise HTTPException(status_code=404, detail=f"No files found for date: {date}")
|
|
165
|
+
|
|
166
|
+
deleted_count = sum(1 for item in folder.iterdir() if item.is_file())
|
|
167
|
+
shutil.rmtree(folder)
|
|
168
|
+
backup_deleted = False
|
|
169
|
+
if delete_backup and backup_folder.exists():
|
|
170
|
+
shutil.rmtree(backup_folder)
|
|
171
|
+
backup_deleted = True
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
"message": f"Deleted {deleted_count} files for date {date}",
|
|
175
|
+
"date": date,
|
|
176
|
+
"deleted_count": deleted_count,
|
|
177
|
+
"storage_backend": "local",
|
|
178
|
+
"backup_deleted": backup_deleted,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async def delete_backup_folder(date: str, expected_count: int):
|
|
182
|
+
"""Background task to delete backup folder for a specific date."""
|
|
183
|
+
try:
|
|
184
|
+
backup_bucket = S3_BACKUP_BUCKET or (S3_BUCKET + "-backup" if S3_BUCKET else None)
|
|
185
|
+
if not backup_bucket:
|
|
186
|
+
logger.warning("No backup bucket configured; skipping backup deletion.")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
backup_prefix = f"backups/{date}/"
|
|
190
|
+
logger.info(f"Deleting backup folder: {backup_prefix} from {backup_bucket}")
|
|
191
|
+
|
|
192
|
+
response = s3_client.list_objects_v2(Bucket=backup_bucket, Prefix=backup_prefix)
|
|
193
|
+
if "Contents" in response:
|
|
194
|
+
objects_to_delete = [{"Key": obj["Key"]} for obj in response["Contents"]]
|
|
195
|
+
batch_size = 1000
|
|
196
|
+
deleted_total = 0
|
|
197
|
+
for i in range(0, len(objects_to_delete), batch_size):
|
|
198
|
+
batch = objects_to_delete[i:i + batch_size]
|
|
199
|
+
s3_client.delete_objects(Bucket=backup_bucket, Delete={"Objects": batch})
|
|
200
|
+
deleted_total += len(batch)
|
|
201
|
+
logger.info(f"✅ Deleted {deleted_total} backup files for {date}")
|
|
202
|
+
else:
|
|
203
|
+
logger.info(f"No backup files found for {date}")
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.error(f"Failed to delete backup folder for {date}: {e}", exc_info=True)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
async def upload_file_helper(
|
|
209
|
+
file: UploadFile = File(...),
|
|
210
|
+
notify: bool = True
|
|
211
|
+
):
|
|
212
|
+
"""
|
|
213
|
+
Upload a document to S3 and optionally forward it to another service via WebSocket.
|
|
214
|
+
Folder structure: uploads/YYYY-MM-DD/<uuid>.<ext>
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
logger.info(f"Uploading file: {file.filename}, notify: {notify}")
|
|
218
|
+
|
|
219
|
+
# Create unique filename and S3 key
|
|
220
|
+
ist_now = datetime.now(ZoneInfo("Asia/Kolkata"))
|
|
221
|
+
date_folder = ist_now.strftime("%Y-%m-%d")
|
|
222
|
+
file_extension = os.path.splitext(file.filename)[-1]
|
|
223
|
+
unique_filename = f"{uuid4()}{file_extension}"
|
|
224
|
+
s3_key = f"uploads/{str(date_folder)}/{unique_filename}"
|
|
225
|
+
|
|
226
|
+
# Read file
|
|
227
|
+
file_content = await file.read()
|
|
228
|
+
logger.info(f"Read {len(file_content)} bytes from uploaded file")
|
|
229
|
+
|
|
230
|
+
# Get MIME type
|
|
231
|
+
mime_type = file.content_type
|
|
232
|
+
|
|
233
|
+
storage_backend = "s3"
|
|
234
|
+
storage_error = None
|
|
235
|
+
local_path = None
|
|
236
|
+
|
|
237
|
+
if S3_BUCKET:
|
|
238
|
+
try:
|
|
239
|
+
s3_client.put_object(
|
|
240
|
+
Bucket=S3_BUCKET,
|
|
241
|
+
Key=s3_key,
|
|
242
|
+
Body=file_content,
|
|
243
|
+
ContentType=file.content_type or "application/octet-stream"
|
|
244
|
+
)
|
|
245
|
+
logger.info(f"Successfully uploaded to S3: {s3_key}")
|
|
246
|
+
file_url = f"https://{S3_BUCKET}.s3.{AWS_REGION}.amazonaws.com/{s3_key}"
|
|
247
|
+
except (BotoCoreError, ClientError, Exception) as exc:
|
|
248
|
+
storage_backend = "local"
|
|
249
|
+
storage_error = str(exc)
|
|
250
|
+
local_path = _save_local_file(date_folder, unique_filename, file_content)
|
|
251
|
+
file_url = str(local_path)
|
|
252
|
+
logger.error(
|
|
253
|
+
"S3 upload failed; saved file locally at %s: %s",
|
|
254
|
+
local_path,
|
|
255
|
+
exc,
|
|
256
|
+
exc_info=True,
|
|
257
|
+
)
|
|
258
|
+
else:
|
|
259
|
+
storage_backend = "local"
|
|
260
|
+
storage_error = "S3 bucket is not configured"
|
|
261
|
+
local_path = _save_local_file(date_folder, unique_filename, file_content)
|
|
262
|
+
file_url = str(local_path)
|
|
263
|
+
logger.warning("S3 bucket is not configured; saved file locally at %s", local_path)
|
|
264
|
+
|
|
265
|
+
response_data = {
|
|
266
|
+
"message": "File uploaded successfully",
|
|
267
|
+
"filename": unique_filename,
|
|
268
|
+
"date_folder": date_folder,
|
|
269
|
+
"url": file_url,
|
|
270
|
+
"mime_type": mime_type,
|
|
271
|
+
"s3_key": s3_key if storage_backend == "s3" else None,
|
|
272
|
+
"storage_backend": storage_backend,
|
|
273
|
+
"local_path": str(local_path) if local_path else None,
|
|
274
|
+
"storage_error": storage_error,
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# 4️⃣ Forward file via WebSocket
|
|
278
|
+
if notify and FORWARD_ENABLED:
|
|
279
|
+
logger.info(f"Forwarding file to WebSocket bridge at {FORWARD_URL}")
|
|
280
|
+
|
|
281
|
+
# Create temp directory if it doesn't exist
|
|
282
|
+
os.makedirs("/tmp", exist_ok=True)
|
|
283
|
+
temp_file_path = f"/tmp/{unique_filename}"
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
# Save file temporarily for WebSocket sending
|
|
287
|
+
with open(temp_file_path, "wb") as f:
|
|
288
|
+
f.write(file_content)
|
|
289
|
+
logger.info(f"Saved temp file: {temp_file_path}")
|
|
290
|
+
|
|
291
|
+
logger.info(mime_type)
|
|
292
|
+
if mime_type == "application/pdf":
|
|
293
|
+
|
|
294
|
+
# Pass the original filename to preserve it
|
|
295
|
+
ws_result = await send_file_to_bridge(
|
|
296
|
+
file_path=temp_file_path,
|
|
297
|
+
original_filename=file.filename,
|
|
298
|
+
ws_uri=FORWARD_URL,
|
|
299
|
+
timeout=FORWARD_TIMEOUT
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Clean up temp file
|
|
303
|
+
try:
|
|
304
|
+
os.remove(temp_file_path)
|
|
305
|
+
logger.info(f"Cleaned up temp file: {temp_file_path}")
|
|
306
|
+
except Exception as cleanup_err:
|
|
307
|
+
logger.warning(f"Failed to cleanup temp file: {cleanup_err}")
|
|
308
|
+
|
|
309
|
+
if ws_result.get("status") == "queued":
|
|
310
|
+
response_data["forward_status"] = "success"
|
|
311
|
+
response_data["forward_response"] = ws_result
|
|
312
|
+
logger.info(f"✅ File forwarded successfully via WebSocket")
|
|
313
|
+
else:
|
|
314
|
+
response_data["forward_status"] = "failed"
|
|
315
|
+
response_data["forward_response"] = ws_result
|
|
316
|
+
logger.error(f"❌ File forwarding failed: {ws_result}")
|
|
317
|
+
|
|
318
|
+
elif mime_type in ("image/jpeg", "image/png", "image/jpg", "image/webp"):
|
|
319
|
+
logger.info("Processing image with Qwen-VL via OpenRouter for OCR")
|
|
320
|
+
|
|
321
|
+
# Extract text from image using Qwen-VL via OpenRouter
|
|
322
|
+
ocr_result = await extract_text_with_openrouter(file_content, mime_type)
|
|
323
|
+
|
|
324
|
+
if ocr_result.get("success"):
|
|
325
|
+
response_data["ocr_text"] = ocr_result.get("text")
|
|
326
|
+
response_data["ocr_status"] = "success"
|
|
327
|
+
response_data["ocr_model"] = ocr_result.get("model")
|
|
328
|
+
logger.info(f"✅ OCR completed: {len(ocr_result.get('text', ''))} characters extracted")
|
|
329
|
+
# send the extracted text to web socket
|
|
330
|
+
response = json.dumps({"content":ocr_result.get("text"),"mime_type":mime_type})
|
|
331
|
+
await send_text(response)
|
|
332
|
+
else:
|
|
333
|
+
response_data["ocr_status"] = "failed"
|
|
334
|
+
response_data["ocr_error"] = ocr_result.get("error")
|
|
335
|
+
logger.error(f"❌ OCR failed: {ocr_result.get('error')}")
|
|
336
|
+
|
|
337
|
+
# Clean up temp file
|
|
338
|
+
try:
|
|
339
|
+
os.remove(temp_file_path)
|
|
340
|
+
logger.info(f"Cleaned up temp file: {temp_file_path}")
|
|
341
|
+
except Exception as cleanup_err:
|
|
342
|
+
logger.warning(f"Failed to cleanup temp file: {cleanup_err}")
|
|
343
|
+
|
|
344
|
+
except Exception as forward_err:
|
|
345
|
+
logger.error(f"Error during file forwarding: {forward_err}")
|
|
346
|
+
response_data["forward_status"] = "error"
|
|
347
|
+
response_data["forward_error"] = str(forward_err)
|
|
348
|
+
|
|
349
|
+
return response_data
|
|
350
|
+
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.error(f"Error uploading file: {str(e)}")
|
|
353
|
+
raise HTTPException(status_code=500, detail=f"File upload failed: {str(e)}")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
async def extract_text_with_openrouter(
|
|
357
|
+
image_bytes: bytes,
|
|
358
|
+
mime_type: str,
|
|
359
|
+
model: str = "qwen/qwen-2-vl-72b-instruct"
|
|
360
|
+
) -> dict:
|
|
361
|
+
"""
|
|
362
|
+
Extract text from image using Qwen-VL through OpenRouter API.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
image_bytes: Binary image data
|
|
366
|
+
mime_type: MIME type of the image (e.g., 'image/jpeg')
|
|
367
|
+
model: OpenRouter model to use (default: qwen-2-vl-72b-instruct)
|
|
368
|
+
|
|
369
|
+
Available Qwen-VL models on OpenRouter:
|
|
370
|
+
- qwen/qwen-2-vl-72b-instruct (most capable)
|
|
371
|
+
- qwen/qwen-2-vl-7b-instruct (faster, cheaper)
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
dict with keys: success (bool), text (str), model (str), error (str)
|
|
375
|
+
"""
|
|
376
|
+
try:
|
|
377
|
+
# Get OpenRouter API key from environment
|
|
378
|
+
openrouter_api_key = os.getenv("OPENROUTER_API_KEY")
|
|
379
|
+
|
|
380
|
+
if not openrouter_api_key:
|
|
381
|
+
logger.error("OpenRouter API key not configured")
|
|
382
|
+
return {"success": False, "error": "OPENROUTER_API_KEY not configured"}
|
|
383
|
+
|
|
384
|
+
# Encode image to base64
|
|
385
|
+
image_base64 = base64.b64encode(image_bytes).decode('utf-8')
|
|
386
|
+
|
|
387
|
+
# Prepare request payload for OpenRouter
|
|
388
|
+
payload = {
|
|
389
|
+
"model": model,
|
|
390
|
+
"messages": [
|
|
391
|
+
{
|
|
392
|
+
"role": "user",
|
|
393
|
+
"content": [
|
|
394
|
+
{
|
|
395
|
+
"type": "image_url",
|
|
396
|
+
"image_url": {
|
|
397
|
+
"url": f"data:{mime_type};base64,{image_base64}"
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"type": "text",
|
|
402
|
+
"text": "Extract all text from this image. Return only the extracted text without any additional commentary or explanation."
|
|
403
|
+
}
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
]
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Make async HTTP request to OpenRouter
|
|
410
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
411
|
+
response = await client.post(
|
|
412
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
413
|
+
headers={
|
|
414
|
+
"Authorization": f"Bearer {openrouter_api_key}",
|
|
415
|
+
"Content-Type": "application/json",
|
|
416
|
+
"HTTP-Referer": os.getenv("APP_URL", "http://localhost:8000"), # Optional
|
|
417
|
+
"X-Title": os.getenv("APP_NAME", "Document Processor") # Optional
|
|
418
|
+
},
|
|
419
|
+
json=payload
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if response.status_code == 200:
|
|
423
|
+
result = response.json()
|
|
424
|
+
extracted_text = result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
"success": True,
|
|
428
|
+
"text": extracted_text.strip(),
|
|
429
|
+
"model": result.get("model"),
|
|
430
|
+
"usage": result.get("usage"),
|
|
431
|
+
"provider": "openrouter"
|
|
432
|
+
}
|
|
433
|
+
else:
|
|
434
|
+
error_detail = response.text
|
|
435
|
+
logger.error(f"OpenRouter API error: {response.status_code} - {error_detail}")
|
|
436
|
+
return {
|
|
437
|
+
"success": False,
|
|
438
|
+
"error": f"API request failed: {response.status_code} - {error_detail}"
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
except httpx.TimeoutException:
|
|
442
|
+
logger.error("OpenRouter API request timed out")
|
|
443
|
+
return {
|
|
444
|
+
"success": False,
|
|
445
|
+
"error": "Request timed out after 60 seconds"
|
|
446
|
+
}
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logger.error(f"Error in OCR processing with OpenRouter: {str(e)}")
|
|
449
|
+
return {
|
|
450
|
+
"success": False,
|
|
451
|
+
"error": str(e)
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async def list_files_by_date_helper(date: str, create_backup: bool = True):
|
|
455
|
+
"""
|
|
456
|
+
List all files uploaded on a given date with backup status.
|
|
457
|
+
Example: GET /files/2025-11-11
|
|
458
|
+
"""
|
|
459
|
+
if not S3_BUCKET:
|
|
460
|
+
logger.warning("S3 bucket is not configured; listing local files for %s", date)
|
|
461
|
+
return _list_local_files(date, create_backup=create_backup)
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
logger.info(f"Listing files for date: {date}")
|
|
465
|
+
prefix = f"uploads/{date}/"
|
|
466
|
+
response = s3_client.list_objects_v2(Bucket=S3_BUCKET, Prefix=prefix)
|
|
467
|
+
|
|
468
|
+
if "Contents" not in response:
|
|
469
|
+
logger.info(f"No files found for date: {date}")
|
|
470
|
+
local_result = _list_local_files(date, create_backup=create_backup)
|
|
471
|
+
if local_result.get("file_count", 0) > 0:
|
|
472
|
+
local_result["s3_message"] = "No files found in S3; returned local files"
|
|
473
|
+
return local_result
|
|
474
|
+
return {"date": date, "files": [], "message": "No files found for this date"}
|
|
475
|
+
|
|
476
|
+
files = []
|
|
477
|
+
backup_bucket = S3_BACKUP_BUCKET or f"{S3_BUCKET}-backup"
|
|
478
|
+
|
|
479
|
+
for obj in response["Contents"]:
|
|
480
|
+
key = obj["Key"]
|
|
481
|
+
filename = key.split("/")[-1]
|
|
482
|
+
backup_key = f"backups/{date}/{filename}"
|
|
483
|
+
|
|
484
|
+
backup_exists = False
|
|
485
|
+
try:
|
|
486
|
+
s3_client.head_object(Bucket=backup_bucket, Key=backup_key)
|
|
487
|
+
backup_exists = True
|
|
488
|
+
except ClientError:
|
|
489
|
+
backup_exists = False
|
|
490
|
+
|
|
491
|
+
if not backup_exists and create_backup:
|
|
492
|
+
try:
|
|
493
|
+
file_obj = s3_client.get_object(Bucket=S3_BUCKET, Key=key)
|
|
494
|
+
file_bytes = file_obj["Body"].read()
|
|
495
|
+
if backup_bucket:
|
|
496
|
+
s3_client.put_object(Bucket=backup_bucket, Key=backup_key, Body=file_bytes)
|
|
497
|
+
backup_exists = True
|
|
498
|
+
logger.info(f"Created backup for: {key}")
|
|
499
|
+
except Exception as e:
|
|
500
|
+
logger.error(f"Failed to upload {key} to backup: {e}")
|
|
501
|
+
|
|
502
|
+
file_info = {
|
|
503
|
+
"filename": filename,
|
|
504
|
+
"key": key,
|
|
505
|
+
"size": obj.get("Size"),
|
|
506
|
+
"last_modified": obj.get("LastModified").isoformat() if obj.get("LastModified") else None,
|
|
507
|
+
"url": f"https://{S3_BUCKET}.s3.{AWS_REGION}.amazonaws.com/{key}",
|
|
508
|
+
"backup_exists": backup_exists
|
|
509
|
+
}
|
|
510
|
+
files.append(file_info)
|
|
511
|
+
|
|
512
|
+
logger.info(f"Found {len(files)} files for date: {date}")
|
|
513
|
+
return {"date": date, "file_count": len(files), "files": files}
|
|
514
|
+
|
|
515
|
+
except (BotoCoreError, ClientError) as e:
|
|
516
|
+
logger.error("S3 list error; listing local files for %s: %s", date, e, exc_info=True)
|
|
517
|
+
result = _list_local_files(date, create_backup=create_backup)
|
|
518
|
+
result["s3_error"] = str(e)
|
|
519
|
+
return result
|
|
520
|
+
except Exception as e:
|
|
521
|
+
logger.error("Unexpected S3 list error; listing local files for %s: %s", date, e, exc_info=True)
|
|
522
|
+
result = _list_local_files(date, create_backup=create_backup)
|
|
523
|
+
result["s3_error"] = str(e)
|
|
524
|
+
return result
|
|
525
|
+
|
|
526
|
+
async def delete_file_helper(date: str, filename: str, delete_backup: bool = True):
|
|
527
|
+
"""Delete a file from S3 and optionally its backup."""
|
|
528
|
+
if not S3_BUCKET:
|
|
529
|
+
logger.warning("S3 bucket is not configured; deleting local file %s/%s", date, filename)
|
|
530
|
+
return _delete_local_file(date, filename, delete_backup=delete_backup)
|
|
531
|
+
|
|
532
|
+
s3_key = f"uploads/{date}/{filename}"
|
|
533
|
+
logger.info(f"Deleting file: {s3_key}, delete_backup: {delete_backup}")
|
|
534
|
+
|
|
535
|
+
try:
|
|
536
|
+
# Check existence
|
|
537
|
+
try:
|
|
538
|
+
s3_client.head_object(Bucket=S3_BUCKET, Key=s3_key)
|
|
539
|
+
except ClientError as e:
|
|
540
|
+
err_code = e.response.get("Error", {}).get("Code")
|
|
541
|
+
if err_code in ("404", "NoSuchKey", "NotFound"):
|
|
542
|
+
logger.error("File not found in S3; trying local delete: %s", s3_key)
|
|
543
|
+
return _delete_local_file(date, filename, delete_backup=delete_backup)
|
|
544
|
+
else:
|
|
545
|
+
raise
|
|
546
|
+
|
|
547
|
+
s3_client.delete_object(Bucket=S3_BUCKET, Key=s3_key)
|
|
548
|
+
logger.info(f"Deleted from S3: {s3_key}")
|
|
549
|
+
|
|
550
|
+
response_data = {
|
|
551
|
+
"message": "File deleted successfully",
|
|
552
|
+
"filename": filename,
|
|
553
|
+
"date": date,
|
|
554
|
+
"s3_key": s3_key,
|
|
555
|
+
"backup_deleted": False
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if delete_backup:
|
|
559
|
+
backup_bucket = S3_BACKUP_BUCKET or f"{S3_BUCKET}-backup"
|
|
560
|
+
backup_key = f"backups/{date}/{filename}"
|
|
561
|
+
try:
|
|
562
|
+
s3_client.delete_object(Bucket=backup_bucket, Key=backup_key)
|
|
563
|
+
response_data["backup_deleted"] = True
|
|
564
|
+
logger.info(f"Deleted backup: {backup_key}")
|
|
565
|
+
except ClientError as e:
|
|
566
|
+
err_code = e.response.get("Error", {}).get("Code")
|
|
567
|
+
if err_code in ("404", "NoSuchKey", "NotFound"):
|
|
568
|
+
response_data["backup_deleted"] = False
|
|
569
|
+
logger.info(f"No backup found: {backup_key}")
|
|
570
|
+
else:
|
|
571
|
+
raise
|
|
572
|
+
|
|
573
|
+
return response_data
|
|
574
|
+
|
|
575
|
+
except HTTPException:
|
|
576
|
+
raise
|
|
577
|
+
except (BotoCoreError, ClientError) as e:
|
|
578
|
+
logger.error("S3 delete error; deleting local file %s/%s: %s", date, filename, e, exc_info=True)
|
|
579
|
+
response_data = _delete_local_file(date, filename, delete_backup=delete_backup)
|
|
580
|
+
response_data["s3_error"] = str(e)
|
|
581
|
+
return response_data
|
|
582
|
+
except Exception as e:
|
|
583
|
+
logger.error("Unexpected S3 delete error; deleting local file %s/%s: %s", date, filename, e, exc_info=True)
|
|
584
|
+
response_data = _delete_local_file(date, filename, delete_backup=delete_backup)
|
|
585
|
+
response_data["s3_error"] = str(e)
|
|
586
|
+
return response_data
|
|
587
|
+
|
|
588
|
+
async def read_file_helper(date: str, filename: str, *, as_attachment: bool = True):
|
|
589
|
+
"""
|
|
590
|
+
Read or download a file from S3 given its date and filename.
|
|
591
|
+
URL format: /files/YYYY-MM-DD/<filename>
|
|
592
|
+
"""
|
|
593
|
+
if not S3_BUCKET:
|
|
594
|
+
logger.warning("S3 bucket is not configured; reading local file %s/%s", date, filename)
|
|
595
|
+
return _read_local_file(date, filename, as_attachment=as_attachment)
|
|
596
|
+
|
|
597
|
+
s3_key = f"uploads/{date}/{filename}"
|
|
598
|
+
logger.info(f"Reading file: {s3_key}")
|
|
599
|
+
|
|
600
|
+
try:
|
|
601
|
+
file_obj = s3_client.get_object(Bucket=S3_BUCKET, Key=s3_key)
|
|
602
|
+
file_content = file_obj["Body"].read()
|
|
603
|
+
|
|
604
|
+
content_type, _ = guess_type(filename)
|
|
605
|
+
if not content_type:
|
|
606
|
+
content_type = "application/octet-stream"
|
|
607
|
+
|
|
608
|
+
logger.info(f"Successfully read file: {s3_key} ({len(file_content)} bytes)")
|
|
609
|
+
headers = _download_headers(filename) if as_attachment else {"X-Content-Type-Options": "nosniff"}
|
|
610
|
+
return Response(content=file_content, media_type=content_type, headers=headers)
|
|
611
|
+
|
|
612
|
+
except ClientError as e:
|
|
613
|
+
err_code = e.response.get("Error", {}).get("Code")
|
|
614
|
+
if err_code in ("404", "NoSuchKey", "NotFound"):
|
|
615
|
+
logger.error("File not found in S3; trying local fallback: %s", s3_key)
|
|
616
|
+
return _read_local_file(date, filename, as_attachment=as_attachment)
|
|
617
|
+
logger.error("S3 read ClientError; trying local fallback: %s", e, exc_info=True)
|
|
618
|
+
return _read_local_file(date, filename, as_attachment=as_attachment)
|
|
619
|
+
except (BotoCoreError, Exception) as e:
|
|
620
|
+
logger.error("S3 read error; trying local fallback: %s", e, exc_info=True)
|
|
621
|
+
return _read_local_file(date, filename, as_attachment=as_attachment)
|
|
622
|
+
|
|
623
|
+
async def delete_folder_helper(
|
|
624
|
+
date: str,
|
|
625
|
+
delete_backup: bool = True,
|
|
626
|
+
background_tasks: BackgroundTasks = None
|
|
627
|
+
):
|
|
628
|
+
"""Delete all files for a specific date (entire folder)."""
|
|
629
|
+
if not S3_BUCKET:
|
|
630
|
+
logger.warning("S3 bucket is not configured; deleting local folder for %s", date)
|
|
631
|
+
return _delete_local_folder(date, delete_backup=delete_backup)
|
|
632
|
+
|
|
633
|
+
try:
|
|
634
|
+
logger.info(f"Deleting folder for date: {date}, delete_backup: {delete_backup}")
|
|
635
|
+
prefix = f"uploads/{date}/"
|
|
636
|
+
response = s3_client.list_objects_v2(Bucket=S3_BUCKET, Prefix=prefix)
|
|
637
|
+
|
|
638
|
+
if "Contents" not in response:
|
|
639
|
+
logger.error(f"No files found for date: {date}")
|
|
640
|
+
local_result = _delete_local_folder(date, delete_backup=delete_backup)
|
|
641
|
+
local_result["s3_message"] = "No files found in S3; deleted local folder"
|
|
642
|
+
return local_result
|
|
643
|
+
|
|
644
|
+
objects_to_delete = [{"Key": obj["Key"]} for obj in response["Contents"]]
|
|
645
|
+
deleted_count = len(objects_to_delete)
|
|
646
|
+
|
|
647
|
+
if objects_to_delete:
|
|
648
|
+
batch_size = 1000
|
|
649
|
+
for i in range(0, len(objects_to_delete), batch_size):
|
|
650
|
+
batch = objects_to_delete[i:i + batch_size]
|
|
651
|
+
s3_client.delete_objects(Bucket=S3_BUCKET, Delete={"Objects": batch})
|
|
652
|
+
|
|
653
|
+
logger.info(f"Deleted {deleted_count} files for date: {date}")
|
|
654
|
+
|
|
655
|
+
response_data = {
|
|
656
|
+
"message": f"Deleted {deleted_count} files for date {date}",
|
|
657
|
+
"date": date,
|
|
658
|
+
"deleted_count": deleted_count,
|
|
659
|
+
"backup_deleted": False
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if delete_backup and background_tasks:
|
|
663
|
+
background_tasks.add_task(delete_backup_folder, date, deleted_count)
|
|
664
|
+
response_data["backup_deletion"] = "scheduled"
|
|
665
|
+
logger.info(f"Scheduled backup deletion for date: {date}")
|
|
666
|
+
|
|
667
|
+
return response_data
|
|
668
|
+
|
|
669
|
+
except HTTPException:
|
|
670
|
+
raise
|
|
671
|
+
except (BotoCoreError, ClientError) as e:
|
|
672
|
+
logger.error("S3 folder delete error; deleting local folder for %s: %s", date, e, exc_info=True)
|
|
673
|
+
response_data = _delete_local_folder(date, delete_backup=delete_backup)
|
|
674
|
+
response_data["s3_error"] = str(e)
|
|
675
|
+
return response_data
|
|
676
|
+
except Exception as e:
|
|
677
|
+
logger.error("Unexpected S3 folder delete error; deleting local folder for %s: %s", date, e, exc_info=True)
|
|
678
|
+
response_data = _delete_local_folder(date, delete_backup=delete_backup)
|
|
679
|
+
response_data["s3_error"] = str(e)
|
|
680
|
+
return response_data
|
|
681
|
+
|
|
682
|
+
async def send_file_to_bridge(
|
|
683
|
+
file_path: str,
|
|
684
|
+
original_filename: Optional[str] = None,
|
|
685
|
+
ws_uri: Optional[str] = None,
|
|
686
|
+
timeout: float = 30.0,
|
|
687
|
+
send_as_binary: bool = False
|
|
688
|
+
) -> Dict[str, Any]:
|
|
689
|
+
"""
|
|
690
|
+
Send a file to the WebSocket-to-NATS bridge.
|
|
691
|
+
|
|
692
|
+
Args:
|
|
693
|
+
file_path: Path to the file to send
|
|
694
|
+
original_filename: Original filename to preserve (optional)
|
|
695
|
+
ws_uri: WebSocket URI (default: ws://websocket-server:6789)
|
|
696
|
+
Use 'ws://localhost:6789' for local testing
|
|
697
|
+
timeout: Connection and send timeout in seconds
|
|
698
|
+
send_as_binary: If True, send as binary frame; if False, send as base64 JSON
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
Dict with server response, e.g.:
|
|
702
|
+
{
|
|
703
|
+
"status": "queued",
|
|
704
|
+
"len": 12345,
|
|
705
|
+
"saved_file": "invoice.pdf"
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
Raises:
|
|
709
|
+
FileNotFoundError: If file doesn't exist
|
|
710
|
+
websockets.exceptions.WebSocketException: On connection/send errors
|
|
711
|
+
asyncio.TimeoutError: If operation times out
|
|
712
|
+
json.JSONDecodeError: If server response is invalid
|
|
713
|
+
"""
|
|
714
|
+
# Default to Docker service name for container environments
|
|
715
|
+
if ws_uri is None:
|
|
716
|
+
ws_uri = os.getenv("WEBSOCKET_BRIDGE_URI", "ws://websocket-server:6789")
|
|
717
|
+
|
|
718
|
+
# Validate file exists
|
|
719
|
+
if not os.path.isfile(file_path):
|
|
720
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
721
|
+
|
|
722
|
+
# FIX 4: Use original filename if provided, otherwise use basename
|
|
723
|
+
file_name = original_filename if original_filename else os.path.basename(file_path)
|
|
724
|
+
file_size = os.path.getsize(file_path)
|
|
725
|
+
|
|
726
|
+
logger.info("Sending file to WebSocket bridge: %s (%d bytes)", file_name, file_size)
|
|
727
|
+
logger.debug("WebSocket URI: %s", ws_uri)
|
|
728
|
+
logger.debug("Send mode: %s", "binary" if send_as_binary else "base64-JSON")
|
|
729
|
+
|
|
730
|
+
try:
|
|
731
|
+
# Connect to WebSocket server with timeout
|
|
732
|
+
async with websockets.connect(ws_uri, open_timeout=timeout) as websocket:
|
|
733
|
+
logger.debug("✓ Connected to WebSocket server")
|
|
734
|
+
|
|
735
|
+
# Read file contents
|
|
736
|
+
with open(file_path, "rb") as f:
|
|
737
|
+
file_data = f.read()
|
|
738
|
+
|
|
739
|
+
logger.info(f"Read {len(file_data)} bytes from file")
|
|
740
|
+
|
|
741
|
+
# Send file based on mode
|
|
742
|
+
if send_as_binary:
|
|
743
|
+
# Send as raw binary frame
|
|
744
|
+
logger.debug("Sending binary frame...")
|
|
745
|
+
await websocket.send(file_data)
|
|
746
|
+
|
|
747
|
+
else:
|
|
748
|
+
# Send as base64-encoded JSON
|
|
749
|
+
logger.debug("Encoding file as base64...")
|
|
750
|
+
encoded_data = base64.b64encode(file_data).decode('utf-8')
|
|
751
|
+
|
|
752
|
+
payload = {
|
|
753
|
+
"fileName": file_name,
|
|
754
|
+
"data": encoded_data
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
logger.debug("Sending JSON payload with fileName: %s", file_name)
|
|
758
|
+
await websocket.send(json.dumps(payload))
|
|
759
|
+
|
|
760
|
+
logger.debug("Waiting for server acknowledgment...")
|
|
761
|
+
response = {
|
|
762
|
+
"status": "queued",
|
|
763
|
+
"note": "File forwarded to bridge (no ACK expected)"
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
# Parse response
|
|
768
|
+
if isinstance(response, str):
|
|
769
|
+
try:
|
|
770
|
+
ack = json.loads(response)
|
|
771
|
+
logger.debug("Server response JSON: %s", ack)
|
|
772
|
+
except json.JSONDecodeError as e:
|
|
773
|
+
# FIX 6: Treat non-JSON text responses as successful acknowledgment
|
|
774
|
+
# Many WebSocket servers return simple text confirmations like "OK" or server info
|
|
775
|
+
logger.info(f"WebSocket server response (non-JSON): {response[:200]}")
|
|
776
|
+
ack = {
|
|
777
|
+
"status": "queued", # Treat as success if we got a response
|
|
778
|
+
"server_response": str(response)[:200],
|
|
779
|
+
"note": "Server returned text instead of JSON - treating as acknowledgment"
|
|
780
|
+
}
|
|
781
|
+
else:
|
|
782
|
+
# FIX 5: Handle binary responses - also treat as success
|
|
783
|
+
logger.info(f"WebSocket server sent binary response ({len(response)} bytes)")
|
|
784
|
+
ack = {
|
|
785
|
+
"status": "queued", # Treat as success
|
|
786
|
+
"binary_response_size": len(response),
|
|
787
|
+
"note": "Server returned binary response - treating as acknowledgment"
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
# Check response status
|
|
791
|
+
if ack.get("status") == "queued":
|
|
792
|
+
logger.info("✓ File queued successfully: %s", file_name)
|
|
793
|
+
elif ack.get("status") == "nack":
|
|
794
|
+
reason = ack.get("reason", "unknown")
|
|
795
|
+
logger.warning("✗ File rejected by server: %s", reason)
|
|
796
|
+
else:
|
|
797
|
+
logger.warning("⚠ Unexpected server response status: %s", ack.get("status"))
|
|
798
|
+
|
|
799
|
+
return ack
|
|
800
|
+
|
|
801
|
+
except websockets.exceptions.InvalidURI as e:
|
|
802
|
+
logger.error("Invalid WebSocket URI: %s", ws_uri)
|
|
803
|
+
raise
|
|
804
|
+
|
|
805
|
+
except websockets.exceptions.WebSocketException as e:
|
|
806
|
+
logger.error("WebSocket error: %s", e)
|
|
807
|
+
raise
|
|
808
|
+
|
|
809
|
+
except Exception as e:
|
|
810
|
+
logger.error("Unexpected error sending file: %s", e, exc_info=True)
|
|
811
|
+
raise
|
|
812
|
+
|
|
813
|
+
async def send_text(text):
|
|
814
|
+
payload = {
|
|
815
|
+
"type": "text",
|
|
816
|
+
"message": text
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async with websockets.connect(FORWARD_URL) as ws:
|
|
820
|
+
print("📡 Connected (text mode)")
|
|
821
|
+
await ws.send(json.dumps(payload))
|
|
822
|
+
print("💬 Message sent — closing connection")
|
|
823
|
+
|
|
824
|
+
await asyncio.sleep(0.1)
|
|
825
|
+
await ws.close()
|