diff --git a/src/oci-jms-mcp-server/DETAILS_OF_TOOLS.md b/src/oci-jms-mcp-server/DETAILS_OF_TOOLS.md new file mode 100644 index 00000000..7d9e16ff --- /dev/null +++ b/src/oci-jms-mcp-server/DETAILS_OF_TOOLS.md @@ -0,0 +1,796 @@ +# JMS Tool Details + +This file documents the 13 tools exposed by `oracle.oci-jms-mcp-server`. + +Use this as a quick reference for: + +- tool input parameters +- expected output shape +- demo MCP/agent queries + +The source of truth is: + +- `src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/server.py` +- `src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/models.py` + +## General Notes + +- Do not send extra keys such as `task_progress` to the tool call payload. +- Enum-like inputs are accepted in flexible form by the wrapper, but the safest values are the OCI SDK values shown below. +- `sort_order` values should be `ASC` or `DESC`. +- Timestamp inputs must be RFC3339 strings. +- For `get_fleet`, prefer using a fleet OCID returned by `list_fleets`. + +--- + +## 1. `list_fleets` + +Lists JMS fleets in a compartment. + +### Inputs + +```json +{ + "compartment_id": "string", + "id": "string", + "lifecycle_state": "ACTIVE|CREATING|DELETED|DELETING|FAILED|NEEDS_ATTENTION|UPDATING", + "display_name": "string", + "display_name_contains": "string", + "limit": 50, + "sort_order": "ASC|DESC", + "sort_by": "displayName|timeCreated" +} +``` + +### Output + +Returns a list of `FleetSummary` objects: + +```json +[ + { + "id": "ocid1.jmsfleet...", + "display_name": "Test Fleet", + "description": "Fleet description", + "compartment_id": "ocid1.compartment...", + "approximate_jre_count": 0, + "approximate_installation_count": 0, + "approximate_application_count": 0, + "approximate_managed_instance_count": 0, + "approximate_java_server_count": 0, + "approximate_library_count": 0, + "approximate_library_vulnerability_count": 0, + "inventory_log": { + "namespace": null, + "bucket_name": null, + "object_name": null, + "log_group_id": "ocid1.loggroup...", + "log_id": "ocid1.log..." + }, + "operation_log": { + "namespace": null, + "bucket_name": null, + "object_name": null, + "log_group_id": "ocid1.loggroup...", + "log_id": "ocid1.log..." + }, + "is_advanced_features_enabled": true, + "is_export_setting_enabled": false, + "time_created": "2026-02-10T09:27:25.338000Z", + "lifecycle_state": "FAILED", + "defined_tags": {}, + "freeform_tags": {}, + "system_tags": {} + } +] +``` + +### Demo Query + +```text +Call the `list_fleets` tool with: +{ + "compartment_id": "ocid1.compartment.oc1..example", + "sort_order": "ASC", + "sort_by": "displayName", + "limit": 50 +} +``` + +### Demo Query For Failed Fleets + +```text +Call the `list_fleets` tool with: +{ + "compartment_id": "ocid1.compartment.oc1..example", + "lifecycle_state": "FAILED", + "sort_order": "DESC", + "sort_by": "timeCreated", + "limit": 20 +} +``` + +--- + +## 2. `get_fleet` + +Gets one JMS fleet by OCID. + +### Inputs + +```json +{ + "fleet_id": "ocid1.jmsfleet..." +} +``` + +### Output + +Returns a `Fleet` object. It has the same shape as `FleetSummary` in this server. + +### Demo Query + +```text +Call the `get_fleet` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example" +} +``` + +--- + +## 3. `list_jms_plugins` + +Lists JMS plugins for a compartment or fleet. + +### Inputs + +```json +{ + "compartment_id": "string", + "compartment_id_in_subtree": false, + "id": "string", + "fleet_id": "string", + "agent_id": "string", + "lifecycle_state": "ACTIVE|INACTIVE|NEEDS_ATTENTION|DELETED", + "availability_status": "ACTIVE|SILENT|NOT_AVAILABLE", + "agent_type": "OMA|OCA", + "time_registered_less_than_or_equal_to": "2026-03-01T00:00:00Z", + "time_last_seen_less_than_or_equal_to": "2026-03-01T00:00:00Z", + "hostname_contains": "host", + "limit": 50, + "sort_order": "ASC|DESC", + "sort_by": "id|timeLastSeen|timeRegistered|hostname|agentId|agentType|lifecycleState|availabilityStatus|fleetId|compartmentId|osFamily|osArchitecture|osDistribution|pluginVersion" +} +``` + +### Output + +Returns a list of `JmsPluginSummary` objects: + +```json +[ + { + "id": "ocid1.jmsplugin...", + "agent_id": "ocid1.managementagent...", + "agent_type": "OMA", + "lifecycle_state": "ACTIVE", + "availability_status": "ACTIVE", + "fleet_id": "ocid1.jmsfleet...", + "compartment_id": "ocid1.compartment...", + "hostname": "host1", + "os_family": "LINUX", + "os_architecture": "x86_64", + "os_distribution": "Oracle Linux", + "plugin_version": "string", + "time_registered": "2026-03-01T00:00:00Z", + "time_last_seen": "2026-03-10T00:00:00Z", + "defined_tags": {}, + "freeform_tags": {}, + "system_tags": {} + } +] +``` + +### Demo Query + +```text +Call the `list_jms_plugins` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example", + "sort_order": "DESC", + "sort_by": "timeLastSeen", + "limit": 25 +} +``` + +--- + +## 4. `get_jms_plugin` + +Gets one JMS plugin by OCID. + +### Inputs + +```json +{ + "jms_plugin_id": "ocid1.jmsplugin..." +} +``` + +### Output + +Returns a `JmsPlugin` object. In this server it has the same shape as `JmsPluginSummary`. + +### Demo Query + +```text +Call the `get_jms_plugin` tool with: +{ + "jms_plugin_id": "ocid1.jmsplugin.oc1.iad.example" +} +``` + +--- + +## 5. `list_installation_sites` + +Lists Java installation sites for a fleet. + +### Inputs + +```json +{ + "fleet_id": "ocid1.jmsfleet...", + "jre_vendor": "string", + "jre_distribution": "string", + "jre_version": "string", + "installation_path": "string", + "application_id": "string", + "managed_instance_id": "string", + "os_family": ["LINUX", "WINDOWS", "MACOS", "UNKNOWN"], + "jre_security_status": "EARLY_ACCESS|UNKNOWN|UP_TO_DATE|UPDATE_REQUIRED|UPGRADE_REQUIRED", + "path_contains": "string", + "time_start": "2026-03-01T00:00:00Z", + "time_end": "2026-03-10T00:00:00Z", + "limit": 50, + "sort_order": "ASC|DESC", + "sort_by": "managedInstanceId|jreDistribution|jreVendor|jreVersion|path|approximateApplicationCount|osName|securityStatus" +} +``` + +### Output + +Returns a list of `InstallationSiteSummary` objects: + +```json +[ + { + "installation_key": "string", + "managed_instance_id": "ocid1.instance...", + "jre": { + "version": "17", + "vendor": "Oracle", + "distribution": "JDK", + "jre_key": "string" + }, + "security_status": "UP_TO_DATE", + "path": "/usr/java", + "operating_system": { + "family": "LINUX", + "name": "Oracle Linux", + "distribution": "Oracle Linux", + "version": "9", + "architecture": "x86_64", + "managed_instance_count": null, + "container_count": null + }, + "approximate_application_count": 0, + "time_last_seen": "2026-03-10T00:00:00Z", + "blocklist": [], + "lifecycle_state": "ACTIVE" + } +] +``` + +### Demo Query + +```text +Call the `list_installation_sites` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example", + "os_family": ["LINUX"], + "sort_order": "ASC", + "sort_by": "managedInstanceId", + "limit": 20 +} +``` + +--- + +## 6. `get_fleet_agent_configuration` + +Gets fleet-level agent configuration. + +### Inputs + +```json +{ + "fleet_id": "ocid1.jmsfleet..." +} +``` + +### Output + +Returns a `FleetAgentConfiguration` object: + +```json +{ + "jre_scan_frequency_in_minutes": 60, + "java_usage_tracker_processing_frequency_in_minutes": 15, + "work_request_validity_period_in_days": 7, + "agent_polling_interval_in_minutes": 5, + "is_collecting_managed_instance_metrics_enabled": true, + "is_collecting_usernames_enabled": false, + "is_capturing_ip_address_and_fqdn_enabled": false, + "is_libraries_scan_enabled": false, + "linux_configuration": { + "include_paths": ["/usr/java"], + "exclude_paths": ["/tmp"] + }, + "windows_configuration": null, + "mac_os_configuration": null, + "time_last_modified": "2026-03-01T00:00:00Z" +} +``` + +### Demo Query + +```text +Call the `get_fleet_agent_configuration` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example" +} +``` + +--- + +## 7. `get_fleet_advanced_feature_configuration` + +Gets advanced feature configuration for a fleet. + +### Inputs + +```json +{ + "fleet_id": "ocid1.jmsfleet..." +} +``` + +### Output + +Returns a `FleetAdvancedFeatureConfiguration` object: + +```json +{ + "analytic_namespace": "string", + "analytic_bucket_name": "string", + "lcm": {}, + "crypto_event_analysis": {}, + "advanced_usage_tracking": {}, + "jfr_recording": {}, + "performance_tuning_analysis": {}, + "java_migration_analysis": {}, + "time_last_modified": "2026-03-01T00:00:00Z" +} +``` + +### Demo Query + +```text +Call the `get_fleet_advanced_feature_configuration` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example" +} +``` + +--- + +## 8. `summarize_resource_inventory` + +Summarizes high-level JMS inventory counts for a compartment. + +### Inputs + +```json +{ + "compartment_id": "ocid1.compartment...", + "compartment_id_in_subtree": false, + "time_start": "2026-03-01T00:00:00Z", + "time_end": "2026-03-10T00:00:00Z" +} +``` + +### Output + +Returns a `ResourceInventory` object: + +```json +{ + "active_fleet_count": 1, + "managed_instance_count": 2, + "jre_count": 3, + "installation_count": 4, + "application_count": 5 +} +``` + +### Demo Query + +```text +Call the `summarize_resource_inventory` tool with: +{ + "compartment_id": "ocid1.compartment.oc1..example", + "compartment_id_in_subtree": false +} +``` + +--- + +## 9. `summarize_managed_instance_usage` + +Summarizes managed instance usage records for a fleet. + +### Inputs + +```json +{ + "fleet_id": "ocid1.jmsfleet...", + "managed_instance_id": "string", + "managed_instance_type": "ORACLE_MANAGEMENT_AGENT|ORACLE_CLOUD_AGENT", + "jre_vendor": "string", + "jre_distribution": "string", + "jre_version": "string", + "installation_path": "string", + "application_id": "string", + "fields": ["approximateJreCount", "approximateInstallationCount", "approximateApplicationCount"], + "time_start": "2026-03-01T00:00:00Z", + "time_end": "2026-03-10T00:00:00Z", + "limit": 50, + "sort_order": "ASC|DESC", + "sort_by": "timeFirstSeen|timeLastSeen|approximateJreCount|approximateInstallationCount|approximateApplicationCount|osName", + "os_family": ["LINUX", "WINDOWS", "MACOS", "UNKNOWN"], + "hostname_contains": "string", + "library_key": "string" +} +``` + +### Output + +Returns a list of `ManagedInstanceUsage` objects: + +```json +[ + { + "managed_instance_id": "ocid1.instance...", + "managed_instance_type": "ORACLE_MANAGEMENT_AGENT", + "hostname": "host1", + "host_id": "string", + "ip_addresses": ["10.0.0.10"], + "hostnames": ["host1"], + "fqdns": ["host1.example.internal"], + "operating_system": { + "family": "LINUX", + "name": "Oracle Linux", + "distribution": "Oracle Linux", + "version": "9", + "architecture": "x86_64", + "managed_instance_count": null, + "container_count": null + }, + "agent": {}, + "cluster_details": {}, + "approximate_application_count": 0, + "approximate_installation_count": 0, + "approximate_jre_count": 0, + "drs_file_status": "PRESENT", + "application_invoked_by": "oracle", + "time_start": "2026-03-01T00:00:00Z", + "time_end": "2026-03-10T00:00:00Z", + "time_first_seen": "2026-03-01T00:00:00Z", + "time_last_seen": "2026-03-10T00:00:00Z" + } +] +``` + +### Demo Query + +```text +Call the `summarize_managed_instance_usage` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example", + "fields": ["approximateJreCount"], + "sort_order": "DESC", + "sort_by": "timeLastSeen", + "limit": 25 +} +``` + +--- + +## 10. `summarize_fleet_health` + +Summarizes fleet health using fleet diagnoses and fleet errors. + +### Inputs + +```json +{ + "fleet_id": "ocid1.jmsfleet..." +} +``` + +### Output + +Returns a `FleetHealthSummary` object: + +```json +{ + "fleet_id": "ocid1.jmsfleet...", + "diagnosis_count": 2, + "fleet_errors": [ + { + "fleet_id": "ocid1.jmsfleet...", + "fleet_name": "Test Fleet", + "errors": [ + { + "reason": "Agent connectivity failure", + "details": "Critical agent reporting failure", + "time_last_seen": "2026-03-18T12:00:00Z" + } + ] + } + ], + "top_issue_categories": ["Inventory scan issue", "Agent connectivity failure"], + "overall_health_status": "CRITICAL", + "recommended_next_checks": [ + "Review fleet agent configuration and inventory collection settings.", + "Check JMS notices for any known service-side issues or advisories." + ] +} +``` + +### Demo Query + +```text +Call the `summarize_fleet_health` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example" +} +``` + +--- + +## 11. `get_fleet_health_diagnostics` + +Gets detailed fleet diagnoses and fleet errors for drill-down troubleshooting. + +### Inputs + +```json +{ + "fleet_id": "ocid1.jmsfleet..." +} +``` + +### Output + +Returns a `FleetHealthDiagnostics` object: + +```json +{ + "fleet_id": "ocid1.jmsfleet...", + "diagnoses": [ + { + "resource_id": "ocid1.jmsfleet...", + "resource_diagnosis": "Inventory scan issue", + "resource_state": "FAILED", + "resource_type": "JMS_FLEET" + } + ], + "fleet_errors": [ + { + "fleet_id": "ocid1.jmsfleet...", + "fleet_name": "Test Fleet", + "errors": [ + { + "reason": "Agent connectivity failure", + "details": "Critical agent reporting failure", + "time_last_seen": "2026-03-18T12:00:00Z" + } + ] + } + ], + "diagnosis_count": 1, + "fleet_error_count": 1 +} +``` + +### Demo Query + +```text +Call the `get_fleet_health_diagnostics` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example" +} +``` + +--- + +## 12. `list_jms_notices` + +Lists JMS announcements and notices. + +### Inputs + +```json +{ + "summary_contains": "maintenance", + "time_start": "2026-03-01T00:00:00Z", + "time_end": "2026-03-10T00:00:00Z", + "limit": 10, + "sort_order": "DESC", + "sort_by": "timeReleased" +} +``` + +### Output + +Returns a list of `JmsNotice` objects: + +```json +[ + { + "key": "announcement-1", + "summary": "Planned JMS maintenance", + "time_released": "2026-03-18T12:00:00Z", + "url": "https://example.com" + } +] +``` + +### Demo Query + +```text +Call the `list_jms_notices` tool with: +{ + "summary_contains": "maintenance", + "sort_order": "DESC", + "sort_by": "timeReleased", + "limit": 10 +} +``` + +--- + +## 13. `java_runtime_compliance` + +Summarizes Java runtime compliance for a JMS fleet. + +### Inputs + +```json +{ + "fleet_id": "ocid1.jmsfleet..." +} +``` + +### Output + +Returns a `JavaRuntimeComplianceReport` object: + +```json +{ + "fleet_id": "ocid1.jmsfleet...", + "total_runtimes_in_fleet": 7, + "up_to_date_runtimes": 4, + "runtimes_requiring_update": 2, + "runtimes_requiring_upgrade": 1, + "unknown_runtimes": 0, + "version_breakdown": [ + { + "version": "21.0.2", + "vendor": "Oracle", + "distribution": "JDK", + "security_status": "UP_TO_DATE", + "runtime_count": 4, + "release_type": "CPU", + "license_type": "NFTC" + }, + { + "version": "17.0.10", + "vendor": "Oracle", + "distribution": "JDK", + "security_status": "UPDATE_REQUIRED", + "runtime_count": 3, + "release_type": "CPU", + "license_type": "NFTC" + } + ], + "vendor_breakdown": [ + { + "key": "Oracle", + "runtime_count": 7 + } + ], + "distribution_breakdown": [ + { + "key": "JDK", + "runtime_count": 7 + } + ], + "outdated_installations": [ + { + "installation_key": "install1", + "managed_instance_id": "mi1", + "path": "/usr/java/jdk-17", + "version": "17.0.10", + "vendor": "Oracle", + "distribution": "JDK", + "security_status": "UPDATE_REQUIRED" + } + ] +} +``` + +### Demo Query + +```text +Call the `java_runtime_compliance` tool with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example" +} +``` + +--- + +## Suggested Agent Workflow + +### Find a fleet, then inspect it + +```text +1. Call `list_fleets` with: +{ + "compartment_id": "ocid1.compartment.oc1..example", + "sort_order": "ASC", + "sort_by": "displayName", + "limit": 50 +} +2. Take one returned fleet id. +3. Call `get_fleet` with that fleet id. +``` + +### Find plugins for a fleet + +```text +1. Call `list_jms_plugins` with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example", + "sort_order": "DESC", + "sort_by": "timeLastSeen", + "limit": 25 +} +``` + +### Find installation sites for a fleet + +```text +1. Call `list_installation_sites` with: +{ + "fleet_id": "ocid1.jmsfleet.oc1.iad.example", + "os_family": ["LINUX"], + "sort_order": "ASC", + "sort_by": "managedInstanceId", + "limit": 20 +} +``` diff --git a/src/oci-jms-mcp-server/README.md b/src/oci-jms-mcp-server/README.md new file mode 100644 index 00000000..321968ae --- /dev/null +++ b/src/oci-jms-mcp-server/README.md @@ -0,0 +1,155 @@ +# OCI JMS MCP Server + +## Overview + +This server provides tools to interact with Oracle Cloud Infrastructure Java Management Service (JMS). +It focuses on fleet inventory, discovery, and health troubleshooting workflows. +It runs over stdio only. + +## Authentication + +This server expects OCI session-token auth through a normal OCI CLI config file. + +- Use `OCI_CONFIG_FILE` to point at the OCI config file +- Use `OCI_CONFIG_PROFILE` to select the profile +- The configured profile must include a valid `security_token_file` +- Refresh expired sessions with `oci session authenticate ...` + +This server does **not** use the env names from the separate `oci_api_mcp` project. + + +- Use `OCI_CONFIG_FILE`, not `OCI_CONFIG` +- Use `OCI_CONFIG_PROFILE`, not `OCI_PROFILE` + +## JMS Environment Endpoint Selection + +By default, this server lets the OCI Python SDK derive the production JMS endpoint from the OCI config region. + +For dev and other non-prod JMS environments, set `JMS_TEST_ENVIRONMENT` to derive the service endpoint using the same environment naming pattern used by JMS Java tooling. + +Supported base values: + +- `PROD` +- `HERDS` +- `DEV` +- `DEV2` +- `DEV3` +- `TEST` +- `TEST2` +- `TEST3` +- `STAGE` +- `VANILLA` + +Optional suffixes are allowed and ignored for endpoint derivation, for example `DEV-canary` or `DEV_canary`. + +Examples: + +- `JMS_TEST_ENVIRONMENT=DEV` -> `https://javamanagement-dev..oci.oc-test.com` +- `JMS_TEST_ENVIRONMENT=TEST` -> `https://javamanagement-test..oci.oc-test.com` +- `JMS_TEST_ENVIRONMENT=TEST2` -> `https://javamanagement-test2..oci.oc-test.com` +- `JMS_TEST_ENVIRONMENT=STAGE` -> `https://javamanagement-stage..oci.oc-test.com` +- `JMS_TEST_ENVIRONMENT=HERDS` -> `https://javamanagement-herds..oci.rbcloud.oc-test.com` + +When `JMS_TEST_ENVIRONMENT=PROD` or the variable is unset, the server keeps the default SDK-derived production endpoint. + +## Running the server + +From the JMS package directory: + +```sh +cd src/oci-jms-mcp-server +mkdir -p /tmp/uv-cache +UV_CACHE_DIR=/tmp/uv-cache uv sync +``` + +### STDIO transport mode + +```sh +OCI_CONFIG_FILE=~/.oci/config OCI_CONFIG_PROFILE=DEFAULT UV_CACHE_DIR=/tmp/uv-cache uv run oracle.oci-jms-mcp-server +``` + +Non-prod example: + +```sh +OCI_CONFIG_FILE=~/.oci/config OCI_CONFIG_PROFILE=DEFAULT JMS_TEST_ENVIRONMENT=DEV UV_CACHE_DIR=/tmp/uv-cache uv run oracle.oci-jms-mcp-server +``` + +## MCP Client Configuration + +### Cline / stdio + +```json +{ + "mcpServers": { + "oracle-oci-jms-mcp-server": { + "type": "stdio", + "command": "/bin/zsh", + "args": [ + "-lc", + "cd src/oci-jms-mcp-server && OCI_CONFIG_FILE=~/.oci/config OCI_CONFIG_PROFILE=DEFAULT UV_CACHE_DIR=/tmp/uv-cache uv run oracle.oci-jms-mcp-server" + ], + "env": { + "FASTMCP_LOG_LEVEL": "ERROR" + } + } + } +} +``` + +## Verification + +Verify the server through an MCP agent in this order: + +1. List the tools from `oracle-oci-jms-mcp-server` +2. Call `list_fleets` for a known compartment +3. Use one returned fleet OCID in `get_fleet` + +Prefer verifying `get_fleet` only with an OCID returned by `list_fleets`. If `get_fleet` fails with `NotAuthorizedOrNotFound`, first confirm the fleet is visible to the active profile. + +## Troubleshooting + +- If `uv` fails with `Failed to initialize cache`, create a writable cache directory and run with `UV_CACHE_DIR=/tmp/uv-cache`. +- If `get_fleet` returns `NotAuthorizedOrNotFound`, verify: + - the active `OCI_CONFIG_PROFILE` + - the region/tenancy for the fleet OCID + - IAM permission to read JMS fleets + - the fleet exists by first calling `list_fleets` +- If your client config was copied from `oci_api_mcp`, replace `OCI_CONFIG`/`OCI_PROFILE` with `OCI_CONFIG_FILE`/`OCI_CONFIG_PROFILE`. +- If you set `JMS_TEST_ENVIRONMENT`, make sure the OCI config contains a `region`. +- If you need a dev endpoint, set `JMS_TEST_ENVIRONMENT`; tool inputs do not accept a per-call endpoint override. +- Blank optional filter values are ignored. For example, empty `time_start`, `time_end`, or blank entries in `os_family` are treated as unset inputs instead of being sent to the OCI SDK. + +## Tools + +| Tool Name | Description | +| --- | --- | +| list_fleets | List JMS fleets in a compartment | +| get_fleet | Get a JMS fleet by OCID | +| list_jms_plugins | List JMS plugins in a compartment or fleet | +| get_jms_plugin | Get a JMS plugin by OCID | +| list_installation_sites | List Java installation sites in a JMS fleet | +| get_fleet_agent_configuration | Get fleet agent configuration | +| get_fleet_advanced_feature_configuration | Get fleet advanced feature configuration | +| summarize_resource_inventory | Summarize JMS resource inventory | +| summarize_managed_instance_usage | Summarize managed instance usage in a fleet | +| summarize_fleet_health | Summarize fleet health using diagnoses and fleet errors | +| get_fleet_health_diagnostics | Get detailed fleet health diagnoses and fleet errors | +| list_jms_notices | List JMS announcements and notices | +| java_runtime_compliance | Summarize Java runtime compliance for a fleet | + +⚠️ **NOTE**: All actions are performed with the permissions of the configured OCI CLI profile. We advise least-privilege IAM setup, secure credential management, safe network practices, secure logging, and warn against exposing secrets. + +## Third-Party APIs + +Developers choosing to distribute a binary implementation of this project are responsible for obtaining and providing all required licenses and copyright notices for the third-party code used in order to ensure compliance with their respective open source licenses. + +## Disclaimer + +Users are responsible for their local environment and credential safety. Different language model selections may yield different results and performance. + +## License + +Copyright (c) 2025 Oracle and/or its affiliates. + +Released under the Universal Permissive License v1.0 as shown at +. diff --git a/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/__init__.py b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/__init__.py new file mode 100644 index 00000000..35ca2659 --- /dev/null +++ b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/__init__.py @@ -0,0 +1,8 @@ +""" +Copyright (c) 2025, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +__project__ = "oracle.oci-jms-mcp-server" +__version__ = "1.0.0" diff --git a/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/models.py b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/models.py new file mode 100644 index 00000000..80ad4b61 --- /dev/null +++ b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/models.py @@ -0,0 +1,735 @@ +""" +Copyright (c) 2025, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +from __future__ import annotations + +from datetime import datetime +from typing import Any, Dict, List, Literal, Optional + +import oci +from pydantic import BaseModel, Field + + +def _oci_to_dict(obj): + """Best-effort conversion of OCI SDK objects into plain dictionaries.""" + if obj is None: + return None + try: + from oci.util import to_dict as oci_to_dict + + return oci_to_dict(obj) + except Exception: + pass + if isinstance(obj, dict): + return obj + if hasattr(obj, "__dict__"): + return {k: v for k, v in obj.__dict__.items() if not k.startswith("_")} + return None + + +class CustomLog(BaseModel): + """Structured log destination metadata returned by JMS for fleet logging settings.""" + namespace: Optional[str] = Field(None, description="OCI Logging namespace.") + bucket_name: Optional[str] = Field(None, description="Bucket name associated with the log.") + object_name: Optional[str] = Field(None, description="Object name associated with the log.") + log_group_id: Optional[str] = Field(None, description="OCI Logging log group OCID.") + log_id: Optional[str] = Field(None, description="OCI Logging log OCID.") + + +def map_custom_log(custom_log) -> CustomLog | None: + """Convert an OCI JMS custom log object into the local `CustomLog` model.""" + if custom_log is None: + return None + return CustomLog( + namespace=getattr(custom_log, "namespace", None), + bucket_name=getattr(custom_log, "bucket_name", None), + object_name=getattr(custom_log, "object_name", None), + log_group_id=getattr(custom_log, "log_group_id", None), + log_id=getattr(custom_log, "log_id", None), + ) + + +class FleetSummary(BaseModel): + """Summary view of a JMS fleet as returned by list-style fleet APIs.""" + id: Optional[str] = Field(None, description="The OCID of the fleet.") + display_name: Optional[str] = Field(None, description="The name of the fleet.") + description: Optional[str] = Field(None, description="Description of the fleet.") + compartment_id: Optional[str] = Field(None, description="The OCID of the compartment.") + approximate_jre_count: Optional[int] = Field(None, description="Approximate number of Java runtimes.") + approximate_installation_count: Optional[int] = Field( + None, description="Approximate number of Java installations." + ) + approximate_application_count: Optional[int] = Field( + None, description="Approximate number of applications." + ) + approximate_managed_instance_count: Optional[int] = Field( + None, description="Approximate number of managed instances." + ) + approximate_java_server_count: Optional[int] = Field( + None, description="Approximate number of Java servers." + ) + approximate_library_count: Optional[int] = Field( + None, description="Approximate number of libraries." + ) + approximate_library_vulnerability_count: Optional[int] = Field( + None, + description="Approximate number of library vulnerabilities.", + ) + inventory_log: Optional[CustomLog] = Field(None, description="Inventory log configuration.") + operation_log: Optional[CustomLog] = Field(None, description="Operation log configuration.") + is_advanced_features_enabled: Optional[bool] = Field( + None, description="Whether advanced features are enabled." + ) + is_export_setting_enabled: Optional[bool] = Field( + None, description="Whether export setting is enabled." + ) + time_created: Optional[datetime] = Field(None, description="Fleet creation time.") + lifecycle_state: Optional[ + Literal[ + "ACTIVE", + "CREATING", + "DELETED", + "DELETING", + "FAILED", + "NEEDS_ATTENTION", + "UPDATING", + "UNKNOWN_ENUM_VALUE", + ] + ] = Field(None, description="Lifecycle state of the fleet.") + defined_tags: Optional[Dict[str, Dict[str, Any]]] = Field( + None, description="Defined tags for this fleet." + ) + freeform_tags: Optional[Dict[str, str]] = Field(None, description="Free-form tags for this fleet.") + system_tags: Optional[Dict[str, Dict[str, Any]]] = Field( + None, description="System tags for this fleet." + ) + + +def map_fleet_summary(data: oci.jms.models.FleetSummary) -> FleetSummary | None: + """Convert `oci.jms.models.FleetSummary` into the local `FleetSummary` model.""" + if data is None: + return None + return FleetSummary( + id=getattr(data, "id", None), + display_name=getattr(data, "display_name", None), + description=getattr(data, "description", None), + compartment_id=getattr(data, "compartment_id", None), + approximate_jre_count=getattr(data, "approximate_jre_count", None), + approximate_installation_count=getattr(data, "approximate_installation_count", None), + approximate_application_count=getattr(data, "approximate_application_count", None), + approximate_managed_instance_count=getattr(data, "approximate_managed_instance_count", None), + approximate_java_server_count=getattr(data, "approximate_java_server_count", None), + approximate_library_count=getattr(data, "approximate_library_count", None), + approximate_library_vulnerability_count=getattr( + data, "approximate_library_vulnerability_count", None + ), + inventory_log=map_custom_log(getattr(data, "inventory_log", None)), + operation_log=map_custom_log(getattr(data, "operation_log", None)), + is_advanced_features_enabled=getattr(data, "is_advanced_features_enabled", None), + is_export_setting_enabled=getattr(data, "is_export_setting_enabled", None), + time_created=getattr(data, "time_created", None), + lifecycle_state=getattr(data, "lifecycle_state", None), + defined_tags=getattr(data, "defined_tags", None), + freeform_tags=getattr(data, "freeform_tags", None), + system_tags=getattr(data, "system_tags", None), + ) + + +class Fleet(FleetSummary): + """Detailed JMS fleet model for single-fleet reads.""" + pass + + +def map_fleet(data: oci.jms.models.Fleet) -> Fleet | None: + """Convert `oci.jms.models.Fleet` into the local `Fleet` model.""" + if data is None: + return None + return Fleet.model_validate(map_fleet_summary(data).model_dump()) + + +class JmsPluginSummary(BaseModel): + """Summary view of a JMS plugin returned by list APIs.""" + id: Optional[str] = Field(None, description="The OCID of the JMS plugin.") + agent_id: Optional[str] = Field(None, description="The agent OCID.") + agent_type: Optional[Literal["OMA", "OCA", "OCMA", "UNKNOWN_ENUM_VALUE"]] = Field( + None, description="Type of agent reporting the plugin." + ) + lifecycle_state: Optional[ + Literal["ACTIVE", "INACTIVE", "NEEDS_ATTENTION", "DELETED", "UNKNOWN_ENUM_VALUE"] + ] = Field(None, description="Lifecycle state of the plugin.") + availability_status: Optional[ + Literal["ACTIVE", "SILENT", "NOT_AVAILABLE", "UNKNOWN_ENUM_VALUE"] + ] = Field(None, description="Availability status of the plugin.") + fleet_id: Optional[str] = Field(None, description="The associated fleet OCID.") + compartment_id: Optional[str] = Field(None, description="The compartment OCID.") + hostname: Optional[str] = Field(None, description="Hostname of the plugin host.") + os_family: Optional[Literal["LINUX", "WINDOWS", "MACOS", "UNKNOWN", "UNKNOWN_ENUM_VALUE"]] = ( + Field(None, description="Operating system family.") + ) + os_architecture: Optional[str] = Field(None, description="Operating system architecture.") + os_distribution: Optional[str] = Field(None, description="Operating system distribution.") + plugin_version: Optional[str] = Field(None, description="Plugin version.") + time_registered: Optional[datetime] = Field(None, description="Registration time.") + time_last_seen: Optional[datetime] = Field(None, description="Last seen time.") + defined_tags: Optional[Dict[str, Dict[str, Any]]] = Field( + None, description="Defined tags for this plugin." + ) + freeform_tags: Optional[Dict[str, str]] = Field(None, description="Free-form tags for this plugin.") + system_tags: Optional[Dict[str, Dict[str, Any]]] = Field( + None, description="System tags for this plugin." + ) + + +def map_jms_plugin_summary(data: oci.jms.models.JmsPluginSummary) -> JmsPluginSummary | None: + """Convert `oci.jms.models.JmsPluginSummary` into `JmsPluginSummary`.""" + if data is None: + return None + return JmsPluginSummary( + id=getattr(data, "id", None), + agent_id=getattr(data, "agent_id", None), + agent_type=getattr(data, "agent_type", None), + lifecycle_state=getattr(data, "lifecycle_state", None), + availability_status=getattr(data, "availability_status", None), + fleet_id=getattr(data, "fleet_id", None), + compartment_id=getattr(data, "compartment_id", None), + hostname=getattr(data, "hostname", None), + os_family=getattr(data, "os_family", None), + os_architecture=getattr(data, "os_architecture", None), + os_distribution=getattr(data, "os_distribution", None), + plugin_version=getattr(data, "plugin_version", None), + time_registered=getattr(data, "time_registered", None), + time_last_seen=getattr(data, "time_last_seen", None), + defined_tags=getattr(data, "defined_tags", None), + freeform_tags=getattr(data, "freeform_tags", None), + system_tags=getattr(data, "system_tags", None), + ) + + +class JmsPlugin(JmsPluginSummary): + """Detailed JMS plugin model for single-plugin reads.""" + pass + + +def map_jms_plugin(data: oci.jms.models.JmsPlugin) -> JmsPlugin | None: + """Convert `oci.jms.models.JmsPlugin` into the local `JmsPlugin` model.""" + if data is None: + return None + return JmsPlugin.model_validate(map_jms_plugin_summary(data).model_dump()) + + +class JavaRuntimeId(BaseModel): + """Minimal Java runtime identity used in installation-site responses.""" + version: Optional[str] = Field(None, description="Java runtime version.") + vendor: Optional[str] = Field(None, description="Java runtime vendor.") + distribution: Optional[str] = Field(None, description="Java runtime distribution.") + jre_key: Optional[str] = Field(None, description="Unique runtime key.") + + +def map_java_runtime_id(data) -> JavaRuntimeId | None: + """Convert an OCI JMS Java runtime identity object into `JavaRuntimeId`.""" + if data is None: + return None + return JavaRuntimeId( + version=getattr(data, "version", None), + vendor=getattr(data, "vendor", None), + distribution=getattr(data, "distribution", None), + jre_key=getattr(data, "jre_key", None), + ) + + +class OperatingSystem(BaseModel): + """Operating system metadata attached to JMS resources and usage records.""" + family: Optional[Literal["LINUX", "WINDOWS", "MACOS", "UNKNOWN", "UNKNOWN_ENUM_VALUE"]] = ( + Field(None, description="Operating system family.") + ) + name: Optional[str] = Field(None, description="Operating system name.") + distribution: Optional[str] = Field(None, description="Operating system distribution.") + version: Optional[str] = Field(None, description="Operating system version.") + architecture: Optional[str] = Field(None, description="Operating system architecture.") + managed_instance_count: Optional[int] = Field( + None, description="Number of managed instances for this operating system." + ) + container_count: Optional[int] = Field( + None, description="Number of containers for this operating system." + ) + + +def map_operating_system(data) -> OperatingSystem | None: + """Convert an OCI JMS operating system object into the local `OperatingSystem` model.""" + if data is None: + return None + return OperatingSystem( + family=getattr(data, "family", None), + name=getattr(data, "name", None), + distribution=getattr(data, "distribution", None), + version=getattr(data, "version", None), + architecture=getattr(data, "architecture", None), + managed_instance_count=getattr(data, "managed_instance_count", None), + container_count=getattr(data, "container_count", None), + ) + + +class InstallationSiteSummary(BaseModel): + """Summary of a Java installation site discovered within a JMS fleet.""" + installation_key: Optional[str] = Field(None, description="Unique installation identifier.") + managed_instance_id: Optional[str] = Field(None, description="Managed instance OCID.") + jre: Optional[JavaRuntimeId] = Field(None, description="Associated Java runtime identifier.") + security_status: Optional[ + Literal[ + "EARLY_ACCESS", + "UNKNOWN", + "UP_TO_DATE", + "UPDATE_REQUIRED", + "UPGRADE_REQUIRED", + "UNKNOWN_ENUM_VALUE", + ] + ] = Field(None, description="Security status of the Java runtime.") + path: Optional[str] = Field(None, description="Installation path.") + operating_system: Optional[OperatingSystem] = Field( + None, description="Operating system for the installation site." + ) + approximate_application_count: Optional[int] = Field( + None, description="Approximate number of applications using this installation." + ) + time_last_seen: Optional[datetime] = Field(None, description="Last seen time.") + blocklist: Optional[List[Dict[str, Any]]] = Field( + None, description="Blocklist entries associated with the installation." + ) + lifecycle_state: Optional[ + Literal[ + "ACTIVE", + "CREATING", + "DELETED", + "DELETING", + "FAILED", + "NEEDS_ATTENTION", + "UPDATING", + "UNKNOWN_ENUM_VALUE", + ] + ] = Field(None, description="Lifecycle state of the installation site.") + + +def map_installation_site_summary( + data: oci.jms.models.InstallationSiteSummary, +) -> InstallationSiteSummary | None: + """Convert `oci.jms.models.InstallationSiteSummary` into `InstallationSiteSummary`.""" + if data is None: + return None + blocklist = getattr(data, "blocklist", None) + return InstallationSiteSummary( + installation_key=getattr(data, "installation_key", None), + managed_instance_id=getattr(data, "managed_instance_id", None), + jre=map_java_runtime_id(getattr(data, "jre", None)), + security_status=getattr(data, "security_status", None), + path=getattr(data, "path", None), + operating_system=map_operating_system(getattr(data, "operating_system", None)), + approximate_application_count=getattr(data, "approximate_application_count", None), + time_last_seen=getattr(data, "time_last_seen", None), + blocklist=[_oci_to_dict(item) for item in blocklist] if blocklist else None, + lifecycle_state=getattr(data, "lifecycle_state", None), + ) + + +class FleetAgentOsConfiguration(BaseModel): + """OS-specific include and exclude path configuration for JMS fleet agents.""" + include_paths: Optional[List[str]] = Field(None, description="Included filesystem paths.") + exclude_paths: Optional[List[str]] = Field(None, description="Excluded filesystem paths.") + + +def map_fleet_agent_os_configuration(data) -> FleetAgentOsConfiguration | None: + """Convert an OCI JMS fleet-agent OS configuration into the local model.""" + if data is None: + return None + return FleetAgentOsConfiguration( + include_paths=getattr(data, "include_paths", None), + exclude_paths=getattr(data, "exclude_paths", None), + ) + + +class FleetAgentConfiguration(BaseModel): + """Fleet-wide JMS agent configuration returned for a fleet.""" + jre_scan_frequency_in_minutes: Optional[int] = Field( + None, description="JRE scanning frequency in minutes." + ) + java_usage_tracker_processing_frequency_in_minutes: Optional[int] = Field( + None, description="Java usage tracker processing frequency in minutes." + ) + work_request_validity_period_in_days: Optional[int] = Field( + None, description="Validity period for work requests in days." + ) + agent_polling_interval_in_minutes: Optional[int] = Field( + None, description="Agent polling interval in minutes." + ) + is_collecting_managed_instance_metrics_enabled: Optional[bool] = Field( + None, description="Whether managed instance metrics collection is enabled." + ) + is_collecting_usernames_enabled: Optional[bool] = Field( + None, description="Whether username collection is enabled." + ) + is_capturing_ip_address_and_fqdn_enabled: Optional[bool] = Field( + None, description="Whether IP address and FQDN capture is enabled." + ) + is_libraries_scan_enabled: Optional[bool] = Field( + None, description="Whether library scanning is enabled." + ) + linux_configuration: Optional[FleetAgentOsConfiguration] = Field( + None, description="Linux-specific agent configuration." + ) + windows_configuration: Optional[FleetAgentOsConfiguration] = Field( + None, description="Windows-specific agent configuration." + ) + mac_os_configuration: Optional[FleetAgentOsConfiguration] = Field( + None, description="macOS-specific agent configuration." + ) + time_last_modified: Optional[datetime] = Field(None, description="Last modified time.") + + +def map_fleet_agent_configuration( + data: oci.jms.models.FleetAgentConfiguration, +) -> FleetAgentConfiguration | None: + """Convert `oci.jms.models.FleetAgentConfiguration` into `FleetAgentConfiguration`.""" + if data is None: + return None + return FleetAgentConfiguration( + jre_scan_frequency_in_minutes=getattr(data, "jre_scan_frequency_in_minutes", None), + java_usage_tracker_processing_frequency_in_minutes=getattr( + data, "java_usage_tracker_processing_frequency_in_minutes", None + ), + work_request_validity_period_in_days=getattr( + data, "work_request_validity_period_in_days", None + ), + agent_polling_interval_in_minutes=getattr(data, "agent_polling_interval_in_minutes", None), + is_collecting_managed_instance_metrics_enabled=getattr( + data, "is_collecting_managed_instance_metrics_enabled", None + ), + is_collecting_usernames_enabled=getattr(data, "is_collecting_usernames_enabled", None), + is_capturing_ip_address_and_fqdn_enabled=getattr( + data, "is_capturing_ip_address_and_fqdn_enabled", None + ), + is_libraries_scan_enabled=getattr(data, "is_libraries_scan_enabled", None), + linux_configuration=map_fleet_agent_os_configuration( + getattr(data, "linux_configuration", None) + ), + windows_configuration=map_fleet_agent_os_configuration( + getattr(data, "windows_configuration", None) + ), + mac_os_configuration=map_fleet_agent_os_configuration( + getattr(data, "mac_os_configuration", None) + ), + time_last_modified=getattr(data, "time_last_modified", None), + ) + + +class FleetAdvancedFeatureConfiguration(BaseModel): + """Advanced feature configuration attached to a JMS fleet.""" + analytic_namespace: Optional[str] = Field(None, description="Analytics namespace.") + analytic_bucket_name: Optional[str] = Field(None, description="Analytics bucket name.") + lcm: Optional[Dict[str, Any]] = Field(None, description="LCM configuration.") + crypto_event_analysis: Optional[Dict[str, Any]] = Field( + None, description="Crypto event analysis configuration." + ) + advanced_usage_tracking: Optional[Dict[str, Any]] = Field( + None, description="Advanced usage tracking configuration." + ) + jfr_recording: Optional[Dict[str, Any]] = Field(None, description="JFR recording configuration.") + performance_tuning_analysis: Optional[Dict[str, Any]] = Field( + None, description="Performance tuning analysis configuration." + ) + java_migration_analysis: Optional[Dict[str, Any]] = Field( + None, description="Java migration analysis configuration." + ) + time_last_modified: Optional[datetime] = Field(None, description="Last modified time.") + + +def map_fleet_advanced_feature_configuration( + data: oci.jms.models.FleetAdvancedFeatureConfiguration, +) -> FleetAdvancedFeatureConfiguration | None: + """Convert fleet advanced feature configuration into a serializable local model.""" + if data is None: + return None + return FleetAdvancedFeatureConfiguration( + analytic_namespace=getattr(data, "analytic_namespace", None), + analytic_bucket_name=getattr(data, "analytic_bucket_name", None), + lcm=_oci_to_dict(getattr(data, "lcm", None)), + crypto_event_analysis=_oci_to_dict(getattr(data, "crypto_event_analysis", None)), + advanced_usage_tracking=_oci_to_dict(getattr(data, "advanced_usage_tracking", None)), + jfr_recording=_oci_to_dict(getattr(data, "jfr_recording", None)), + performance_tuning_analysis=_oci_to_dict( + getattr(data, "performance_tuning_analysis", None) + ), + java_migration_analysis=_oci_to_dict(getattr(data, "java_migration_analysis", None)), + time_last_modified=getattr(data, "time_last_modified", None), + ) + + +class ManagedInstanceUsage(BaseModel): + """Managed instance usage summary returned by JMS usage aggregation APIs.""" + managed_instance_id: Optional[str] = Field(None, description="Managed instance OCID.") + managed_instance_type: Optional[ + Literal[ + "ORACLE_MANAGEMENT_AGENT", + "ORACLE_CLOUD_AGENT", + "ORACLE_CONTAINER_MANAGEMENT_AGENT", + "UNKNOWN_ENUM_VALUE", + ] + ] = Field(None, description="Managed instance type.") + hostname: Optional[str] = Field(None, description="Hostname of the managed instance.") + host_id: Optional[str] = Field(None, description="Host OCID or host identifier.") + ip_addresses: Optional[List[str]] = Field(None, description="IP addresses of the managed instance.") + hostnames: Optional[List[str]] = Field(None, description="Hostnames associated with the instance.") + fqdns: Optional[List[str]] = Field(None, description="FQDNs associated with the instance.") + operating_system: Optional[OperatingSystem] = Field( + None, description="Operating system information." + ) + agent: Optional[Dict[str, Any]] = Field(None, description="Agent details.") + cluster_details: Optional[Dict[str, Any]] = Field(None, description="Cluster details.") + approximate_application_count: Optional[int] = Field( + None, description="Approximate application count." + ) + approximate_installation_count: Optional[int] = Field( + None, description="Approximate installation count." + ) + approximate_jre_count: Optional[int] = Field(None, description="Approximate JRE count.") + drs_file_status: Optional[ + Literal["PRESENT", "ABSENT", "MISMATCH", "NOT_CONFIGURED", "UNKNOWN_ENUM_VALUE"] + ] = Field(None, description="DRS file status.") + application_invoked_by: Optional[str] = Field( + None, description="Username or principal that invoked the application." + ) + time_start: Optional[datetime] = Field(None, description="Usage summary start time.") + time_end: Optional[datetime] = Field(None, description="Usage summary end time.") + time_first_seen: Optional[datetime] = Field(None, description="First seen time.") + time_last_seen: Optional[datetime] = Field(None, description="Last seen time.") + + +def map_managed_instance_usage( + data: oci.jms.models.ManagedInstanceUsage, +) -> ManagedInstanceUsage | None: + """Convert `oci.jms.models.ManagedInstanceUsage` into `ManagedInstanceUsage`.""" + if data is None: + return None + return ManagedInstanceUsage( + managed_instance_id=getattr(data, "managed_instance_id", None), + managed_instance_type=getattr(data, "managed_instance_type", None), + hostname=getattr(data, "hostname", None), + host_id=getattr(data, "host_id", None), + ip_addresses=getattr(data, "ip_addresses", None), + hostnames=getattr(data, "hostnames", None), + fqdns=getattr(data, "fqdns", None), + operating_system=map_operating_system(getattr(data, "operating_system", None)), + agent=_oci_to_dict(getattr(data, "agent", None)), + cluster_details=_oci_to_dict(getattr(data, "cluster_details", None)), + approximate_application_count=getattr(data, "approximate_application_count", None), + approximate_installation_count=getattr(data, "approximate_installation_count", None), + approximate_jre_count=getattr(data, "approximate_jre_count", None), + drs_file_status=getattr(data, "drs_file_status", None), + application_invoked_by=getattr(data, "application_invoked_by", None), + time_start=getattr(data, "time_start", None), + time_end=getattr(data, "time_end", None), + time_first_seen=getattr(data, "time_first_seen", None), + time_last_seen=getattr(data, "time_last_seen", None), + ) + + +class ResourceInventory(BaseModel): + """High-level inventory counts for JMS resources in a compartment.""" + active_fleet_count: Optional[int] = Field(None, description="Number of active fleets.") + managed_instance_count: Optional[int] = Field(None, description="Number of managed instances.") + jre_count: Optional[int] = Field(None, description="Number of Java runtimes.") + installation_count: Optional[int] = Field(None, description="Number of Java installations.") + application_count: Optional[int] = Field(None, description="Number of applications.") + + +def map_resource_inventory(data: oci.jms.models.ResourceInventory) -> ResourceInventory | None: + """Convert `oci.jms.models.ResourceInventory` into `ResourceInventory`.""" + if data is None: + return None + return ResourceInventory( + active_fleet_count=getattr(data, "active_fleet_count", None), + managed_instance_count=getattr(data, "managed_instance_count", None), + jre_count=getattr(data, "jre_count", None), + installation_count=getattr(data, "installation_count", None), + application_count=getattr(data, "application_count", None), + ) + + +class FleetDiagnosisRecord(BaseModel): + """Fleet diagnosis record returned by JMS health APIs.""" + resource_diagnosis: Optional[str] = Field(None, description="Diagnosis message for the resource.") + resource_id: Optional[str] = Field(None, description="Affected resource OCID or identifier.") + resource_state: Optional[str] = Field(None, description="Lifecycle or health state of the resource.") + resource_type: Optional[str] = Field(None, description="Type of the affected resource.") + + +def map_fleet_diagnosis(data: oci.jms.models.FleetDiagnosisSummary) -> FleetDiagnosisRecord | None: + """Convert `oci.jms.models.FleetDiagnosisSummary` into `FleetDiagnosisRecord`.""" + if data is None: + return None + return FleetDiagnosisRecord( + resource_diagnosis=getattr(data, "resource_diagnosis", None), + resource_id=getattr(data, "resource_id", None), + resource_state=getattr(data, "resource_state", None), + resource_type=getattr(data, "resource_type", None), + ) + + +class FleetErrorDetail(BaseModel): + """Detailed fleet error entry nested inside a fleet error summary.""" + details: Optional[str] = Field(None, description="Detailed description of the error.") + reason: Optional[str] = Field(None, description="High-level reason for the error.") + time_last_seen: Optional[datetime] = Field(None, description="Last time the error was seen.") + + +def map_fleet_error_detail(data: oci.jms.models.FleetErrorDetails) -> FleetErrorDetail | None: + """Convert `oci.jms.models.FleetErrorDetails` into `FleetErrorDetail`.""" + if data is None: + return None + return FleetErrorDetail( + details=getattr(data, "details", None), + reason=getattr(data, "reason", None), + time_last_seen=getattr(data, "time_last_seen", None), + ) + + +class FleetErrorRecord(BaseModel): + """Fleet-scoped error summary returned by JMS health APIs.""" + compartment_id: Optional[str] = Field(None, description="Compartment OCID containing the fleet.") + fleet_id: Optional[str] = Field(None, description="Fleet OCID.") + fleet_name: Optional[str] = Field(None, description="Fleet display name.") + errors: Optional[List[FleetErrorDetail]] = Field( + None, description="Error detail entries associated with this fleet." + ) + time_first_seen: Optional[datetime] = Field(None, description="First time the fleet error was seen.") + time_last_seen: Optional[datetime] = Field(None, description="Last time the fleet error was seen.") + + +def map_fleet_error(data: oci.jms.models.FleetErrorSummary) -> FleetErrorRecord | None: + """Convert `oci.jms.models.FleetErrorSummary` into `FleetErrorRecord`.""" + if data is None: + return None + errors = getattr(data, "errors", None) + return FleetErrorRecord( + compartment_id=getattr(data, "compartment_id", None), + fleet_id=getattr(data, "fleet_id", None), + fleet_name=getattr(data, "fleet_name", None), + errors=[map_fleet_error_detail(item) for item in errors] if errors else None, + time_first_seen=getattr(data, "time_first_seen", None), + time_last_seen=getattr(data, "time_last_seen", None), + ) + + +class FleetHealthSummary(BaseModel): + """Chat-friendly summary of the health posture of a JMS fleet.""" + fleet_id: str = Field(..., description="Fleet OCID.") + diagnosis_count: int = Field(..., description="Number of diagnosis records for the fleet.") + fleet_errors: List[FleetErrorRecord] = Field( + default_factory=list, description="Fleet error records returned for the fleet." + ) + top_issue_categories: List[str] = Field( + default_factory=list, description="Deduplicated high-signal issue categories." + ) + overall_health_status: Literal["HEALTHY", "WARNING", "CRITICAL", "UNKNOWN"] = Field( + ..., description="Derived overall health status for the fleet." + ) + recommended_next_checks: List[str] = Field( + default_factory=list, + description="MCP-generated follow-up checks derived from returned diagnoses and errors.", + ) + + +class FleetHealthDiagnostics(BaseModel): + """Detailed fleet health diagnostics for drill-down troubleshooting.""" + fleet_id: str = Field(..., description="Fleet OCID.") + diagnoses: List[FleetDiagnosisRecord] = Field( + default_factory=list, description="Detailed diagnosis records for the fleet." + ) + fleet_errors: List[FleetErrorRecord] = Field( + default_factory=list, description="Detailed fleet error records for the fleet." + ) + diagnosis_count: int = Field(..., description="Number of diagnosis records returned.") + fleet_error_count: int = Field(..., description="Number of fleet error records returned.") + + +class JmsNotice(BaseModel): + """Announcement or notice surfaced by the JMS service.""" + key: Optional[int] = Field(None, description="Announcement key.") + summary: Optional[str] = Field(None, description="Announcement summary.") + time_released: Optional[datetime] = Field(None, description="Announcement release time.") + url: Optional[str] = Field(None, description="Announcement reference URL.") + + +def map_jms_notice(data: oci.jms.models.AnnouncementSummary) -> JmsNotice | None: + """Convert `oci.jms.models.AnnouncementSummary` into `JmsNotice`.""" + if data is None: + return None + return JmsNotice( + key=getattr(data, "key", None), + summary=getattr(data, "summary", None), + time_released=getattr(data, "time_released", None), + url=getattr(data, "url", None), + ) + + +class JavaRuntimeComplianceBucket(BaseModel): + """Compliance summary for one runtime version or version/vendor/distribution tuple.""" + version: Optional[str] = Field(None, description="Java runtime version.") + vendor: Optional[str] = Field(None, description="Java runtime vendor.") + distribution: Optional[str] = Field(None, description="Java runtime distribution.") + security_status: Optional[str] = Field(None, description="Security status of the runtime.") + runtime_count: int = Field(..., description="Approximate count of runtimes/installations in this bucket.") + approximate_managed_instance_count: Optional[int] = Field( + None, description="Approximate number of managed instances for this runtime." + ) + approximate_application_count: Optional[int] = Field( + None, description="Approximate number of applications for this runtime." + ) + release_date: Optional[datetime] = Field(None, description="Java release date when available.") + days_under_security_baseline: Optional[int] = Field( + None, description="Days the release is under the security baseline when available." + ) + license_type: Optional[str] = Field(None, description="License type for the Java release.") + release_type: Optional[str] = Field(None, description="Release type for the Java release.") + release_notes_url: Optional[str] = Field(None, description="Release notes URL when available.") + + +class JavaRuntimeCountBreakdown(BaseModel): + """Simple count breakdown by one grouping key such as vendor or distribution.""" + key: str = Field(..., description="Grouping key label.") + runtime_count: int = Field(..., description="Approximate count of runtimes/installations in this group.") + + +class OutdatedJavaInstallation(BaseModel): + """Drill-down row for one outdated installation site.""" + installation_key: Optional[str] = Field(None, description="Unique installation identifier.") + managed_instance_id: Optional[str] = Field(None, description="Managed instance OCID.") + path: Optional[str] = Field(None, description="Installation path.") + version: Optional[str] = Field(None, description="Java runtime version.") + vendor: Optional[str] = Field(None, description="Java runtime vendor.") + distribution: Optional[str] = Field(None, description="Java runtime distribution.") + security_status: Optional[str] = Field(None, description="Security status of the runtime.") + time_last_seen: Optional[datetime] = Field(None, description="Last seen time for the installation.") + + +class JavaRuntimeComplianceReport(BaseModel): + """Fleet-level runtime compliance report built from JMS usage and release metadata APIs.""" + fleet_id: str = Field(..., description="Fleet OCID.") + total_runtimes_in_fleet: int = Field(..., description="Approximate total number of runtimes in the fleet.") + up_to_date_runtimes: int = Field(..., description="Approximate count of up-to-date runtimes.") + runtimes_requiring_update: int = Field(..., description="Approximate count of runtimes requiring update.") + runtimes_requiring_upgrade: int = Field(..., description="Approximate count of runtimes requiring upgrade.") + unknown_runtimes: int = Field(..., description="Approximate count of runtimes with unknown status.") + version_breakdown: List[JavaRuntimeComplianceBucket] = Field( + default_factory=list, description="Compliance breakdown by runtime version." + ) + vendor_breakdown: List[JavaRuntimeCountBreakdown] = Field( + default_factory=list, description="Compliance breakdown by vendor." + ) + distribution_breakdown: List[JavaRuntimeCountBreakdown] = Field( + default_factory=list, description="Compliance breakdown by distribution." + ) + outdated_installations: List[OutdatedJavaInstallation] = Field( + default_factory=list, description="Bounded drill-down rows for outdated installations." + ) diff --git a/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/server.py b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/server.py new file mode 100644 index 00000000..96715b74 --- /dev/null +++ b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/server.py @@ -0,0 +1,1070 @@ +""" +Copyright (c) 2025, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +import os +from datetime import datetime +from logging import Logger +from typing import Literal, Optional + +import oci +from fastmcp import FastMCP +from oracle.oci_jms_mcp_server.models import ( + Fleet, + FleetAdvancedFeatureConfiguration, + FleetAgentConfiguration, + FleetDiagnosisRecord, + FleetErrorRecord, + FleetHealthDiagnostics, + FleetHealthSummary, + FleetSummary, + InstallationSiteSummary, + JavaRuntimeComplianceBucket, + JavaRuntimeComplianceReport, + JavaRuntimeCountBreakdown, + JmsNotice, + JmsPlugin, + JmsPluginSummary, + ManagedInstanceUsage, + OutdatedJavaInstallation, + ResourceInventory, + map_fleet, + map_fleet_advanced_feature_configuration, + map_fleet_agent_configuration, + map_fleet_diagnosis, + map_fleet_error, + map_fleet_summary, + map_installation_site_summary, + map_jms_notice, + map_jms_plugin, + map_jms_plugin_summary, + map_managed_instance_usage, + map_resource_inventory, +) +from pydantic import Field + +from . import __project__, __version__ +from .util import get_jms_service_endpoint + +# Setup +logger = Logger(__name__, level="INFO") + +mcp = FastMCP(name=__project__) + +_UNSUPPORTED_ENV_ALIASES = { + "OCI_CONFIG": "OCI_CONFIG_FILE", + "OCI_PROFILE": "OCI_CONFIG_PROFILE", +} +_MAX_OUTDATED_INSTALLATIONS = 25 + +# Input Normalization Helpers +def _normalize_enum(value: Optional[str]) -> Optional[str]: + """Normalize flexible enum input into OCI SDK-style uppercase values.""" + if value is None: + return None + normalized = value.strip() + if not normalized: + return None + return normalized.upper().replace("-", "_").replace(" ", "_") + + +def _normalize_enum_list(values: Optional[list[str]]) -> Optional[list[str]]: + """Normalize lists of enum-like values.""" + if values is None: + return None + normalized = [_normalize_enum(value) for value in values] + normalized = [value for value in normalized if value is not None] + return normalized or None + + +def _normalized_key(value: str) -> str: + """Canonicalize user input for case-insensitive matching of mixed-case SDK values.""" + return "".join(ch for ch in value.strip().lower() if ch.isalnum()) + + +def _normalize_choice(value: Optional[str], allowed_values: list[str]) -> Optional[str]: + """Map flexible user input to one of the SDK's exact allowed string values.""" + if value is None: + return None + normalized = value.strip() + if not normalized: + return None + + by_key = {_normalized_key(item): item for item in allowed_values} + return by_key.get(_normalized_key(normalized), value) + + +def _normalize_choice_list( + values: Optional[list[str]], allowed_values: list[str] +) -> Optional[list[str]]: + """Map a list of flexible user inputs to exact SDK values where possible.""" + if values is None: + return None + normalized = [_normalize_choice(value, allowed_values) for value in values] + normalized = [value for value in normalized if value is not None] + return normalized or None + + +def _omit_none(**kwargs): + """Drop only None values so optional OCI SDK kwargs are not sent spuriously.""" + return {key: value for key, value in kwargs.items() if value is not None} + + +def _parse_rfc3339(value: Optional[str]) -> Optional[datetime]: + """Convert an optional RFC3339 timestamp string into a datetime.""" + if value is None: + return None + normalized = value.strip() + if not normalized: + return None + return datetime.fromisoformat(normalized.replace("Z", "+00:00")) + + +def _collect_paginated_items(method, **kwargs): + """Collect all items from a paginated OCI list operation.""" + items = [] + has_next_page = True + next_page: Optional[str] = None + limit = kwargs.get("limit") + + while has_next_page and (limit is None or len(items) < limit): + response = method(**_omit_none(**kwargs, page=next_page)) + response_items = list(getattr(response.data, "items", [])) + if limit is not None: + remaining = limit - len(items) + response_items = response_items[:remaining] + items.extend(response_items) + has_next_page = getattr(response, "has_next_page", False) + next_page = response.next_page if hasattr(response, "next_page") else None + + return items + + +def _collect_text_fragments(*values) -> list[str]: + """Recursively gather non-empty strings from nested tool data.""" + fragments: list[str] = [] + for value in values: + if value is None: + continue + if isinstance(value, str): + normalized = value.strip() + if normalized and normalized != "UNKNOWN_ENUM_VALUE": + fragments.append(normalized) + continue + if isinstance(value, list): + fragments.extend(_collect_text_fragments(*value)) + continue + if isinstance(value, dict): + fragments.extend(_collect_text_fragments(*value.values())) + continue + if hasattr(value, "model_dump"): + fragments.extend(_collect_text_fragments(value.model_dump())) + return fragments + + +def _normalize_issue_category(value: str) -> str: + """Produce a stable issue category label from diagnosis or error text.""" + normalized = " ".join(value.replace("_", " ").split()) + return normalized[:1].upper() + normalized[1:] if normalized else normalized + + +def _pick_issue_category(*values: Optional[str]) -> Optional[str]: + """Pick the first non-empty, non-sentinel issue text.""" + for value in values: + if value and value.strip() and value != "UNKNOWN_ENUM_VALUE": + return value + return None + + +def _derive_top_issue_categories( + diagnoses: list[FleetDiagnosisRecord], fleet_errors: list[FleetErrorRecord] +) -> list[str]: + """Extract a stable, deduplicated list of high-signal issue categories.""" + categories: list[str] = [] + seen: set[str] = set() + + for diagnosis in diagnoses: + candidate = diagnosis.resource_diagnosis + if candidate: + normalized = _normalize_issue_category(candidate) + key = normalized.casefold() + if key not in seen: + seen.add(key) + categories.append(normalized) + + for fleet_error in fleet_errors: + for error in fleet_error.errors or []: + candidate = _pick_issue_category(error.reason, error.details) + if candidate: + normalized = _normalize_issue_category(candidate) + key = normalized.casefold() + if key not in seen: + seen.add(key) + categories.append(normalized) + + return categories[:5] + + +def _derive_overall_health_status( + diagnoses: list[FleetDiagnosisRecord], fleet_errors: list[FleetErrorRecord] +) -> Literal["HEALTHY", "WARNING", "CRITICAL", "UNKNOWN"]: + """Collapse fleet diagnoses and errors into a single coarse health status.""" + if not diagnoses and not fleet_errors: + return "HEALTHY" + + fragments = _collect_text_fragments(diagnoses, fleet_errors) + if not fragments: + return "UNKNOWN" + + severe_markers = ( + "critical", + "failed", + "failure", + "error", + "severe", + "not available", + "unavailable", + ) + warning_markers = ( + "needs attention", + "warning", + "inventory", + "scan", + "plugin", + "agent", + ) + + lowered = [fragment.casefold() for fragment in fragments] + # Prefer a stable coarse status over surfacing raw provider-specific states. + if any(marker in fragment for fragment in lowered for marker in severe_markers): + return "CRITICAL" + if any(marker in fragment for fragment in lowered for marker in warning_markers): + return "WARNING" + return "WARNING" + + +def _derive_recommended_next_checks( + diagnoses: list[FleetDiagnosisRecord], fleet_errors: list[FleetErrorRecord] +) -> list[str]: + """Generate deterministic next-step hints from returned diagnoses and errors.""" + if not diagnoses and not fleet_errors: + return [] + + fragments = [fragment.casefold() for fragment in _collect_text_fragments(diagnoses, fleet_errors)] + recommendations: list[str] = [] + + def add_once(text: str): + if text not in recommendations: + recommendations.append(text) + + # These recommendations intentionally trade completeness for deterministic, + # chat-friendly next steps derived from recurring diagnosis/error keywords. + if any( + marker in fragment + for fragment in fragments + for marker in ("inventory", "scan", "discovery", "collection") + ): + add_once("Review fleet agent configuration and inventory collection settings.") + + if any( + marker in fragment + for fragment in fragments + for marker in ("agent", "plugin", "silent", "report", "heartbeat", "connect") + ): + add_once("Inspect detailed fleet health diagnostics and verify recent agent or plugin reporting.") + + if any( + marker in fragment + for fragment in fragments + for marker in ("auth", "permission", "unauthorized", "forbidden", "region") + ): + add_once("Verify fleet visibility, OCI access, and region or compartment alignment.") + + add_once("Check JMS notices for any known service-side issues or advisories.") + return recommendations + + +def _normalize_count(value: Optional[int]) -> int: + """Treat missing approximate counts as zero for aggregation purposes.""" + return int(value or 0) + + +def _safe_get_java_release(client, release_version: Optional[str]): + """Best-effort Java release lookup; ignore not-found style misses for enrichment.""" + if not release_version: + return None + try: + response: oci.response.Response = client.get_java_release(release_version=release_version) + return response.data + except oci.exceptions.ServiceError as exc: + if exc.status in (400, 404): + return None + raise + + +def _build_runtime_count_breakdowns(values: list[tuple[Optional[str], int]]) -> list[JavaRuntimeCountBreakdown]: + """Aggregate runtime counts by a single string key such as vendor or distribution.""" + counts: dict[str, int] = {} + for raw_key, count in values: + key = raw_key or "UNKNOWN" + counts[key] = counts.get(key, 0) + count + + return [ + JavaRuntimeCountBreakdown(key=key, runtime_count=runtime_count) + for key, runtime_count in sorted(counts.items(), key=lambda item: (-item[1], item[0])) + ] + + +def _warn_on_unsupported_env_aliases(): + """Warn when env vars from the separate generic OCI API MCP project are used.""" + for alias, canonical in _UNSUPPORTED_ENV_ALIASES.items(): + if os.getenv(alias) and not os.getenv(canonical): + logger.warning( + f"{alias} is not used by oracle.oci-jms-mcp-server; use {canonical} instead." + ) + + +def _load_oci_config() -> dict: + """Load OCI SDK config using the JMS server's supported env var names.""" + # Warn early because users often copy client config from the generic OCI API MCP server. + _warn_on_unsupported_env_aliases() + return oci.config.from_file( + file_location=os.getenv("OCI_CONFIG_FILE", oci.config.DEFAULT_LOCATION), + profile_name=os.getenv("OCI_CONFIG_PROFILE", oci.config.DEFAULT_PROFILE), + ) + + +def _build_security_token_signer(config: dict) -> oci.auth.signers.SecurityTokenSigner: + """Create a security-token signer from the OCI config file referenced key and token.""" + private_key = oci.signer.load_private_key_from_file(config["key_file"]) + token_file = os.path.expanduser(config["security_token_file"]) + with open(token_file, "r") as f: + token = f.read().strip() + return oci.auth.signers.SecurityTokenSigner(token, private_key) + + +def get_jms_client(): + """Construct a fresh OCI Java Management Service client for the active profile.""" + logger.info("entering get_jms_client") + config = _load_oci_config() + user_agent_name = __project__.split("oracle.", 1)[1].split("-server", 1)[0] + config["additional_user_agent"] = f"{user_agent_name}/{__version__}" + signer = _build_security_token_signer(config) + service_endpoint = get_jms_service_endpoint(config) + if service_endpoint: + logger.info(f"Using JMS endpoint override from JMS_TEST_ENVIRONMENT: {service_endpoint}") + client_kwargs = _omit_none(signer=signer, service_endpoint=service_endpoint) + return oci.jms.JavaManagementServiceClient(config, **client_kwargs) + +# mcp tool functions +@mcp.tool(description="List Java Management Service fleets in a compartment.") +def list_fleets( + compartment_id: Optional[str] = Field( + None, + description="The OCID of the compartment in which to list fleets. Required unless `id` is provided.", + ), + id: Optional[str] = Field(None, description="The OCID of a specific fleet to filter for."), + lifecycle_state: Optional[str] = Field( + None, + description=( + "Filter fleets by lifecycle state. Accepted values include ACTIVE, CREATING, " + "DELETED, DELETING, FAILED, NEEDS_ATTENTION, and UPDATING." + ), + ), + display_name: Optional[str] = Field(None, description="Filter fleets by exact display name."), + display_name_contains: Optional[str] = Field( + None, + description="Filter fleets whose display name contains this value.", + ), + limit: Optional[int] = Field(None, description="Maximum number of fleets to return.", ge=1), + sort_order: Optional[str] = Field( + None, description="Sort order for the fleet results: ASC or DESC." + ), + sort_by: Optional[str] = Field( + None, + description="Field to sort fleets by.", + ), +) -> list[FleetSummary]: + """List fleets visible in the requested compartment, handling pagination transparently.""" + fleets: list[FleetSummary] = [] + + try: + client = get_jms_client() + + response: oci.response.Response | None = None + has_next_page = True + next_page: Optional[str] = None + + while has_next_page and (limit is None or len(fleets) < limit): + # Continue paging until OCI stops returning pages or the caller-supplied limit is met. + response = client.list_fleets(**_omit_none( + compartment_id=compartment_id, + id=id, + lifecycle_state=_normalize_enum(lifecycle_state), + display_name=display_name, + display_name_contains=display_name_contains, + limit=limit, + sort_order=_normalize_enum(sort_order), + sort_by=_normalize_choice(sort_by, ["displayName", "timeCreated"]), + page=next_page, + )) + has_next_page = response.has_next_page + next_page = response.next_page if hasattr(response, "next_page") else None + + for item in response.data.items: + fleets.append(map_fleet_summary(item)) + if limit is not None and len(fleets) >= limit: + break + + logger.info(f"Found {len(fleets)} fleets") + return fleets + except Exception as e: + logger.error(f"Error in list_fleets tool: {str(e)}") + raise + + +@mcp.tool(description="Get a JMS fleet by its OCID.") +def get_fleet(fleet_id: str = Field(..., description="The OCID of the fleet.")) -> Fleet: + """Fetch a single fleet by OCID and convert the OCI SDK model into the local Pydantic model.""" + try: + client = get_jms_client() + response: oci.response.Response = client.get_fleet(fleet_id=fleet_id) + logger.info("Found fleet") + return map_fleet(response.data) + except Exception as e: + logger.error(f"Error in get_fleet tool: {str(e)}") + raise + + +@mcp.tool(description="List JMS plugins in a compartment or fleet.") +def list_jms_plugins( + compartment_id: Optional[str] = Field( + None, + description="The OCID of the compartment in which to list plugins.", + ), + compartment_id_in_subtree: bool = Field( + False, + description="Whether to gather plugin information from the compartment subtree.", + ), + id: Optional[str] = Field(None, description="The OCID of a specific JMS plugin."), + fleet_id: Optional[str] = Field(None, description="Filter by fleet OCID."), + agent_id: Optional[str] = Field(None, description="Filter by agent OCID."), + lifecycle_state: Optional[str] = Field( + None, + description=( + "Filter plugins by lifecycle state. Accepted values include ACTIVE, INACTIVE, " + "NEEDS_ATTENTION, and DELETED." + ), + ), + availability_status: Optional[str] = Field( + None, + description="Filter plugins by availability status: ACTIVE, SILENT, or NOT_AVAILABLE.", + ), + agent_type: Optional[str] = Field( + None, + description="Filter plugins by agent type: OMA or OCA.", + ), + time_registered_less_than_or_equal_to: Optional[str] = Field( + None, + description="Only return plugins registered at or before this RFC3339 timestamp.", + ), + time_last_seen_less_than_or_equal_to: Optional[str] = Field( + None, + description="Only return plugins last seen at or before this RFC3339 timestamp.", + ), + hostname_contains: Optional[str] = Field( + None, + description="Filter the list with hostname contains this value.", + ), + limit: Optional[int] = Field(None, description="Maximum number of plugins to return.", ge=1), + sort_order: Optional[str] = Field( + None, description="Sort order for the plugin results: ASC or DESC." + ), + sort_by: Optional[str] = Field(None, description="Field to sort plugins by."), +) -> list[JmsPluginSummary]: + """List JMS plugins with optional fleet, agent, lifecycle, and time-based filtering.""" + plugins: list[JmsPluginSummary] = [] + + try: + client = get_jms_client() + + response: oci.response.Response | None = None + has_next_page = True + next_page: Optional[str] = None + + while has_next_page and (limit is None or len(plugins) < limit): + # Parse timestamp filters up front so the SDK receives native datetimes. + response = client.list_jms_plugins(**_omit_none( + compartment_id=compartment_id, + compartment_id_in_subtree=compartment_id_in_subtree, + id=id, + fleet_id=fleet_id, + agent_id=agent_id, + lifecycle_state=_normalize_enum(lifecycle_state), + availability_status=_normalize_enum(availability_status), + agent_type=_normalize_enum(agent_type), + time_registered_less_than_or_equal_to=_parse_rfc3339( + time_registered_less_than_or_equal_to + ), + time_last_seen_less_than_or_equal_to=_parse_rfc3339( + time_last_seen_less_than_or_equal_to + ), + hostname_contains=hostname_contains, + limit=limit, + sort_order=_normalize_enum(sort_order), + sort_by=_normalize_choice( + sort_by, + [ + "id", + "timeLastSeen", + "timeRegistered", + "hostname", + "agentId", + "agentType", + "lifecycleState", + "availabilityStatus", + "fleetId", + "compartmentId", + "osFamily", + "osArchitecture", + "osDistribution", + "pluginVersion", + ], + ), + page=next_page, + )) + has_next_page = response.has_next_page + next_page = response.next_page if hasattr(response, "next_page") else None + + for item in response.data.items: + plugins.append(map_jms_plugin_summary(item)) + if limit is not None and len(plugins) >= limit: + break + + logger.info(f"Found {len(plugins)} JMS plugins") + return plugins + except Exception as e: + logger.error(f"Error in list_jms_plugins tool: {str(e)}") + raise + + +@mcp.tool(description="Get a JMS plugin by its OCID.") +def get_jms_plugin( + jms_plugin_id: str = Field(..., description="The OCID of the JMS plugin.") +) -> JmsPlugin: + """Fetch a single JMS plugin by OCID.""" + try: + client = get_jms_client() + response: oci.response.Response = client.get_jms_plugin(jms_plugin_id=jms_plugin_id) + logger.info("Found JMS plugin") + return map_jms_plugin(response.data) + except Exception as e: + logger.error(f"Error in get_jms_plugin tool: {str(e)}") + raise + + +@mcp.tool(description="List Java installation sites in a JMS fleet.") +def list_installation_sites( + fleet_id: str = Field(..., description="The OCID of the fleet."), + jre_vendor: Optional[str] = Field(None, description="Filter by JRE vendor."), + jre_distribution: Optional[str] = Field(None, description="Filter by JRE distribution."), + jre_version: Optional[str] = Field(None, description="Filter by JRE version."), + installation_path: Optional[str] = Field(None, description="Filter by installation path."), + application_id: Optional[str] = Field(None, description="Filter by application identifier."), + managed_instance_id: Optional[str] = Field( + None, description="Filter by managed instance identifier." + ), + os_family: Optional[list[str]] = Field(None, description="Filter by operating system family."), + jre_security_status: Optional[str] = Field( + None, + description=( + "Filter by JRE security status. Accepted values include EARLY_ACCESS, UNKNOWN, " + "UP_TO_DATE, UPDATE_REQUIRED, and UPGRADE_REQUIRED." + ), + ), + path_contains: Optional[str] = Field( + None, + description="Filter installation sites where the path contains this value.", + ), + time_start: Optional[str] = Field( + None, + description="Search start time in RFC3339 format.", + ), + time_end: Optional[str] = Field( + None, + description="Search end time in RFC3339 format.", + ), + limit: Optional[int] = Field(None, description="Maximum number of installation sites to return.", ge=1), + sort_order: Optional[str] = Field( + None, description="Sort order for installation sites: ASC or DESC." + ), + sort_by: Optional[str] = Field(None, description="Field to sort installation sites by."), +) -> list[InstallationSiteSummary]: + """List Java installation sites in a fleet with optional runtime, host, and time filters.""" + installation_sites: list[InstallationSiteSummary] = [] + + try: + client = get_jms_client() + + response: oci.response.Response | None = None + has_next_page = True + next_page: Optional[str] = None + + while has_next_page and (limit is None or len(installation_sites) < limit): + # Convert RFC3339 strings before handing them to the OCI SDK search parameters. + response = client.list_installation_sites(**_omit_none( + fleet_id=fleet_id, + jre_vendor=jre_vendor, + jre_distribution=jre_distribution, + jre_version=jre_version, + installation_path=installation_path, + application_id=application_id, + managed_instance_id=managed_instance_id, + os_family=_normalize_enum_list(os_family), + jre_security_status=_normalize_enum(jre_security_status), + path_contains=path_contains, + time_start=_parse_rfc3339(time_start), + time_end=_parse_rfc3339(time_end), + limit=limit, + sort_order=_normalize_enum(sort_order), + sort_by=_normalize_choice( + sort_by, + [ + "managedInstanceId", + "jreDistribution", + "jreVendor", + "jreVersion", + "path", + "approximateApplicationCount", + "osName", + "securityStatus", + ], + ), + page=next_page, + )) + has_next_page = response.has_next_page + next_page = response.next_page if hasattr(response, "next_page") else None + + for item in response.data.items: + installation_sites.append(map_installation_site_summary(item)) + if limit is not None and len(installation_sites) >= limit: + break + + logger.info(f"Found {len(installation_sites)} installation sites") + return installation_sites + except Exception as e: + logger.error(f"Error in list_installation_sites tool: {str(e)}") + raise + + +@mcp.tool(description="Get fleet agent configuration for a JMS fleet.") +def get_fleet_agent_configuration( + fleet_id: str = Field(..., description="The OCID of the fleet.") +) -> FleetAgentConfiguration: + """Return the fleet-wide agent configuration for a JMS fleet.""" + try: + client = get_jms_client() + response: oci.response.Response = client.get_fleet_agent_configuration(fleet_id=fleet_id) + logger.info("Found fleet agent configuration") + return map_fleet_agent_configuration(response.data) + except Exception as e: + logger.error(f"Error in get_fleet_agent_configuration tool: {str(e)}") + raise + + +@mcp.tool(description="Get advanced feature configuration for a JMS fleet.") +def get_fleet_advanced_feature_configuration( + fleet_id: str = Field(..., description="The OCID of the fleet.") +) -> FleetAdvancedFeatureConfiguration: + """Return the advanced feature configuration for a JMS fleet.""" + try: + client = get_jms_client() + response: oci.response.Response = client.get_fleet_advanced_feature_configuration( + fleet_id=fleet_id + ) + logger.info("Found fleet advanced feature configuration") + return map_fleet_advanced_feature_configuration(response.data) + except Exception as e: + logger.error(f"Error in get_fleet_advanced_feature_configuration tool: {str(e)}") + raise + + +@mcp.tool(description="Summarize JMS resource inventory in a compartment.") +def summarize_resource_inventory( + compartment_id: Optional[str] = Field( + None, + description="The OCID of the compartment in which to summarize inventory.", + ), + compartment_id_in_subtree: bool = Field( + False, + description="Whether to include the compartment subtree in the summary.", + ), + time_start: Optional[str] = Field( + None, + description="Summary start time in RFC3339 format.", + ), + time_end: Optional[str] = Field( + None, + description="Summary end time in RFC3339 format.", + ), +) -> ResourceInventory: + """Summarize high-level JMS resource counts for a compartment and optional time range.""" + try: + client = get_jms_client() + response: oci.response.Response = client.summarize_resource_inventory(**_omit_none( + compartment_id=compartment_id, + compartment_id_in_subtree=compartment_id_in_subtree, + time_start=_parse_rfc3339(time_start), + time_end=_parse_rfc3339(time_end), + )) + logger.info("Summarized resource inventory") + return map_resource_inventory(response.data) + except Exception as e: + logger.error(f"Error in summarize_resource_inventory tool: {str(e)}") + raise + + +@mcp.tool(description="Summarize managed instance usage within a JMS fleet.") +def summarize_managed_instance_usage( + fleet_id: str = Field(..., description="The OCID of the fleet."), + managed_instance_id: Optional[str] = Field( + None, + description="Filter by managed instance OCID.", + ), + managed_instance_type: Optional[str] = Field( + None, + description=( + "Filter by managed instance type. Accepted values include " + "ORACLE_MANAGEMENT_AGENT and ORACLE_CLOUD_AGENT." + ), + ), + jre_vendor: Optional[str] = Field(None, description="Filter by JRE vendor."), + jre_distribution: Optional[str] = Field(None, description="Filter by JRE distribution."), + jre_version: Optional[str] = Field(None, description="Filter by JRE version."), + installation_path: Optional[str] = Field(None, description="Filter by installation path."), + application_id: Optional[str] = Field(None, description="Filter by application identifier."), + fields: Optional[list[str]] = Field( + None, description="Additional fields to include in each usage record." + ), + time_start: Optional[str] = Field(None, description="Summary start time in RFC3339 format."), + time_end: Optional[str] = Field(None, description="Summary end time in RFC3339 format."), + limit: Optional[int] = Field(None, description="Maximum number of usage records to return.", ge=1), + sort_order: Optional[str] = Field( + None, description="Sort order for the usage results: ASC or DESC." + ), + sort_by: Optional[str] = Field(None, description="Field to sort usage results by."), + os_family: Optional[list[str]] = Field(None, description="Filter by operating system family."), + hostname_contains: Optional[str] = Field( + None, + description="Filter the list with hostname contains this value.", + ), + library_key: Optional[str] = Field(None, description="Filter by library key."), +) -> list[ManagedInstanceUsage]: + """Summarize managed instance usage records for a fleet with optional filters.""" + usage: list[ManagedInstanceUsage] = [] + + try: + client = get_jms_client() + + response: oci.response.Response | None = None + has_next_page = True + next_page: Optional[str] = None + + while has_next_page and (limit is None or len(usage) < limit): + # The summarize API is paginated even though it returns aggregate-style records. + response = client.summarize_managed_instance_usage(**_omit_none( + fleet_id=fleet_id, + managed_instance_id=managed_instance_id, + managed_instance_type=_normalize_enum(managed_instance_type), + jre_vendor=jre_vendor, + jre_distribution=jre_distribution, + jre_version=jre_version, + installation_path=installation_path, + application_id=application_id, + fields=_normalize_choice_list( + fields, + [ + "approximateJreCount", + "approximateInstallationCount", + "approximateApplicationCount", + ], + ), + time_start=_parse_rfc3339(time_start), + time_end=_parse_rfc3339(time_end), + limit=limit, + sort_order=_normalize_enum(sort_order), + sort_by=_normalize_choice( + sort_by, + [ + "timeFirstSeen", + "timeLastSeen", + "approximateJreCount", + "approximateInstallationCount", + "approximateApplicationCount", + "osName", + ], + ), + os_family=_normalize_enum_list(os_family), + hostname_contains=hostname_contains, + library_key=library_key, + page=next_page, + )) + has_next_page = response.has_next_page + next_page = response.next_page if hasattr(response, "next_page") else None + + for item in response.data.items: + usage.append(map_managed_instance_usage(item)) + if limit is not None and len(usage) >= limit: + break + + logger.info(f"Found {len(usage)} managed instance usage records") + return usage + except Exception as e: + logger.error(f"Error in summarize_managed_instance_usage tool: {str(e)}") + raise + + +@mcp.tool(description="Summarize fleet health using JMS fleet diagnoses and fleet errors.") +def summarize_fleet_health( + fleet_id: str = Field(..., description="The OCID of the fleet.") +) -> FleetHealthSummary: + """Return a chat-friendly health summary for a JMS fleet.""" + try: + client = get_jms_client() + diagnoses = [ + map_fleet_diagnosis(item) + for item in _collect_paginated_items(client.list_fleet_diagnoses, fleet_id=fleet_id) + ] + fleet_errors = [ + map_fleet_error(item) + for item in _collect_paginated_items(client.list_fleet_errors, fleet_id=fleet_id) + ] + diagnoses = [item for item in diagnoses if item is not None] + fleet_errors = [item for item in fleet_errors if item is not None] + + return FleetHealthSummary( + fleet_id=fleet_id, + diagnosis_count=len(diagnoses), + fleet_errors=fleet_errors, + top_issue_categories=_derive_top_issue_categories(diagnoses, fleet_errors), + overall_health_status=_derive_overall_health_status(diagnoses, fleet_errors), + recommended_next_checks=_derive_recommended_next_checks(diagnoses, fleet_errors), + ) + except Exception as e: + logger.error(f"Error in summarize_fleet_health tool: {str(e)}") + raise + + +@mcp.tool(description="Get detailed fleet health diagnostics using JMS diagnoses and fleet errors.") +def get_fleet_health_diagnostics( + fleet_id: str = Field(..., description="The OCID of the fleet.") +) -> FleetHealthDiagnostics: + """Return detailed fleet diagnoses and fleet errors for drill-down troubleshooting.""" + try: + client = get_jms_client() + diagnoses = [ + map_fleet_diagnosis(item) + for item in _collect_paginated_items(client.list_fleet_diagnoses, fleet_id=fleet_id) + ] + fleet_errors = [ + map_fleet_error(item) + for item in _collect_paginated_items(client.list_fleet_errors, fleet_id=fleet_id) + ] + diagnoses = [item for item in diagnoses if item is not None] + fleet_errors = [item for item in fleet_errors if item is not None] + + return FleetHealthDiagnostics( + fleet_id=fleet_id, + diagnoses=diagnoses, + fleet_errors=fleet_errors, + diagnosis_count=len(diagnoses), + fleet_error_count=len(fleet_errors), + ) + except Exception as e: + logger.error(f"Error in get_fleet_health_diagnostics tool: {str(e)}") + raise + + +@mcp.tool(description="List JMS announcements and notices.") +def list_jms_notices( + summary_contains: Optional[str] = Field( + None, + description="Filter notices whose summary contains this value.", + ), + time_start: Optional[str] = Field(None, description="Search start time in RFC3339 format."), + time_end: Optional[str] = Field(None, description="Search end time in RFC3339 format."), + limit: Optional[int] = Field(None, description="Maximum number of notices to return.", ge=1), + sort_order: Optional[str] = Field(None, description="Sort order for notices: ASC or DESC."), + sort_by: Optional[str] = Field( + None, + description="Field to sort notices by: timeReleased or summary.", + ), +) -> list[JmsNotice]: + """List JMS notices and announcements with optional text, time, and sort filters.""" + try: + client = get_jms_client() + notices = [ + map_jms_notice(item) + for item in _collect_paginated_items( + client.list_announcements, + summary_contains=summary_contains, + time_start=_parse_rfc3339(time_start), + time_end=_parse_rfc3339(time_end), + limit=limit, + sort_order=_normalize_enum(sort_order), + sort_by=_normalize_choice(sort_by, ["timeReleased", "summary"]), + ) + ] + return [item for item in notices if item is not None] + except Exception as e: + logger.error(f"Error in list_jms_notices tool: {str(e)}") + raise + + +@mcp.tool(description="Summarize Java runtime compliance for a JMS fleet.") +def java_runtime_compliance( + fleet_id: str = Field(..., description="The OCID of the fleet.") +) -> JavaRuntimeComplianceReport: + """Return a fleet-level Java runtime compliance report enriched with release metadata.""" + try: + client = get_jms_client() + usage_rows = _collect_paginated_items( + client.summarize_jre_usage, + fleet_id=fleet_id, + fields=[ + "approximateInstallationCount", + "approximateApplicationCount", + "approximateManagedInstanceCount", + ], + ) + + release_cache: dict[str, object | None] = {} + version_breakdown: list[JavaRuntimeComplianceBucket] = [] + total_runtimes = 0 + up_to_date_runtimes = 0 + runtimes_requiring_update = 0 + runtimes_requiring_upgrade = 0 + unknown_runtimes = 0 + vendor_values: list[tuple[Optional[str], int]] = [] + distribution_values: list[tuple[Optional[str], int]] = [] + outdated_installations: list[OutdatedJavaInstallation] = [] + + for row in usage_rows: + version = getattr(row, "version", None) + vendor = getattr(row, "vendor", None) + distribution = getattr(row, "distribution", None) + security_status = getattr(row, "security_status", None) + runtime_count = _normalize_count(getattr(row, "approximate_installation_count", None)) + total_runtimes += runtime_count + + if security_status == "UP_TO_DATE": + up_to_date_runtimes += runtime_count + elif security_status == "UPDATE_REQUIRED": + runtimes_requiring_update += runtime_count + elif security_status == "UPGRADE_REQUIRED": + runtimes_requiring_upgrade += runtime_count + else: + unknown_runtimes += runtime_count + + vendor_values.append((vendor, runtime_count)) + distribution_values.append((distribution, runtime_count)) + + if version not in release_cache: + # Reuse release lookups across identical versions so the report + # stays bounded even when multiple usage rows share a release. + release_cache[version] = _safe_get_java_release(client, version) + release = release_cache[version] + + version_breakdown.append( + JavaRuntimeComplianceBucket( + version=version, + vendor=vendor, + distribution=distribution, + security_status=security_status, + runtime_count=runtime_count, + approximate_managed_instance_count=getattr( + row, "approximate_managed_instance_count", None + ), + approximate_application_count=getattr( + row, "approximate_application_count", None + ), + release_date=getattr(release, "release_date", None) if release else None, + days_under_security_baseline=getattr( + release, "days_under_security_baseline", None + ) + if release + else None, + license_type=getattr(release, "license_type", None) if release else None, + release_type=getattr(release, "release_type", None) if release else None, + release_notes_url=getattr(release, "release_notes_url", None) if release else None, + ) + ) + + if ( + security_status in {"UPDATE_REQUIRED", "UPGRADE_REQUIRED"} + and len(outdated_installations) < _MAX_OUTDATED_INSTALLATIONS + ): + # Drill down only for outdated buckets and cap the sample so the + # tool remains readable while still surfacing concrete fixes. + remaining = _MAX_OUTDATED_INSTALLATIONS - len(outdated_installations) + sites = _collect_paginated_items( + client.list_installation_sites, + fleet_id=fleet_id, + jre_version=version, + jre_vendor=vendor, + jre_distribution=distribution, + jre_security_status=security_status, + limit=remaining, + ) + for site in sites: + mapped_site = map_installation_site_summary(site) + if mapped_site is None: + continue + outdated_installations.append( + OutdatedJavaInstallation( + installation_key=mapped_site.installation_key, + managed_instance_id=mapped_site.managed_instance_id, + path=mapped_site.path, + version=mapped_site.jre.version if mapped_site.jre else None, + vendor=mapped_site.jre.vendor if mapped_site.jre else None, + distribution=mapped_site.jre.distribution if mapped_site.jre else None, + security_status=mapped_site.security_status, + time_last_seen=mapped_site.time_last_seen, + ) + ) + + version_breakdown.sort( + key=lambda item: ( + -(item.runtime_count or 0), + item.version or "", + item.vendor or "", + item.distribution or "", + ) + ) + + return JavaRuntimeComplianceReport( + fleet_id=fleet_id, + total_runtimes_in_fleet=total_runtimes, + up_to_date_runtimes=up_to_date_runtimes, + runtimes_requiring_update=runtimes_requiring_update, + runtimes_requiring_upgrade=runtimes_requiring_upgrade, + unknown_runtimes=unknown_runtimes, + version_breakdown=version_breakdown, + vendor_breakdown=_build_runtime_count_breakdowns(vendor_values), + distribution_breakdown=_build_runtime_count_breakdowns(distribution_values), + outdated_installations=outdated_installations, + ) + except Exception as e: + logger.error(f"Error in java_runtime_compliance tool: {str(e)}") + raise + + +def main(): + """Run the JMS MCP server over stdio.""" + mcp.run(transport="stdio") + + +if __name__ == "__main__": + main() diff --git a/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/tests/test_jms_models.py b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/tests/test_jms_models.py new file mode 100644 index 00000000..22f14949 --- /dev/null +++ b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/tests/test_jms_models.py @@ -0,0 +1,201 @@ +""" +Copyright (c) 2025, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +from datetime import datetime, UTC + +import oci + +from oracle.oci_jms_mcp_server.models import ( + map_fleet_diagnosis, + map_fleet_error, + map_jms_notice, + map_fleet, + map_fleet_advanced_feature_configuration, + map_fleet_agent_configuration, + map_fleet_summary, + map_installation_site_summary, + map_jms_plugin, + map_jms_plugin_summary, + map_managed_instance_usage, + map_resource_inventory, +) + + +def test_map_fleet_summary(): + now = datetime.now(UTC) + fleet = oci.jms.models.FleetSummary( + id="fleet1", + display_name="Fleet 1", + compartment_id="compartment1", + time_created=now, + ) + + result = map_fleet_summary(fleet) + + assert result.id == "fleet1" + assert result.display_name == "Fleet 1" + assert result.time_created == now + + +def test_map_fleet(): + fleet = oci.jms.models.Fleet(id="fleet1", display_name="Fleet 1") + result = map_fleet(fleet) + assert result.id == "fleet1" + + +def test_map_jms_plugin_summary(): + now = datetime.now(UTC) + plugin = oci.jms.models.JmsPluginSummary( + id="plugin1", + agent_type="OMA", + time_last_seen=now, + ) + + result = map_jms_plugin_summary(plugin) + + assert result.id == "plugin1" + assert result.agent_type == "OMA" + assert result.time_last_seen == now + + +def test_map_jms_plugin(): + plugin = oci.jms.models.JmsPlugin(id="plugin1", hostname="host1") + result = map_jms_plugin(plugin) + assert result.hostname == "host1" + + +def test_map_installation_site_summary_with_nested_values(): + now = datetime.now(UTC) + site = oci.jms.models.InstallationSiteSummary( + installation_key="install1", + managed_instance_id="mi1", + jre=oci.jms.models.JavaRuntimeId(version="17", vendor="Oracle", distribution="JDK"), + operating_system=oci.jms.models.OperatingSystem( + family="LINUX", + name="Linux", + version="9", + architecture="x86_64", + ), + time_last_seen=now, + ) + + result = map_installation_site_summary(site) + + assert result.installation_key == "install1" + assert result.jre.version == "17" + assert result.operating_system.family == "LINUX" + assert result.time_last_seen == now + + +def test_map_fleet_agent_configuration_with_nested_os_configuration(): + config = oci.jms.models.FleetAgentConfiguration( + jre_scan_frequency_in_minutes=60, + java_usage_tracker_processing_frequency_in_minutes=15, + linux_configuration=oci.jms.models.FleetAgentOsConfiguration(include_paths=["/usr/java"]), + ) + + result = map_fleet_agent_configuration(config) + + assert result.jre_scan_frequency_in_minutes == 60 + assert result.linux_configuration.include_paths == ["/usr/java"] + + +def test_map_fleet_advanced_feature_configuration(): + config = oci.jms.models.FleetAdvancedFeatureConfiguration( + analytic_namespace="analytics_ns", + analytic_bucket_name="bucket", + ) + + result = map_fleet_advanced_feature_configuration(config) + + assert result.analytic_namespace == "analytics_ns" + assert result.analytic_bucket_name == "bucket" + + +def test_map_managed_instance_usage(): + now = datetime.now(UTC) + usage = oci.jms.models.ManagedInstanceUsage( + managed_instance_id="mi1", + managed_instance_type="ORACLE_MANAGEMENT_AGENT", + hostname="host1", + time_first_seen=now, + ) + + result = map_managed_instance_usage(usage) + + assert result.managed_instance_id == "mi1" + assert result.hostname == "host1" + assert result.time_first_seen == now + + +def test_map_resource_inventory(): + inventory = oci.jms.models.ResourceInventory( + active_fleet_count=1, + managed_instance_count=2, + jre_count=3, + installation_count=4, + application_count=5, + ) + + result = map_resource_inventory(inventory) + + assert result.active_fleet_count == 1 + assert result.application_count == 5 + + +def test_map_fleet_diagnosis(): + diagnosis = oci.jms.models.FleetDiagnosisSummary( + resource_diagnosis="Inventory scan issue", + resource_id="resource1", + resource_state="FAILED", + resource_type="JMS_FLEET", + ) + + result = map_fleet_diagnosis(diagnosis) + + assert result.resource_diagnosis == "Inventory scan issue" + assert result.resource_id == "resource1" + assert result.resource_state == "UNKNOWN_ENUM_VALUE" + + +def test_map_fleet_error_with_nested_details(): + now = datetime.now(UTC) + fleet_error = oci.jms.models.FleetErrorSummary( + fleet_id="fleet1", + fleet_name="Fleet 1", + time_first_seen=now, + errors=[ + oci.jms.models.FleetErrorDetails( + reason="Agent connectivity failure", + details="Critical reporting failure", + time_last_seen=now, + ) + ], + ) + + result = map_fleet_error(fleet_error) + + assert result.fleet_id == "fleet1" + assert result.fleet_name == "Fleet 1" + assert result.time_first_seen == now + assert result.errors[0].reason == "UNKNOWN_ENUM_VALUE" + assert result.errors[0].details == "Critical reporting failure" + + +def test_map_jms_notice(): + now = datetime.now(UTC) + notice = oci.jms.models.AnnouncementSummary( + key=1001, + summary="Planned maintenance", + time_released=now, + url="https://example.com", + ) + + result = map_jms_notice(notice) + + assert result.key == 1001 + assert result.summary == "Planned maintenance" + assert result.time_released == now diff --git a/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/tests/test_jms_tools.py b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/tests/test_jms_tools.py new file mode 100644 index 00000000..baba5edf --- /dev/null +++ b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/tests/test_jms_tools.py @@ -0,0 +1,1279 @@ +""" +Copyright (c) 2025, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +import os +from datetime import UTC, datetime +from unittest.mock import MagicMock, create_autospec, mock_open, patch + +import fastmcp.exceptions +import oci +import pytest +from fastmcp import Client +from oracle.oci_jms_mcp_server import server +from oracle.oci_jms_mcp_server.server import mcp + + +class TestJmsTools: + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_fleets_paginates(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response_page_1 = create_autospec(oci.response.Response) + response_page_1.data = oci.jms.models.FleetCollection( + items=[ + oci.jms.models.FleetSummary(id="fleet1", display_name="Fleet 1"), + ] + ) + response_page_1.has_next_page = True + response_page_1.next_page = "token" + + response_page_2 = create_autospec(oci.response.Response) + response_page_2.data = oci.jms.models.FleetCollection( + items=[ + oci.jms.models.FleetSummary(id="fleet2", display_name="Fleet 2"), + ] + ) + response_page_2.has_next_page = False + response_page_2.next_page = None + + mock_client.list_fleets.side_effect = [response_page_1, response_page_2] + + async with Client(mcp) as client: + result = ( + await client.call_tool("list_fleets", {"compartment_id": "ocid1.compartment.oc1..test"}) + ).structured_content["result"] + + assert [item["id"] for item in result] == ["fleet1", "fleet2"] + assert "lifecycle_state" not in mock_client.list_fleets.call_args.kwargs + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_fleets_normalizes_lifecycle_state(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.FleetCollection( + items=[oci.jms.models.FleetSummary(id="fleet1", display_name="Fleet 1")] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_fleets.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_fleets", + { + "compartment_id": "ocid1.compartment.oc1..test", + "lifecycle_state": "needs-attention", + }, + ) + ).structured_content["result"] + + assert result[0]["id"] == "fleet1" + assert ( + mock_client.list_fleets.call_args.kwargs["lifecycle_state"] == "NEEDS_ATTENTION" + ) + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_fleets_normalizes_sort_order(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.FleetCollection( + items=[oci.jms.models.FleetSummary(id="fleet1", display_name="Fleet 1")] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_fleets.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_fleets", + { + "compartment_id": "ocid1.compartment.oc1..test", + "sort_order": "desc", + }, + ) + ).structured_content["result"] + + assert result[0]["id"] == "fleet1" + assert mock_client.list_fleets.call_args.kwargs["sort_order"] == "DESC" + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_fleets_ignores_blank_lifecycle_state(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.FleetCollection( + items=[oci.jms.models.FleetSummary(id="fleet1", display_name="Fleet 1")] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_fleets.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_fleets", + { + "compartment_id": "ocid1.compartment.oc1..test", + "lifecycle_state": " ", + }, + ) + ).structured_content["result"] + + assert result[0]["id"] == "fleet1" + assert "lifecycle_state" not in mock_client.list_fleets.call_args.kwargs + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_fleets_normalizes_sort_by(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.FleetCollection( + items=[oci.jms.models.FleetSummary(id="fleet1", display_name="Fleet 1")] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_fleets.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_fleets", + { + "compartment_id": "ocid1.compartment.oc1..test", + "sort_by": "time_created", + }, + ) + ).structured_content["result"] + + assert result[0]["id"] == "fleet1" + assert mock_client.list_fleets.call_args.kwargs["sort_by"] == "timeCreated" + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_fleets_exception(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + mock_client.list_fleets.side_effect = oci.exceptions.ServiceError( + status=500, + code="InternalServerError", + message="Internal server error", + opc_request_id="req", + headers={}, + ) + + async with Client(mcp) as client: + with pytest.raises(fastmcp.exceptions.ToolError): + await client.call_tool("list_fleets", {"compartment_id": "ocid1.compartment.oc1..test"}) + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_get_fleet(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.Fleet(id="fleet1", display_name="Fleet 1") + mock_client.get_fleet.return_value = response + + async with Client(mcp) as client: + result = (await client.call_tool("get_fleet", {"fleet_id": "fleet1"})).structured_content + assert result["id"] == "fleet1" + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_get_fleet_exception(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + mock_client.get_fleet.side_effect = oci.exceptions.ServiceError( + status=404, + code="NotAuthorizedOrNotFound", + message="Not found", + opc_request_id="req", + headers={}, + ) + + async with Client(mcp) as client: + with pytest.raises(fastmcp.exceptions.ToolError): + await client.call_tool("get_fleet", {"fleet_id": "fleet1"}) + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_jms_plugins(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.JmsPluginCollection( + items=[oci.jms.models.JmsPluginSummary(id="plugin1", hostname="host1")] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_jms_plugins.return_value = response + + async with Client(mcp) as client: + result = (await client.call_tool("list_jms_plugins", {"fleet_id": "fleet1"})).structured_content[ + "result" + ] + assert result[0]["id"] == "plugin1" + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_get_jms_plugin(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.JmsPlugin(id="plugin1", hostname="host1") + mock_client.get_jms_plugin.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool("get_jms_plugin", {"jms_plugin_id": "plugin1"}) + ).structured_content + assert result["id"] == "plugin1" + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_installation_sites(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.InstallationSiteCollection( + items=[ + oci.jms.models.InstallationSiteSummary( + installation_key="install1", + managed_instance_id="mi1", + path="/usr/java", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_installation_sites.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool("list_installation_sites", {"fleet_id": "fleet1"}) + ).structured_content["result"] + assert result[0]["installation_key"] == "install1" + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_installation_sites_normalizes_os_family(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.InstallationSiteCollection( + items=[ + oci.jms.models.InstallationSiteSummary( + installation_key="install1", + managed_instance_id="mi1", + path="/usr/java", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_installation_sites.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_installation_sites", + {"fleet_id": "fleet1", "os_family": ["linux"]}, + ) + ).structured_content["result"] + + assert result[0]["installation_key"] == "install1" + assert mock_client.list_installation_sites.call_args.kwargs["os_family"] == ["LINUX"] + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_installation_sites_omits_blank_os_family_entries(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.InstallationSiteCollection( + items=[ + oci.jms.models.InstallationSiteSummary( + installation_key="install1", + managed_instance_id="mi1", + path="/usr/java", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_installation_sites.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_installation_sites", + {"fleet_id": "fleet1", "os_family": ["linux", " "]}, + ) + ).structured_content["result"] + + assert result[0]["installation_key"] == "install1" + assert mock_client.list_installation_sites.call_args.kwargs["os_family"] == ["LINUX"] + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_installation_sites_omits_os_family_when_only_blank_values(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.InstallationSiteCollection( + items=[ + oci.jms.models.InstallationSiteSummary( + installation_key="install1", + managed_instance_id="mi1", + path="/usr/java", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_installation_sites.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_installation_sites", + {"fleet_id": "fleet1", "os_family": [" "]}, + ) + ).structured_content["result"] + + assert result[0]["installation_key"] == "install1" + assert "os_family" not in mock_client.list_installation_sites.call_args.kwargs + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_installation_sites_normalizes_sort_by(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.InstallationSiteCollection( + items=[ + oci.jms.models.InstallationSiteSummary( + installation_key="install1", + managed_instance_id="mi1", + path="/usr/java", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_installation_sites.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_installation_sites", + {"fleet_id": "fleet1", "sort_by": "managed_instance_id"}, + ) + ).structured_content["result"] + + assert result[0]["installation_key"] == "install1" + assert ( + mock_client.list_installation_sites.call_args.kwargs["sort_by"] + == "managedInstanceId" + ) + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_get_fleet_agent_configuration(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.FleetAgentConfiguration( + jre_scan_frequency_in_minutes=60, + java_usage_tracker_processing_frequency_in_minutes=10, + ) + mock_client.get_fleet_agent_configuration.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool("get_fleet_agent_configuration", {"fleet_id": "fleet1"}) + ).structured_content + assert result["jre_scan_frequency_in_minutes"] == 60 + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_get_fleet_advanced_feature_configuration(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.FleetAdvancedFeatureConfiguration( + analytic_namespace="analytics_ns", + analytic_bucket_name="bucket", + ) + mock_client.get_fleet_advanced_feature_configuration.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "get_fleet_advanced_feature_configuration", {"fleet_id": "fleet1"} + ) + ).structured_content + assert result["analytic_namespace"] == "analytics_ns" + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_summarize_resource_inventory(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.ResourceInventory( + active_fleet_count=1, + managed_instance_count=2, + jre_count=3, + installation_count=4, + application_count=5, + ) + mock_client.summarize_resource_inventory.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "summarize_resource_inventory", + {"compartment_id": "ocid1.compartment.oc1..test"}, + ) + ).structured_content + assert result["active_fleet_count"] == 1 + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_summarize_resource_inventory_omits_blank_time_start(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.ResourceInventory( + active_fleet_count=1, + managed_instance_count=2, + jre_count=3, + installation_count=4, + application_count=5, + ) + mock_client.summarize_resource_inventory.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "summarize_resource_inventory", + { + "compartment_id": "ocid1.compartment.oc1..test", + "time_start": "", + }, + ) + ).structured_content + + assert result["active_fleet_count"] == 1 + assert "time_start" not in mock_client.summarize_resource_inventory.call_args.kwargs + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_summarize_managed_instance_usage(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.ManagedInstanceUsageCollection( + items=[ + oci.jms.models.ManagedInstanceUsage( + managed_instance_id="mi1", + managed_instance_type="ORACLE_MANAGEMENT_AGENT", + hostname="host1", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.summarize_managed_instance_usage.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool("summarize_managed_instance_usage", {"fleet_id": "fleet1"}) + ).structured_content["result"] + assert result[0]["managed_instance_id"] == "mi1" + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_summarize_managed_instance_usage_normalizes_fields_and_sort_by( + self, mock_get_client + ): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.ManagedInstanceUsageCollection( + items=[ + oci.jms.models.ManagedInstanceUsage( + managed_instance_id="mi1", + managed_instance_type="ORACLE_MANAGEMENT_AGENT", + hostname="host1", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.summarize_managed_instance_usage.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "summarize_managed_instance_usage", + { + "fleet_id": "fleet1", + "fields": ["approximate_jre_count"], + "sort_by": "time_first_seen", + }, + ) + ).structured_content["result"] + + assert result[0]["managed_instance_id"] == "mi1" + assert mock_client.summarize_managed_instance_usage.call_args.kwargs["fields"] == [ + "approximateJreCount" + ] + assert ( + mock_client.summarize_managed_instance_usage.call_args.kwargs["sort_by"] + == "timeFirstSeen" + ) + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_summarize_managed_instance_usage_omits_blank_os_family_entries_and_time_end( + self, mock_get_client + ): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.ManagedInstanceUsageCollection( + items=[ + oci.jms.models.ManagedInstanceUsage( + managed_instance_id="mi1", + managed_instance_type="ORACLE_MANAGEMENT_AGENT", + hostname="host1", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.summarize_managed_instance_usage.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "summarize_managed_instance_usage", + { + "fleet_id": "fleet1", + "os_family": ["linux", " "], + "time_end": " ", + }, + ) + ).structured_content["result"] + + assert result[0]["managed_instance_id"] == "mi1" + assert mock_client.summarize_managed_instance_usage.call_args.kwargs["os_family"] == [ + "LINUX" + ] + assert "time_end" not in mock_client.summarize_managed_instance_usage.call_args.kwargs + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_summarize_fleet_health_with_issues(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + diagnoses_page = create_autospec(oci.response.Response) + diagnoses_page.data = oci.jms.models.FleetDiagnosisCollection( + items=[ + oci.jms.models.FleetDiagnosisSummary( + resource_id="resource1", + resource_diagnosis="Inventory scan issue", + ) + ] + ) + diagnoses_page.has_next_page = False + diagnoses_page.next_page = None + + error_detail = oci.jms.models.FleetErrorDetails( + details="Critical agent reporting failure", + reason="Agent connectivity failure", + time_last_seen=datetime.now(UTC), + ) + fleet_errors_page = create_autospec(oci.response.Response) + fleet_errors_page.data = oci.jms.models.FleetErrorCollection( + items=[ + oci.jms.models.FleetErrorSummary( + fleet_id="fleet1", + fleet_name="Fleet 1", + errors=[error_detail], + ) + ] + ) + fleet_errors_page.has_next_page = False + fleet_errors_page.next_page = None + + mock_client.list_fleet_diagnoses.return_value = diagnoses_page + mock_client.list_fleet_errors.return_value = fleet_errors_page + + async with Client(mcp) as client: + result = ( + await client.call_tool("summarize_fleet_health", {"fleet_id": "fleet1"}) + ).structured_content + + assert result["fleet_id"] == "fleet1" + assert result["diagnosis_count"] == 1 + assert result["overall_health_status"] == "CRITICAL" + assert "Inventory scan issue" in result["top_issue_categories"] + assert "Critical agent reporting failure" in result["top_issue_categories"] + assert "Check JMS notices for any known service-side issues or advisories." in result[ + "recommended_next_checks" + ] + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_summarize_fleet_health_healthy(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + diagnoses_page = create_autospec(oci.response.Response) + diagnoses_page.data = oci.jms.models.FleetDiagnosisCollection(items=[]) + diagnoses_page.has_next_page = False + diagnoses_page.next_page = None + + fleet_errors_page = create_autospec(oci.response.Response) + fleet_errors_page.data = oci.jms.models.FleetErrorCollection(items=[]) + fleet_errors_page.has_next_page = False + fleet_errors_page.next_page = None + + mock_client.list_fleet_diagnoses.return_value = diagnoses_page + mock_client.list_fleet_errors.return_value = fleet_errors_page + + async with Client(mcp) as client: + result = ( + await client.call_tool("summarize_fleet_health", {"fleet_id": "fleet1"}) + ).structured_content + + assert result["overall_health_status"] == "HEALTHY" + assert result["recommended_next_checks"] == [] + assert result["fleet_errors"] == [] + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_get_fleet_health_diagnostics_paginates(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + diagnoses_page_1 = create_autospec(oci.response.Response) + diagnoses_page_1.data = oci.jms.models.FleetDiagnosisCollection( + items=[oci.jms.models.FleetDiagnosisSummary(resource_id="resource1")] + ) + diagnoses_page_1.has_next_page = True + diagnoses_page_1.next_page = "token" + + diagnoses_page_2 = create_autospec(oci.response.Response) + diagnoses_page_2.data = oci.jms.models.FleetDiagnosisCollection( + items=[oci.jms.models.FleetDiagnosisSummary(resource_id="resource2")] + ) + diagnoses_page_2.has_next_page = False + diagnoses_page_2.next_page = None + + fleet_errors_page = create_autospec(oci.response.Response) + fleet_errors_page.data = oci.jms.models.FleetErrorCollection( + items=[oci.jms.models.FleetErrorSummary(fleet_id="fleet1")] + ) + fleet_errors_page.has_next_page = False + fleet_errors_page.next_page = None + + mock_client.list_fleet_diagnoses.side_effect = [diagnoses_page_1, diagnoses_page_2] + mock_client.list_fleet_errors.return_value = fleet_errors_page + + async with Client(mcp) as client: + result = ( + await client.call_tool("get_fleet_health_diagnostics", {"fleet_id": "fleet1"}) + ).structured_content + + assert result["diagnosis_count"] == 2 + assert result["fleet_error_count"] == 1 + assert [item["resource_id"] for item in result["diagnoses"]] == [ + "resource1", + "resource2", + ] + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_summarize_fleet_health_exception(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + mock_client.list_fleet_diagnoses.side_effect = oci.exceptions.ServiceError( + status=500, + code="InternalServerError", + message="Internal server error", + opc_request_id="req", + headers={}, + ) + + async with Client(mcp) as client: + with pytest.raises(fastmcp.exceptions.ToolError): + await client.call_tool("summarize_fleet_health", {"fleet_id": "fleet1"}) + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_jms_notices_normalizes_sort_by_and_time_filters(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + response = create_autospec(oci.response.Response) + response.data = oci.jms.models.AnnouncementCollection( + items=[ + oci.jms.models.AnnouncementSummary( + key=1001, + summary="Planned maintenance", + time_released=datetime.now(UTC), + url="https://example.com", + ) + ] + ) + response.has_next_page = False + response.next_page = None + mock_client.list_announcements.return_value = response + + async with Client(mcp) as client: + result = ( + await client.call_tool( + "list_jms_notices", + { + "summary_contains": "maintenance", + "time_start": "2026-03-01T00:00:00Z", + "time_end": "2026-03-02T00:00:00Z", + "sort_order": "desc", + "sort_by": "time_released", + }, + ) + ).structured_content["result"] + + assert result[0]["key"] == 1001 + assert mock_client.list_announcements.call_args.kwargs["sort_order"] == "DESC" + assert mock_client.list_announcements.call_args.kwargs["sort_by"] == "timeReleased" + assert ( + mock_client.list_announcements.call_args.kwargs["time_start"].isoformat() + == "2026-03-01T00:00:00+00:00" + ) + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_list_jms_notices_paginates_with_limit(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + page_1 = create_autospec(oci.response.Response) + page_1.data = oci.jms.models.AnnouncementCollection( + items=[ + oci.jms.models.AnnouncementSummary(key=1001), + oci.jms.models.AnnouncementSummary(key=1002), + ] + ) + page_1.has_next_page = True + page_1.next_page = "token" + + page_2 = create_autospec(oci.response.Response) + page_2.data = oci.jms.models.AnnouncementCollection( + items=[oci.jms.models.AnnouncementSummary(key=1003)] + ) + page_2.has_next_page = False + page_2.next_page = None + + mock_client.list_announcements.side_effect = [page_1, page_2] + + async with Client(mcp) as client: + result = ( + await client.call_tool("list_jms_notices", {"limit": 2}) + ).structured_content["result"] + + assert [item["key"] for item in result] == [1001, 1002] + assert mock_client.list_announcements.call_count == 1 + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_java_runtime_compliance_with_release_enrichment_and_drilldown( + self, mock_get_client + ): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + usage_response = create_autospec(oci.response.Response) + usage_response.data = oci.jms.models.JreUsageCollection( + items=[ + oci.jms.models.JreUsage( + id="jre1", + fleet_id="fleet1", + version="17.0.10", + vendor="Oracle", + distribution="JDK", + security_status="UPDATE_REQUIRED", + approximate_installation_count=3, + approximate_managed_instance_count=2, + approximate_application_count=5, + ), + oci.jms.models.JreUsage( + id="jre2", + fleet_id="fleet1", + version="21.0.2", + vendor="Oracle", + distribution="JDK", + security_status="UP_TO_DATE", + approximate_installation_count=4, + approximate_managed_instance_count=2, + approximate_application_count=6, + ), + ] + ) + usage_response.has_next_page = False + usage_response.next_page = None + mock_client.summarize_jre_usage.return_value = usage_response + + release_update = create_autospec(oci.response.Response) + release_update.data = oci.jms.models.JavaRelease( + release_version="17.0.10", + security_status="UPDATE_REQUIRED", + release_type="CPU", + license_type="NFTC", + ) + release_current = create_autospec(oci.response.Response) + release_current.data = oci.jms.models.JavaRelease( + release_version="21.0.2", + security_status="UP_TO_DATE", + release_type="CPU", + license_type="NFTC", + ) + mock_client.get_java_release.side_effect = [release_update, release_current] + + site_response = create_autospec(oci.response.Response) + site_response.data = oci.jms.models.InstallationSiteCollection( + items=[ + oci.jms.models.InstallationSiteSummary( + installation_key="install1", + managed_instance_id="mi1", + path="/usr/java/jdk-17", + jre=oci.jms.models.JavaRuntimeId( + version="17.0.10", vendor="Oracle", distribution="JDK" + ), + security_status="UPDATE_REQUIRED", + time_last_seen=datetime.now(UTC), + ) + ] + ) + site_response.has_next_page = False + site_response.next_page = None + mock_client.list_installation_sites.return_value = site_response + + async with Client(mcp) as client: + result = ( + await client.call_tool("java_runtime_compliance", {"fleet_id": "fleet1"}) + ).structured_content + + assert result["fleet_id"] == "fleet1" + assert result["total_runtimes_in_fleet"] == 7 + assert result["up_to_date_runtimes"] == 4 + assert result["runtimes_requiring_update"] == 3 + assert result["runtimes_requiring_upgrade"] == 0 + assert result["version_breakdown"][0]["license_type"] == "NFTC" + assert result["outdated_installations"][0]["installation_key"] == "install1" + assert mock_client.list_installation_sites.call_count == 1 + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_java_runtime_compliance_paginates_usage(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + page_1 = create_autospec(oci.response.Response) + page_1.data = oci.jms.models.JreUsageCollection( + items=[ + oci.jms.models.JreUsage( + id="jre1", + fleet_id="fleet1", + version="17.0.10", + vendor="Oracle", + distribution="JDK", + security_status="UPDATE_REQUIRED", + approximate_installation_count=1, + ) + ] + ) + page_1.has_next_page = True + page_1.next_page = "token" + + page_2 = create_autospec(oci.response.Response) + page_2.data = oci.jms.models.JreUsageCollection( + items=[ + oci.jms.models.JreUsage( + id="jre2", + fleet_id="fleet1", + version="21.0.2", + vendor="Oracle", + distribution="JDK", + security_status="UP_TO_DATE", + approximate_installation_count=2, + ) + ] + ) + page_2.has_next_page = False + page_2.next_page = None + + mock_client.summarize_jre_usage.side_effect = [page_1, page_2] + release_1 = create_autospec(oci.response.Response) + release_1.data = oci.jms.models.JavaRelease( + release_version="17.0.10" + ) + release_2 = create_autospec(oci.response.Response) + release_2.data = oci.jms.models.JavaRelease( + release_version="21.0.2" + ) + mock_client.get_java_release.side_effect = [release_1, release_2] + empty_sites = create_autospec(oci.response.Response) + empty_sites.data = oci.jms.models.InstallationSiteCollection(items=[]) + empty_sites.has_next_page = False + empty_sites.next_page = None + mock_client.list_installation_sites.return_value = empty_sites + + async with Client(mcp) as client: + result = ( + await client.call_tool("java_runtime_compliance", {"fleet_id": "fleet1"}) + ).structured_content + + assert result["total_runtimes_in_fleet"] == 3 + assert len(result["version_breakdown"]) == 2 + + @pytest.mark.asyncio + @patch("oracle.oci_jms_mcp_server.server.get_jms_client") + async def test_java_runtime_compliance_ignores_missing_release_metadata(self, mock_get_client): + mock_client = MagicMock() + mock_get_client.return_value = mock_client + + usage_response = create_autospec(oci.response.Response) + usage_response.data = oci.jms.models.JreUsageCollection( + items=[ + oci.jms.models.JreUsage( + id="jre1", + fleet_id="fleet1", + version="11.0.0-custom", + vendor="Oracle", + distribution="JDK", + security_status="UNKNOWN", + approximate_installation_count=1, + ) + ] + ) + usage_response.has_next_page = False + usage_response.next_page = None + mock_client.summarize_jre_usage.return_value = usage_response + mock_client.get_java_release.side_effect = oci.exceptions.ServiceError( + status=404, + code="NotAuthorizedOrNotFound", + message="Not found", + opc_request_id="req", + headers={}, + ) + + async with Client(mcp) as client: + result = ( + await client.call_tool("java_runtime_compliance", {"fleet_id": "fleet1"}) + ).structured_content + + assert result["unknown_runtimes"] == 1 + assert result["version_breakdown"][0]["release_type"] is None + + +class TestServerMain: + @patch("oracle.oci_jms_mcp_server.server.mcp.run") + def test_main_uses_stdio_transport(self, mock_mcp_run): + server.main() + mock_mcp_run.assert_called_once_with(transport="stdio") + + +class TestGetJmsClient: + @patch("oracle.oci_jms_mcp_server.server.oci.jms.JavaManagementServiceClient") + @patch("oracle.oci_jms_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_jms_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_jms_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_jms_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_jms_mcp_server.server.os.getenv") + def test_get_jms_client( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + mock_getenv.side_effect = lambda key, default=None: ( + "MYPROFILE" if key == "OCI_CONFIG_PROFILE" else default + ) + config = {"key_file": "/abs/key.pem", "security_token_file": "/abs/token"} + mock_from_file.return_value = config + private_key = object() + mock_load_private_key.return_value = private_key + + result = server.get_jms_client() + + mock_from_file.assert_called_once_with( + file_location=oci.config.DEFAULT_LOCATION, + profile_name="MYPROFILE", + ) + mock_open_file.assert_called_once_with("/abs/token", "r") + mock_security_token_signer.assert_called_once_with("SECURITY_TOKEN", private_key) + passed_config = mock_client.call_args[0][0] + assert passed_config is config + assert "additional_user_agent" in config + assert mock_client.call_args.kwargs == {"signer": mock_security_token_signer.return_value} + assert result == mock_client.return_value + + @patch("oracle.oci_jms_mcp_server.server.oci.jms.JavaManagementServiceClient") + @patch("oracle.oci_jms_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_jms_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_jms_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_jms_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_jms_mcp_server.server.os.getenv") + def test_get_jms_client_with_jms_test_environment_override( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + values = { + "OCI_CONFIG_PROFILE": "MYPROFILE", + "JMS_TEST_ENVIRONMENT": "DEV-canary", + } + mock_getenv.side_effect = lambda key, default=None: values.get(key, default) + config = { + "key_file": "/abs/key.pem", + "security_token_file": "/abs/token", + "region": "us-ashburn-1", + } + mock_from_file.return_value = config + mock_load_private_key.return_value = object() + + server.get_jms_client() + + assert mock_client.call_args.kwargs == { + "signer": mock_security_token_signer.return_value, + "service_endpoint": "https://javamanagement-dev.us-ashburn-1.oci.oc-test.com", + } + + @patch("oracle.oci_jms_mcp_server.server.oci.jms.JavaManagementServiceClient") + @patch("oracle.oci_jms_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_jms_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_jms_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_jms_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_jms_mcp_server.server.os.getenv") + def test_get_jms_client_with_test_endpoint_override( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + values = { + "JMS_TEST_ENVIRONMENT": "TEST", + } + mock_getenv.side_effect = lambda key, default=None: values.get(key, default) + config = { + "key_file": "/abs/key.pem", + "security_token_file": "/abs/token", + "region": "us-ashburn-1", + } + mock_from_file.return_value = config + mock_load_private_key.return_value = object() + + server.get_jms_client() + + assert mock_client.call_args.kwargs == { + "signer": mock_security_token_signer.return_value, + "service_endpoint": "https://javamanagement-test.us-ashburn-1.oci.oc-test.com", + } + + @patch("oracle.oci_jms_mcp_server.server.oci.jms.JavaManagementServiceClient") + @patch("oracle.oci_jms_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_jms_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_jms_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_jms_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_jms_mcp_server.server.os.getenv") + def test_get_jms_client_with_herds_endpoint_override( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + mock_client, + ): + values = { + "JMS_TEST_ENVIRONMENT": "HERDS", + } + mock_getenv.side_effect = lambda key, default=None: values.get(key, default) + config = { + "key_file": "/abs/key.pem", + "security_token_file": "/abs/token", + "region": "eu-frankfurt-1", + } + mock_from_file.return_value = config + mock_load_private_key.return_value = object() + + server.get_jms_client() + + assert mock_client.call_args.kwargs == { + "signer": mock_security_token_signer.return_value, + "service_endpoint": "https://javamanagement-herds.eu-frankfurt-1.oci.rbcloud.oc-test.com", + } + + @patch("oracle.oci_jms_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_jms_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_jms_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_jms_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_jms_mcp_server.server.os.getenv") + def test_get_jms_client_rejects_unknown_jms_test_environment( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + ): + values = { + "JMS_TEST_ENVIRONMENT": "BOGUS", + } + mock_getenv.side_effect = lambda key, default=None: values.get(key, default) + mock_from_file.return_value = { + "key_file": "/abs/key.pem", + "security_token_file": "/abs/token", + "region": "us-ashburn-1", + } + mock_load_private_key.return_value = object() + + with pytest.raises(ValueError, match="Unsupported JMS_TEST_ENVIRONMENT"): + server.get_jms_client() + + @patch("oracle.oci_jms_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_jms_mcp_server.server.oci.signer.load_private_key_from_file") + @patch( + "oracle.oci_jms_mcp_server.server.open", + new_callable=mock_open, + read_data="SECURITY_TOKEN", + ) + @patch("oracle.oci_jms_mcp_server.server.oci.config.from_file") + @patch("oracle.oci_jms_mcp_server.server.os.getenv") + def test_get_jms_client_requires_region_for_jms_test_environment( + self, + mock_getenv, + mock_from_file, + mock_open_file, + mock_load_private_key, + mock_security_token_signer, + ): + values = { + "JMS_TEST_ENVIRONMENT": "DEV2", + } + mock_getenv.side_effect = lambda key, default=None: values.get(key, default) + mock_from_file.return_value = { + "key_file": "/abs/key.pem", + "security_token_file": "/abs/token", + } + mock_load_private_key.return_value = object() + + with pytest.raises(ValueError, match="OCI config does not contain a region"): + server.get_jms_client() + + @patch("oracle.oci_jms_mcp_server.server.logger") + @patch("oracle.oci_jms_mcp_server.server.os.getenv") + def test_load_oci_config_warns_on_oci_api_mcp_env_aliases(self, mock_getenv, mock_logger): + values = { + "OCI_CONFIG": "/Users/test/.oci/config", + "OCI_PROFILE": "DEFAULT", + } + mock_getenv.side_effect = lambda key, default=None: values.get(key, default) + + with patch("oracle.oci_jms_mcp_server.server.oci.config.from_file") as mock_from_file: + server._load_oci_config() + + mock_from_file.assert_called_once_with( + file_location=oci.config.DEFAULT_LOCATION, + profile_name=oci.config.DEFAULT_PROFILE, + ) + assert mock_logger.warning.call_count == 2 + + @patch("oracle.oci_jms_mcp_server.server.open", new_callable=mock_open, read_data="SECURITY_TOKEN\n") + @patch("oracle.oci_jms_mcp_server.server.oci.auth.signers.SecurityTokenSigner") + @patch("oracle.oci_jms_mcp_server.server.oci.signer.load_private_key_from_file") + def test_build_security_token_signer_strips_token( + self, + mock_load_private_key, + mock_security_token_signer, + mock_open_file, + ): + config = { + "key_file": "/abs/key.pem", + "security_token_file": "~/.oci/sessions/DEFAULT/token", + } + private_key = object() + mock_load_private_key.return_value = private_key + + server._build_security_token_signer(config) + + mock_open_file.assert_called_once_with(os.path.expanduser(config["security_token_file"]), "r") + mock_security_token_signer.assert_called_once_with("SECURITY_TOKEN", private_key) diff --git a/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/util.py b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/util.py new file mode 100644 index 00000000..e2c474de --- /dev/null +++ b/src/oci-jms-mcp-server/oracle/oci_jms_mcp_server/util.py @@ -0,0 +1,72 @@ +""" +Copyright (c) 2025, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +import os +import re +from typing import Optional + +_JMS_TEST_ENVIRONMENT = "JMS_TEST_ENVIRONMENT" +_JMS_TEST_ENV_NAME = re.compile(r"^([a-zA-Z0-9]+)([_-]+[\w-]*)?$") +_SUPPORTED_JMS_ENVIRONMENTS = { + "PROD", + "HERDS", + "DEV", + "DEV2", + "DEV3", + "TEST", + "TEST2", + "TEST3", + "STAGE", + "VANILLA", +} + + +def get_jms_environment(value: Optional[str]) -> Optional[str]: + """Parse the JMS environment selector, allowing Java-style optional suffixes.""" + if value is None: + return None + + normalized = value.strip() + if not normalized: + return None + + match = _JMS_TEST_ENV_NAME.match(normalized) + if not match: + raise ValueError( + f"Unable to parse {_JMS_TEST_ENVIRONMENT}={value!r}; expected a base environment name " + "such as PROD, HERDS, DEV, DEV2, DEV3, TEST, TEST2, TEST3, STAGE, or VANILLA." + ) + + environment = match.group(1).upper() + if environment not in _SUPPORTED_JMS_ENVIRONMENTS: + supported = ", ".join(sorted(_SUPPORTED_JMS_ENVIRONMENTS)) + raise ValueError( + f"Unsupported {_JMS_TEST_ENVIRONMENT} base environment {environment!r}; " + f"supported values are: {supported}." + ) + + return environment + + +def get_jms_service_endpoint(config: dict, environment_value: Optional[str] = None) -> Optional[str]: + """Derive a Java-style JMS service endpoint override for non-prod environments.""" + environment = get_jms_environment( + os.getenv(_JMS_TEST_ENVIRONMENT) if environment_value is None else environment_value + ) + if environment is None or environment == "PROD": + return None + + region = config.get("region") + if not region: + raise ValueError( + f"{_JMS_TEST_ENVIRONMENT} is set to {environment!r} but the OCI config does not " + "contain a region." + ) + + if environment == "HERDS": + return f"https://javamanagement-herds.{region}.oci.rbcloud.oc-test.com" + + return f"https://javamanagement-{environment.lower()}.{region}.oci.oc-test.com" diff --git a/src/oci-jms-mcp-server/pyproject.toml b/src/oci-jms-mcp-server/pyproject.toml new file mode 100644 index 00000000..314ff754 --- /dev/null +++ b/src/oci-jms-mcp-server/pyproject.toml @@ -0,0 +1,55 @@ +[project] +name = "oracle.oci-jms-mcp-server" +version = "1.0.0" +description = "OCI Java Management Service MCP server" +readme = "README.md" +requires-python = ">=3.13" +license = "UPL-1.0" +authors = [ + {name = "Oracle MCP", email = "237432095+oracle-mcp@users.noreply.github.com"}, +] +dependencies = [ + "fastmcp==2.14.2", + "oci==2.160.0", + "pydantic==2.12.3", +] + +classifiers = [ + "License :: OSI Approved :: Universal Permissive License (UPL)", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3.13", +] + +[project.scripts] +"oracle.oci-jms-mcp-server" = "oracle.oci_jms_mcp_server.server:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["oracle"] + +[dependency-groups] +dev = [ + "pytest>=8.4.2", + "pytest-asyncio>=1.2.0", + "pytest-cov>=7.0.0", +] + +[tool.coverage.run] +omit = [ + "**/__init__.py", + "**/tests/*", + "dist/*", + ".venv/*", +] + +[tool.coverage.report] +omit = [ + "**/__init__.py", + "**/tests/*", +] +precision = 2 +fail_under = 90 diff --git a/src/oci-jms-mcp-server/uv.lock b/src/oci-jms-mcp-server/uv.lock new file mode 100644 index 00000000..0a6f5b95 --- /dev/null +++ b/src/oci-jms-mcp-server/uv.lock @@ -0,0 +1,1391 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, +] + +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + +[[package]] +name = "cachetools" +version = "7.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "circuitbreaker" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ac/de7a92c4ed39cba31fe5ad9203b76a25ca67c530797f6bb420fff5f65ccb/circuitbreaker-2.1.3.tar.gz", hash = "sha256:1a4baee510f7bea3c91b194dcce7c07805fe96c4423ed5594b75af438531d084", size = 10787, upload-time = "2025-03-31T08:12:08.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/34/15f08edd4628f65217de1fc3c1a27c82e46fe357d60c217fc9881e12ebcc/circuitbreaker-2.1.3-py3-none-any.whl", hash = "sha256:87ba6a3ed03fdc7032bc175561c2b04d52ade9d5faf94ca2b035fbdc5e6b1dd1", size = 7737, upload-time = "2025-03-31T08:12:07.802Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, +] + +[[package]] +name = "cronsim" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/1a/02f105147f7f2e06ed4f734ff5a6439590bb275a53dd91fc73df6312298a/cronsim-2.7-py3-none-any.whl", hash = "sha256:1e1431fa08c51dc7f72e67e571c7c7a09af26420169b607badd4ca9677ffad1e", size = 14213, upload-time = "2025-10-21T16:38:20.431Z" }, +] + +[[package]] +name = "cryptography" +version = "44.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/d6/1411ab4d6108ab167d06254c5be517681f1e331f90edf1379895bcb87020/cryptography-44.0.3.tar.gz", hash = "sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053", size = 711096, upload-time = "2025-05-02T19:36:04.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/53/c776d80e9d26441bb3868457909b4e74dd9ccabd182e10b2b0ae7a07e265/cryptography-44.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88", size = 6670281, upload-time = "2025-05-02T19:34:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/6a/06/af2cf8d56ef87c77319e9086601bef621bedf40f6f59069e1b6d1ec498c5/cryptography-44.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137", size = 3959305, upload-time = "2025-05-02T19:34:53.042Z" }, + { url = "https://files.pythonhosted.org/packages/ae/01/80de3bec64627207d030f47bf3536889efee8913cd363e78ca9a09b13c8e/cryptography-44.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c", size = 4171040, upload-time = "2025-05-02T19:34:54.675Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/bb16b7541d207a19d9ae8b541c70037a05e473ddc72ccb1386524d4f023c/cryptography-44.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76", size = 3963411, upload-time = "2025-05-02T19:34:56.61Z" }, + { url = "https://files.pythonhosted.org/packages/42/b2/7d31f2af5591d217d71d37d044ef5412945a8a8e98d5a2a8ae4fd9cd4489/cryptography-44.0.3-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359", size = 3689263, upload-time = "2025-05-02T19:34:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/25/50/c0dfb9d87ae88ccc01aad8eb93e23cfbcea6a6a106a9b63a7b14c1f93c75/cryptography-44.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43", size = 4196198, upload-time = "2025-05-02T19:35:00.988Z" }, + { url = "https://files.pythonhosted.org/packages/66/c9/55c6b8794a74da652690c898cb43906310a3e4e4f6ee0b5f8b3b3e70c441/cryptography-44.0.3-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01", size = 3966502, upload-time = "2025-05-02T19:35:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f7/7cb5488c682ca59a02a32ec5f975074084db4c983f849d47b7b67cc8697a/cryptography-44.0.3-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d", size = 4196173, upload-time = "2025-05-02T19:35:05.018Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0b/2f789a8403ae089b0b121f8f54f4a3e5228df756e2146efdf4a09a3d5083/cryptography-44.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904", size = 4087713, upload-time = "2025-05-02T19:35:07.187Z" }, + { url = "https://files.pythonhosted.org/packages/1d/aa/330c13655f1af398fc154089295cf259252f0ba5df93b4bc9d9c7d7f843e/cryptography-44.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44", size = 4299064, upload-time = "2025-05-02T19:35:08.879Z" }, + { url = "https://files.pythonhosted.org/packages/10/a8/8c540a421b44fd267a7d58a1fd5f072a552d72204a3f08194f98889de76d/cryptography-44.0.3-cp37-abi3-win32.whl", hash = "sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d", size = 2773887, upload-time = "2025-05-02T19:35:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0d/c4b1657c39ead18d76bbd122da86bd95bdc4095413460d09544000a17d56/cryptography-44.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d", size = 3209737, upload-time = "2025-05-02T19:35:12.12Z" }, + { url = "https://files.pythonhosted.org/packages/34/a3/ad08e0bcc34ad436013458d7528e83ac29910943cea42ad7dd4141a27bbb/cryptography-44.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f", size = 6673501, upload-time = "2025-05-02T19:35:13.775Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f0/7491d44bba8d28b464a5bc8cc709f25a51e3eac54c0a4444cf2473a57c37/cryptography-44.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759", size = 3960307, upload-time = "2025-05-02T19:35:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c8/e5c5d0e1364d3346a5747cdcd7ecbb23ca87e6dea4f942a44e88be349f06/cryptography-44.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645", size = 4170876, upload-time = "2025-05-02T19:35:18.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/96/025cb26fc351d8c7d3a1c44e20cf9a01e9f7cf740353c9c7a17072e4b264/cryptography-44.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2", size = 3964127, upload-time = "2025-05-02T19:35:19.864Z" }, + { url = "https://files.pythonhosted.org/packages/01/44/eb6522db7d9f84e8833ba3bf63313f8e257729cf3a8917379473fcfd6601/cryptography-44.0.3-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54", size = 3689164, upload-time = "2025-05-02T19:35:21.449Z" }, + { url = "https://files.pythonhosted.org/packages/68/fb/d61a4defd0d6cee20b1b8a1ea8f5e25007e26aeb413ca53835f0cae2bcd1/cryptography-44.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93", size = 4198081, upload-time = "2025-05-02T19:35:23.187Z" }, + { url = "https://files.pythonhosted.org/packages/1b/50/457f6911d36432a8811c3ab8bd5a6090e8d18ce655c22820994913dd06ea/cryptography-44.0.3-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c", size = 3967716, upload-time = "2025-05-02T19:35:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/35/6e/dca39d553075980ccb631955c47b93d87d27f3596da8d48b1ae81463d915/cryptography-44.0.3-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f", size = 4197398, upload-time = "2025-05-02T19:35:27.678Z" }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1f2fe681eabc682067c66a74addd46c887ebacf39038ba01f8860338d3d/cryptography-44.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5", size = 4087900, upload-time = "2025-05-02T19:35:29.312Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f5/3599e48c5464580b73b236aafb20973b953cd2e7b44c7c2533de1d888446/cryptography-44.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b", size = 4301067, upload-time = "2025-05-02T19:35:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/d2c48c8137eb39d0c193274db5c04a75dab20d2f7c3f81a7dcc3a8897701/cryptography-44.0.3-cp39-abi3-win32.whl", hash = "sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028", size = 2775467, upload-time = "2025-05-02T19:35:33.805Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/51f212198681ea7b0deaaf8846ee10af99fba4e894f67b353524eab2bbe5/cryptography-44.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334", size = 3210375, upload-time = "2025-05-02T19:35:35.369Z" }, +] + +[[package]] +name = "cyclopts" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/e7/3e26855c046ac527cf94d890f6698e703980337f22ea7097e02b35b910f9/cyclopts-4.10.0.tar.gz", hash = "sha256:0ae04a53274e200ef3477c8b54de63b019bc6cd0162d75c718bf40c9c3fb5268", size = 166394, upload-time = "2026-03-14T14:09:31.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/06/d68a5d5d292c2ad2bc6a02e5ca2cb1bb9c15e941ab02f004a06a342d7f0f/cyclopts-4.10.0-py3-none-any.whl", hash = "sha256:50f333382a60df8d40ec14aa2e627316b361c4f478598ada1f4169d959bf9ea7", size = 204097, upload-time = "2026-03-14T14:09:32.504Z" }, +] + +[[package]] +name = "diskcache" +version = "5.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fakeredis" +version = "2.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/40/fd09efa66205eb32253d2b2ebc63537281384d2040f0a88bcd2289e120e4/fakeredis-2.34.1.tar.gz", hash = "sha256:4ff55606982972eecce3ab410e03d746c11fe5deda6381d913641fbd8865ea9b", size = 177315, upload-time = "2026-02-25T13:17:51.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b5/82f89307d0d769cd9bf46a54fb9136be08e4e57c5570ae421db4c9a2ba62/fakeredis-2.34.1-py3-none-any.whl", hash = "sha256:0107ec99d48913e7eec2a5e3e2403d1bd5f8aa6489d1a634571b975289c48f12", size = 122160, upload-time = "2026-02-25T13:17:49.701Z" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + +[[package]] +name = "fastmcp" +version = "2.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "cyclopts" }, + { name = "exceptiongroup" }, + { name = "httpx" }, + { name = "jsonschema-path" }, + { name = "mcp" }, + { name = "openapi-pydantic" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, + { name = "pydocket" }, + { name = "pyperclip" }, + { name = "python-dotenv" }, + { name = "rich" }, + { name = "uvicorn" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/1e/e3528227688c248283f6d86869b1e900563ffc223eff00f4f923d2750365/fastmcp-2.14.2.tar.gz", hash = "sha256:bd23d1b808b6f446444f10114dac468b11bfb9153ed78628f5619763d0cf573e", size = 8272966, upload-time = "2025-12-31T15:26:13.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/67/8456d39484fcb7afd0defed21918e773ed59a98b39e5b633328527c88367/fastmcp-2.14.2-py3-none-any.whl", hash = "sha256:e33cd622e1ebd5110af6a981804525b6cd41072e3c7d68268ed69ef3be651aca", size = 413279, upload-time = "2025-12-31T15:26:11.178Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/7b/c3081ff1af947915503121c649f26a778e1a2101fd525f74aef997d75b7e/jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581", size = 15832, upload-time = "2026-03-07T15:46:04.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/49/c152890d49102b280ecf86ba5f80a8c111c3a155dafa3bd24aeb64fde9e1/jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808", size = 7005, upload-time = "2026-03-07T15:46:03.515Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-path" +version = "0.4.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pathable" }, + { name = "pyyaml" }, + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "keyring" +version = "25.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, +] + +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" }, + { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" }, + { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" }, + { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" }, + { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" }, + { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" }, + { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" }, + { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" }, + { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" }, + { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" }, + { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "mcp" +version = "1.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + +[[package]] +name = "oci" +version = "2.160.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "circuitbreaker" }, + { name = "cryptography" }, + { name = "pyopenssl" }, + { name = "python-dateutil" }, + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/7b/c9d7fc28f11c25c7875db3584eab5d52ccb2d7df553d07ac47f19a14d075/oci-2.160.0.tar.gz", hash = "sha256:f8e3410204c1405b40247179550cf74f5145a8e17025c4f2a92f2b9ffdc7d26b", size = 15601606, upload-time = "2025-09-09T04:17:43.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/51/752375a4e0d2de371c2788414157eda337417010d2ef7383cd7140388f1e/oci-2.160.0-py3-none-any.whl", hash = "sha256:3dba1ec671ebea23f255fabf836cb0fd08aea0913a8df85610fccaa5a4344ee9", size = 31715365, upload-time = "2025-09-09T04:17:34.998Z" }, +] + +[[package]] +name = "openapi-pydantic" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, +] + +[[package]] +name = "oracle-oci-jms-mcp-server" +version = "1.0.0" +source = { editable = "." } +dependencies = [ + { name = "fastmcp" }, + { name = "oci" }, + { name = "pydantic" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, +] + +[package.metadata] +requires-dist = [ + { name = "fastmcp", specifier = "==2.14.2" }, + { name = "oci", specifier = "==2.160.0" }, + { name = "pydantic", specifier = "==2.12.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-asyncio", specifier = ">=1.2.0" }, + { name = "pytest-cov", specifier = ">=7.0.0" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pathable" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" }, +] + +[[package]] +name = "pathvalidate" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + +[[package]] +name = "py-key-value-aio" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "py-key-value-shared" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" }, +] + +[package.optional-dependencies] +disk = [ + { name = "diskcache" }, + { name = "pathvalidate" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] +redis = [ + { name = "redis" }, +] + +[[package]] +name = "py-key-value-shared" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383, upload-time = "2025-10-17T15:04:21.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431, upload-time = "2025-10-17T15:04:19.346Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629, upload-time = "2025-10-14T10:21:00.006Z" }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049, upload-time = "2025-10-14T10:21:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409, upload-time = "2025-10-14T10:21:03.556Z" }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635, upload-time = "2025-10-14T10:21:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284, upload-time = "2025-10-14T10:21:07.122Z" }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566, upload-time = "2025-10-14T10:21:08.981Z" }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809, upload-time = "2025-10-14T10:21:10.805Z" }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119, upload-time = "2025-10-14T10:21:12.583Z" }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398, upload-time = "2025-10-14T10:21:14.584Z" }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735, upload-time = "2025-10-14T10:21:16.432Z" }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209, upload-time = "2025-10-14T10:21:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324, upload-time = "2025-10-14T10:21:20.363Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515, upload-time = "2025-10-14T10:21:22.339Z" }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819, upload-time = "2025-10-14T10:21:26.683Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866, upload-time = "2025-10-14T10:21:28.951Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034, upload-time = "2025-10-14T10:21:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022, upload-time = "2025-10-14T10:21:32.809Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495, upload-time = "2025-10-14T10:21:34.812Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131, upload-time = "2025-10-14T10:21:36.924Z" }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236, upload-time = "2025-10-14T10:21:38.927Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573, upload-time = "2025-10-14T10:21:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467, upload-time = "2025-10-14T10:21:44.018Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754, upload-time = "2025-10-14T10:21:46.466Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754, upload-time = "2025-10-14T10:21:48.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115, upload-time = "2025-10-14T10:21:50.63Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400, upload-time = "2025-10-14T10:21:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070, upload-time = "2025-10-14T10:21:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277, upload-time = "2025-10-14T10:21:57.474Z" }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608, upload-time = "2025-10-14T10:21:59.557Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614, upload-time = "2025-10-14T10:22:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904, upload-time = "2025-10-14T10:22:04.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538, upload-time = "2025-10-14T10:22:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.13.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, +] + +[[package]] +name = "pydocket" +version = "0.18.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "cronsim" }, + { name = "fakeredis", extra = ["lua"] }, + { name = "opentelemetry-api" }, + { name = "prometheus-client" }, + { name = "py-key-value-aio", extra = ["memory", "redis"] }, + { name = "python-json-logger" }, + { name = "redis" }, + { name = "rich" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, + { name = "uncalled-for" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/5f/82dde9fb6099b960a4203596d3b755d1bd2c0d0210fea104d015d6515d7f/pydocket-0.18.2.tar.gz", hash = "sha256:cc2051d15557f83bb164a83b0743fa9c12c2bfe9a9145cff3a5922b4935ce4f5", size = 354762, upload-time = "2026-03-10T13:09:22.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/cf/8c1b6340baf81d7f6c97fe0181bda7cfd500d5e33bf469fbffbdae07b3c9/pydocket-0.18.2-py3-none-any.whl", hash = "sha256:19e48de15e83370f750e362610b777533ff9c0fa48bf36766ed581f91d266556", size = 99041, upload-time = "2026-03-10T13:09:20.598Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pyopenssl" +version = "24.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/d4/1067b82c4fc674d6f6e9e8d26b3dff978da46d351ca3bac171544693e085/pyopenssl-24.3.0.tar.gz", hash = "sha256:49f7a019577d834746bc55c5fce6ecbcec0f2b4ec5ce1cf43a9a173b8138bb36", size = 178944, upload-time = "2024-11-27T20:43:12.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/22/40f9162e943f86f0fc927ebc648078be87def360d9d8db346619fb97df2b/pyOpenSSL-24.3.0-py3-none-any.whl", hash = "sha256:e474f5a473cd7f92221cc04976e48f4d11502804657a08a989fb3be5514c904a", size = 56111, upload-time = "2024-11-27T20:43:21.112Z" }, +] + +[[package]] +name = "pyperclip" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-json-logger" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "redis" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/82/4d1a5279f6c1251d3d2a603a798a1137c657de9b12cfc1fba4858232c4d2/redis-7.3.0.tar.gz", hash = "sha256:4d1b768aafcf41b01022410b3cc4f15a07d9b3d6fe0c66fc967da2c88e551034", size = 4928081, upload-time = "2026-03-06T18:18:16.287Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl", hash = "sha256:9d4fcb002a12a5e3c3fbe005d59c48a2cc231f87fbb2f6b70c2d89bb64fec364", size = 404379, upload-time = "2026-03-06T18:18:14.583Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" }, +] + +[[package]] +name = "starlette" +version = "0.52.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, +] + +[[package]] +name = "typer" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/24/cb09efec5cc954f7f9b930bf8279447d24618bb6758d4f6adf2574c41780/typer-0.24.1.tar.gz", hash = "sha256:e39b4732d65fbdcde189ae76cf7cd48aeae72919dea1fdfc16593be016256b45", size = 118613, upload-time = "2026-02-21T16:54:40.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uncalled-for" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +] diff --git a/tests/e2e/features/mcphost.json b/tests/e2e/features/mcphost.json index 314c65bb..58b76c6a 100644 --- a/tests/e2e/features/mcphost.json +++ b/tests/e2e/features/mcphost.json @@ -102,6 +102,31 @@ "CURL_CA_BUNDLE": "" } }, + "oracle-oci-jms-mcp-server": { + "disabled": false, + "timeout": 60, + "type": "stdio", + "command": "uv", + "args": [ + "run", + "python", + "-c", + "from oracle.oci_jms_mcp_server.server import main; main()" + ], + "env": { + "PYTHONPATH": "../../../src/oci-jms-mcp-server", + "PYTHONNOUSERSITE": "0", + "OCI_CONFIG_FILE": "${env:OCI_CONFIG_FILE}", + "UV_CACHE_DIR": "/tmp/uv-cache", + "HTTP_PROXY": "http://127.0.0.1:5000", + "HTTPS_PROXY": "http://127.0.0.1:5000", + "OCI_SDK_CERT_BUNDLE": "False", + "PYTHONHTTPSVERIFY": "0", + "OCI_SKIP_SSL_VERIFICATION": "True", + "REQUESTS_CA_BUNDLE": "", + "CURL_CA_BUNDLE": "" + } + }, "oracle-oci-logging-mcp-server": { "disabled": false, "timeout": 60, diff --git a/tests/e2e/features/mocks/services/jms_data.py b/tests/e2e/features/mocks/services/jms_data.py new file mode 100644 index 00000000..9f690aa8 --- /dev/null +++ b/tests/e2e/features/mocks/services/jms_data.py @@ -0,0 +1,288 @@ +""" +Copyright (c) 2026, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +FLEETS = [ + { + "id": "ocid1.jmsfleet.oc1..mock-fleet-1", + "displayName": "mock-jms-fleet", + "description": "Mock JMS fleet for e2e coverage", + "compartmentId": "ocid1.tenancy.oc1..mock", + "approximateJreCount": 7, + "approximateInstallationCount": 4, + "approximateApplicationCount": 3, + "approximateManagedInstanceCount": 2, + "approximateJavaServerCount": 1, + "inventoryLog": { + "logGroupId": "ocid1.loggroup.oc1..mock-jms-inventory", + "logId": "ocid1.log.oc1..mock-jms-inventory-log", + }, + "operationLog": { + "logGroupId": "ocid1.loggroup.oc1..mock-jms-operation", + "logId": "ocid1.log.oc1..mock-jms-operation-log", + }, + "isAdvancedFeaturesEnabled": True, + "isExportSettingEnabled": True, + "timeCreated": "2026-02-11T10:15:00Z", + "lifecycleState": "ACTIVE", + "freeformTags": {"env": "test"}, + } +] + +JMS_PLUGINS = [ + { + "id": "ocid1.jmsplugin.oc1..mock-plugin-1", + "agentId": "ocid1.managementagent.oc1..mock-agent-1", + "agentType": "OMA", + "lifecycleState": "ACTIVE", + "availabilityStatus": "ACTIVE", + "fleetId": "ocid1.jmsfleet.oc1..mock-fleet-1", + "compartmentId": "ocid1.tenancy.oc1..mock", + "hostname": "plugin-host-1", + "osFamily": "LINUX", + "osArchitecture": "X86_64", + "osDistribution": "Oracle Linux", + "pluginVersion": "1.2.3", + "timeRegistered": "2026-02-11T10:30:00Z", + "timeLastSeen": "2026-02-12T09:45:00Z", + }, + { + "id": "ocid1.jmsplugin.oc1..mock-plugin-2", + "agentId": "ocid1.managementagent.oc1..mock-agent-2", + "agentType": "OCA", + "lifecycleState": "ACTIVE", + "availabilityStatus": "SILENT", + "fleetId": "ocid1.jmsfleet.oc1..mock-fleet-1", + "compartmentId": "ocid1.tenancy.oc1..mock", + "hostname": "archive-host-2", + "osFamily": "LINUX", + "osArchitecture": "X86_64", + "osDistribution": "Oracle Linux", + "pluginVersion": "1.1.8", + "timeRegistered": "2026-02-10T08:15:00Z", + "timeLastSeen": "2026-02-10T08:45:00Z", + } +] + +INSTALLATION_SITES = { + "ocid1.jmsfleet.oc1..mock-fleet-1": [ + { + "installationKey": "installation-alpha", + "managedInstanceId": "managed-instance-1", + "jre": { + "version": "17.0.12", + "vendor": "Oracle", + "distribution": "JDK", + "jreKey": "jre-17-oracle", + }, + "securityStatus": "UP_TO_DATE", + "path": "/usr/lib/jvm/java-17-oracle", + "operatingSystem": { + "family": "LINUX", + "name": "Oracle Linux", + "distribution": "Oracle Linux", + "version": "9", + "architecture": "X86_64", + "managedInstanceCount": 1, + }, + "approximateApplicationCount": 2, + "timeLastSeen": "2026-02-12T09:45:00Z", + "lifecycleState": "ACTIVE", + }, + { + "installationKey": "installation-beta", + "managedInstanceId": "managed-instance-2", + "jre": { + "version": "17.0.10", + "vendor": "Oracle", + "distribution": "JDK", + "jreKey": "jre-17-10-oracle", + }, + "securityStatus": "UPDATE_REQUIRED", + "path": "/opt/java/jdk-17.0.10", + "operatingSystem": { + "family": "LINUX", + "name": "Oracle Linux", + "distribution": "Oracle Linux", + "version": "8", + "architecture": "X86_64", + "managedInstanceCount": 1, + }, + "approximateApplicationCount": 1, + "timeLastSeen": "2026-02-13T08:45:00Z", + "lifecycleState": "ACTIVE", + } + ] +} + +FLEET_AGENT_CONFIGURATIONS = { + "ocid1.jmsfleet.oc1..mock-fleet-1": { + "jreScanFrequencyInMinutes": 30, + "javaUsageTrackerProcessingFrequencyInMinutes": 15, + "workRequestValidityPeriodInDays": 7, + "agentPollingIntervalInMinutes": 5, + "linuxConfiguration": { + "includePaths": ["/u01/java", "/usr/lib/jvm"], + "excludePaths": ["/tmp/java-cache"], + }, + "windowsConfiguration": { + "includePaths": ["C:\\Java"], + "excludePaths": ["C:\\Temp\\Java"], + }, + "macOsConfiguration": { + "includePaths": ["/Library/Java/JavaVirtualMachines"], + "excludePaths": ["/tmp/java-cache"], + }, + "timeLastModified": "2026-02-12T10:00:00Z", + } +} + +FLEET_ADVANCED_FEATURE_CONFIGURATIONS = { + "ocid1.jmsfleet.oc1..mock-fleet-1": { + "analyticNamespace": "mock_jms_namespace", + "analyticBucketName": "jms_analytics_bucket", + "lcm": {"isEnabled": True}, + "cryptoEventAnalysis": {"isEnabled": True}, + "advancedUsageTracking": {"isEnabled": True}, + "jfrRecording": {"isEnabled": False}, + "performanceTuningAnalysis": {"isEnabled": False}, + "javaMigrationAnalysis": {"isEnabled": True}, + "timeLastModified": "2026-02-12T10:05:00Z", + } +} + +RESOURCE_INVENTORY = { + "activeFleetCount": 1, + "managedInstanceCount": 2, + "jreCount": 7, + "installationCount": 4, + "applicationCount": 42, +} + +MANAGED_INSTANCE_USAGE = { + "ocid1.jmsfleet.oc1..mock-fleet-1": [ + { + "managedInstanceId": "managed-instance-1", + "managedInstanceType": "ORACLE_MANAGEMENT_AGENT", + "hostname": "usage-host-1", + "hostId": "host-1", + "operatingSystem": { + "family": "LINUX", + "name": "Oracle Linux", + "distribution": "Oracle Linux", + "version": "9", + "architecture": "X86_64", + "managedInstanceCount": 1, + }, + "agent": {"version": "1.1.0"}, + "approximateApplicationCount": 2, + "approximateInstallationCount": 1, + "approximateJreCount": 1, + "drsFileStatus": "PRESENT", + "applicationInvokedBy": "opc", + "timeStart": "2026-02-12T08:00:00Z", + "timeEnd": "2026-02-12T09:00:00Z", + "timeFirstSeen": "2026-02-11T09:00:00Z", + "timeLastSeen": "2026-02-12T09:45:00Z", + } + ] +} + +FLEET_DIAGNOSES = { + "ocid1.jmsfleet.oc1..mock-fleet-1": [ + { + "resourceDiagnosis": "Inventory scan issue", + "resourceId": "ocid1.jmsfleet.oc1..mock-fleet-1", + "resourceState": "FAILED", + "resourceType": "JMS_FLEET", + }, + { + "resourceDiagnosis": "Plugin heartbeat warning", + "resourceId": "ocid1.jmsplugin.oc1..mock-plugin-1", + "resourceState": "NEEDS_ATTENTION", + "resourceType": "JMS_PLUGIN", + }, + ] +} + +FLEET_ERRORS = [ + { + "compartmentId": "ocid1.tenancy.oc1..mock", + "fleetId": "ocid1.jmsfleet.oc1..mock-fleet-1", + "fleetName": "mock-jms-fleet", + "errors": [ + { + "reason": "Agent connectivity failure", + "details": "Critical agent reporting failure for plugin-host-1", + "timeLastSeen": "2026-02-13T10:30:00Z", + } + ], + "timeFirstSeen": "2026-02-13T09:00:00Z", + "timeLastSeen": "2026-02-13T10:30:00Z", + } +] + +JMS_NOTICES = [ + { + "key": 1001, + "summary": "Planned JMS maintenance window", + "timeReleased": "2026-02-14T11:00:00Z", + "url": "https://example.oracle.test/jms/maintenance", + }, + { + "key": 1002, + "summary": "JMS advisory for plugin telemetry delays", + "timeReleased": "2026-02-12T06:00:00Z", + "url": "https://example.oracle.test/jms/advisory", + }, +] + +JRE_USAGE = { + "ocid1.jmsfleet.oc1..mock-fleet-1": [ + { + "id": "jre-usage-1", + "fleetId": "ocid1.jmsfleet.oc1..mock-fleet-1", + "version": "21.0.2", + "vendor": "Oracle", + "distribution": "JDK", + "securityStatus": "UP_TO_DATE", + "approximateInstallationCount": 4, + "approximateManagedInstanceCount": 2, + "approximateApplicationCount": 3, + }, + { + "id": "jre-usage-2", + "fleetId": "ocid1.jmsfleet.oc1..mock-fleet-1", + "version": "17.0.10", + "vendor": "Oracle", + "distribution": "JDK", + "securityStatus": "UPDATE_REQUIRED", + "approximateInstallationCount": 3, + "approximateManagedInstanceCount": 1, + "approximateApplicationCount": 1, + }, + ] +} + +JAVA_RELEASES = { + "21.0.2": { + "releaseVersion": "21.0.2", + "releaseDate": "2026-01-16T00:00:00Z", + "daysUnderSecurityBaseline": 0, + "licenseType": "NFTC", + "releaseType": "CPU", + "releaseNotesUrl": "https://example.oracle.test/jms/releases/21.0.2", + "securityStatus": "UP_TO_DATE", + }, + "17.0.10": { + "releaseVersion": "17.0.10", + "releaseDate": "2025-10-15T00:00:00Z", + "daysUnderSecurityBaseline": 30, + "licenseType": "NFTC", + "releaseType": "CPU", + "releaseNotesUrl": "https://example.oracle.test/jms/releases/17.0.10", + "securityStatus": "UPDATE_REQUIRED", + }, +} diff --git a/tests/e2e/features/mocks/services/jms_routes.py b/tests/e2e/features/mocks/services/jms_routes.py new file mode 100644 index 00000000..b81ec468 --- /dev/null +++ b/tests/e2e/features/mocks/services/jms_routes.py @@ -0,0 +1,291 @@ +""" +Copyright (c) 2026, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +from _common import oci_res +from flask import Blueprint, jsonify, request +from jms_data import ( + FLEET_DIAGNOSES, + FLEET_ADVANCED_FEATURE_CONFIGURATIONS, + FLEET_AGENT_CONFIGURATIONS, + FLEET_ERRORS, + FLEETS, + INSTALLATION_SITES, + JAVA_RELEASES, + JMS_NOTICES, + JMS_PLUGINS, + JRE_USAGE, + MANAGED_INSTANCE_USAGE, + RESOURCE_INVENTORY, +) + +jms_bp = Blueprint("jms", __name__, url_prefix="/20210610") + + +def _apply_limit(items): + try: + limit = request.args.get("limit") + if limit is not None: + lim = int(limit) + if lim >= 0: + return items[:lim] + except Exception: + pass + return items + + +def _find_fleet(fleet_id): + return next((fleet for fleet in FLEETS if fleet.get("id") == fleet_id), None) + + +def _find_plugin(plugin_id): + return next((plugin for plugin in JMS_PLUGINS if plugin.get("id") == plugin_id), None) + + +def _find_java_release(release_version): + return JAVA_RELEASES.get(release_version) + + +def _sort_items(items, sort_key, descending=False): + return sorted(items, key=lambda item: item.get(sort_key) or "", reverse=descending) + + +@jms_bp.route("/fleets", methods=["GET"]) +def list_fleets(): + items = FLEETS + + compartment_id = request.args.get("compartmentId") + if compartment_id: + items = [item for item in items if item.get("compartmentId") == compartment_id] + + fleet_id = request.args.get("id") + if fleet_id: + items = [item for item in items if item.get("id") == fleet_id] + + lifecycle_state = request.args.get("lifecycleState") + if lifecycle_state: + items = [item for item in items if item.get("lifecycleState") == lifecycle_state] + + display_name = request.args.get("displayName") + if display_name: + items = [item for item in items if item.get("displayName") == display_name] + + display_name_contains = request.args.get("displayNameContains") + if display_name_contains: + needle = display_name_contains.lower() + items = [item for item in items if needle in item.get("displayName", "").lower()] + + return oci_res({"items": _apply_limit(items)}) + + +@jms_bp.route("/fleets/", methods=["GET"]) +def get_fleet(fleet_id): + fleet = _find_fleet(fleet_id) + if not fleet: + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + return oci_res(fleet) + + +@jms_bp.route("/jmsPlugins", methods=["GET"]) +def list_jms_plugins(): + items = JMS_PLUGINS + + compartment_id = request.args.get("compartmentId") + if compartment_id: + items = [item for item in items if item.get("compartmentId") == compartment_id] + + fleet_id = request.args.get("fleetId") + if fleet_id: + items = [item for item in items if item.get("fleetId") == fleet_id] + + plugin_id = request.args.get("id") + if plugin_id: + items = [item for item in items if item.get("id") == plugin_id] + + lifecycle_state = request.args.get("lifecycleState") + if lifecycle_state: + items = [item for item in items if item.get("lifecycleState") == lifecycle_state] + + hostname_contains = request.args.get("hostnameContains") + if hostname_contains: + needle = hostname_contains.lower() + items = [item for item in items if needle in item.get("hostname", "").lower()] + + return oci_res({"items": _apply_limit(items)}) + + +@jms_bp.route("/jmsPlugins/", methods=["GET"]) +def get_jms_plugin(jms_plugin_id): + plugin = _find_plugin(jms_plugin_id) + if not plugin: + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + return oci_res(plugin) + + +@jms_bp.route("/fleets//installationSites", methods=["GET"]) +def list_installation_sites(fleet_id): + if not _find_fleet(fleet_id): + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + + items = INSTALLATION_SITES.get(fleet_id, []) + + managed_instance_id = request.args.get("managedInstanceId") + if managed_instance_id: + items = [item for item in items if item.get("managedInstanceId") == managed_instance_id] + + path_contains = request.args.get("pathContains") + if path_contains: + needle = path_contains.lower() + items = [item for item in items if needle in item.get("path", "").lower()] + + jre_version = request.args.get("jreVersion") + if jre_version: + items = [item for item in items if item.get("jre", {}).get("version") == jre_version] + + jre_vendor = request.args.get("jreVendor") + if jre_vendor: + items = [item for item in items if item.get("jre", {}).get("vendor") == jre_vendor] + + jre_distribution = request.args.get("jreDistribution") + if jre_distribution: + items = [ + item for item in items if item.get("jre", {}).get("distribution") == jre_distribution + ] + + jre_security_status = request.args.get("jreSecurityStatus") + if jre_security_status: + items = [item for item in items if item.get("securityStatus") == jre_security_status] + + return oci_res({"items": _apply_limit(items)}) + + +@jms_bp.route("/fleets//agentConfiguration", methods=["GET"]) +def get_fleet_agent_configuration(fleet_id): + if not _find_fleet(fleet_id): + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + config = FLEET_AGENT_CONFIGURATIONS.get(fleet_id) + if not config: + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + return oci_res(config) + + +@jms_bp.route("/fleets//advancedFeatureConfiguration", methods=["GET"]) +def get_fleet_advanced_feature_configuration(fleet_id): + if not _find_fleet(fleet_id): + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + config = FLEET_ADVANCED_FEATURE_CONFIGURATIONS.get(fleet_id) + if not config: + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + return oci_res(config) + + +@jms_bp.route("/summarizeResourceInventory", methods=["GET"]) +def summarize_resource_inventory(): + compartment_id = request.args.get("compartmentId") + if compartment_id and compartment_id != "ocid1.tenancy.oc1..mock": + return oci_res( + { + "activeFleetCount": 0, + "managedInstanceCount": 0, + "jreCount": 0, + "installationCount": 0, + "applicationCount": 0, + } + ) + return oci_res(RESOURCE_INVENTORY) + + +@jms_bp.route("/fleets//actions/summarizeManagedInstanceUsage", methods=["GET"]) +def summarize_managed_instance_usage(fleet_id): + if not _find_fleet(fleet_id): + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + + items = MANAGED_INSTANCE_USAGE.get(fleet_id, []) + + managed_instance_id = request.args.get("managedInstanceId") + if managed_instance_id: + items = [item for item in items if item.get("managedInstanceId") == managed_instance_id] + + hostname_contains = request.args.get("hostnameContains") + if hostname_contains: + needle = hostname_contains.lower() + items = [item for item in items if needle in item.get("hostname", "").lower()] + + return oci_res({"items": _apply_limit(items)}) + + +@jms_bp.route("/fleets//diagnoses", methods=["GET"]) +def list_fleet_diagnoses(fleet_id): + if not _find_fleet(fleet_id): + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + items = FLEET_DIAGNOSES.get(fleet_id, []) + return oci_res({"items": _apply_limit(items)}) + + +@jms_bp.route("/fleetErrors", methods=["GET"]) +def list_fleet_errors(): + items = FLEET_ERRORS + + compartment_id = request.args.get("compartmentId") + if compartment_id: + items = [item for item in items if item.get("compartmentId") == compartment_id] + + fleet_id = request.args.get("fleetId") + if fleet_id: + items = [item for item in items if item.get("fleetId") == fleet_id] + + return oci_res({"items": _apply_limit(items)}) + + +@jms_bp.route("/announcements", methods=["GET"]) +def list_announcements(): + items = JMS_NOTICES + + summary_contains = request.args.get("summaryContains") + if summary_contains: + needle = summary_contains.lower() + items = [item for item in items if needle in item.get("summary", "").lower()] + + sort_by = request.args.get("sortBy") + if sort_by == "summary": + items = _sort_items(items, "summary", request.args.get("sortOrder") == "DESC") + else: + items = _sort_items(items, "timeReleased", request.args.get("sortOrder") == "DESC") + + return oci_res({"items": _apply_limit(items)}) + + +@jms_bp.route("/fleets//actions/summarizeJreUsage", methods=["GET"]) +def summarize_jre_usage(fleet_id): + if not _find_fleet(fleet_id): + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + + items = JRE_USAGE.get(fleet_id, []) + + jre_version = request.args.get("jreVersion") + if jre_version: + items = [item for item in items if item.get("version") == jre_version] + + jre_vendor = request.args.get("jreVendor") + if jre_vendor: + items = [item for item in items if item.get("vendor") == jre_vendor] + + jre_distribution = request.args.get("jreDistribution") + if jre_distribution: + items = [item for item in items if item.get("distribution") == jre_distribution] + + jre_security_status = request.args.get("jreSecurityStatus") + if jre_security_status: + items = [item for item in items if item.get("securityStatus") == jre_security_status] + + return oci_res({"items": _apply_limit(items)}) + + +@jms_bp.route("/javaReleases/", methods=["GET"]) +def get_java_release(release_version): + release = _find_java_release(release_version) + if not release: + return jsonify({"code": "NotAuthorizedOrNotFound"}), 404 + return oci_res(release) diff --git a/tests/e2e/features/oci-jms-mcp-server.feature b/tests/e2e/features/oci-jms-mcp-server.feature new file mode 100644 index 00000000..db22fd0b --- /dev/null +++ b/tests/e2e/features/oci-jms-mcp-server.feature @@ -0,0 +1,64 @@ +# Copyright (c) 2026, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v1.0 as shown at +# https://oss.oracle.com/licenses/upl. + +Feature: OCI JMS MCP Server + Scenario: List the JMS tools available in the agent + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "What JMS mcp tools do you have" + Then the response should contain a list of JMS tools available + + Scenario: List JMS fleets + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "list my JMS fleets" + Then the response should contain a list of JMS fleets + + Scenario: Get fleet details and fleet configuration + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "list my JMS fleets, then get the details of the first JMS fleet in the list, then get the fleet agent configuration and advanced feature configuration for that fleet" + Then the response should contain fleet configuration details + + Scenario: List JMS plugins and get the first plugin + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "list my JMS fleets, then list the JMS plugins in the first fleet with hostname containing plugin, then get the details of the first JMS plugin" + Then the response should contain JMS plugin details + + Scenario: List installation sites and summarize managed instance usage + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "list my JMS fleets, then list the installation sites in the first JMS fleet with path containing /usr/lib/jvm, then summarize managed instance usage for that same fleet with hostname containing usage" + Then the response should contain installation site and managed instance usage details + + Scenario: Summarize JMS resource inventory + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "summarize my JMS resource inventory" + Then the response should contain JMS resource inventory details + + Scenario: Summarize fleet health + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "list my JMS fleets, then summarize fleet health for the first JMS fleet in the list" + Then the response should contain JMS fleet health summary details + + Scenario: Get fleet health diagnostics + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "list my JMS fleets, then get the detailed fleet health diagnostics for the first JMS fleet in the list" + Then the response should contain JMS fleet health diagnostics details + + Scenario: List JMS notices with filter + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "list my JMS notices whose summary contains maintenance and sort by time released descending" + Then the response should contain JMS notice details + + Scenario: Summarize Java runtime compliance + Given the MCP server is running with OCI tools + And the ollama model with the tools is properly working + When I send a request with the prompt "list my JMS fleets, then summarize Java runtime compliance for the first JMS fleet in the list" + Then the response should contain JMS runtime compliance details diff --git a/tests/e2e/features/steps/oci-jms-mcp-server-steps.py b/tests/e2e/features/steps/oci-jms-mcp-server-steps.py new file mode 100644 index 00000000..41cc3ec4 --- /dev/null +++ b/tests/e2e/features/steps/oci-jms-mcp-server-steps.py @@ -0,0 +1,178 @@ +""" +Copyright (c) 2026, Oracle and/or its affiliates. +Licensed under the Universal Permissive License v1.0 as shown at +https://oss.oracle.com/licenses/upl. +""" + +from behave import then + + +@then("the response should contain a list of JMS tools available") +def step_impl_jms_tools_available(context): + response_json = context.response.json() + assert "content" in response_json["message"], "Response does not contain a content key." + content = response_json["message"]["content"].lower() + expected_tools = [ + "list_fleets", + "get_fleet", + "list_jms_plugins", + "get_jms_plugin", + "list_installation_sites", + "get_fleet_agent_configuration", + "get_fleet_advanced_feature_configuration", + "summarize_resource_inventory", + "summarize_managed_instance_usage", + "summarize_fleet_health", + "get_fleet_health_diagnostics", + "list_jms_notices", + "java_runtime_compliance", + ] + missing_tools = [tool for tool in expected_tools if tool not in content] + assert not missing_tools, f"JMS tools could not be queried: missing {missing_tools}" + + +@then("the response should contain a list of JMS fleets") +def step_impl_list_jms_fleets(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert ( + "ocid1.jmsfleet" in content or "mock-jms-fleet" in content + ), "List of JMS fleets not found." + + +@then("the response should contain fleet configuration details") +def step_impl_fleet_configuration(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert any( + value in content + for value in [ + "mock-jms-fleet", + "analyticbucketname", + "jms_analytics_bucket", + "/u01/java", + "jrescanfrequencyinminutes", + ] + ), "Fleet configuration details not found." + + +@then("the response should contain JMS plugin details") +def step_impl_jms_plugin_details(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert any( + value in content + for value in [ + "ocid1.jmsplugin", + "plugin-host-1", + "hostname", + "oraclemanagementagent", + "pluginversion", + ] + ), "JMS plugin details not found." + + +@then("the response should contain installation site and managed instance usage details") +def step_impl_installation_site_and_usage(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert any( + value in content + for value in [ + "installation-alpha", + "/usr/lib/jvm/java-17-oracle", + "managed-instance-1", + "hostname", + "approximatejrecount", + "usage-host-1", + ] + ), "Installation site or managed instance usage details not found." + + +@then("the response should contain JMS resource inventory details") +def step_impl_resource_inventory(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert any( + value in content + for value in [ + "activefleetcount", + "managedinstancecount", + "installationcount", + "applicationcount", + "42", + ] + ), "JMS resource inventory details not found." + + +@then("the response should contain JMS fleet health summary details") +def step_impl_fleet_health_summary(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert any( + value in content + for value in [ + "critical", + "warning", + "agent connectivity failure", + "inventory scan issue", + "recommended_next_checks", + ] + ), "JMS fleet health summary details not found." + + +@then("the response should contain JMS fleet health diagnostics details") +def step_impl_fleet_health_diagnostics(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert any( + value in content + for value in [ + "diagnosis_count", + "fleet_error_count", + "plugin heartbeat warning", + "critical agent reporting failure", + "jms_fleet", + ] + ), "JMS fleet health diagnostics details not found." + + +@then("the response should contain JMS notice details") +def step_impl_jms_notice_details(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert any( + value in content + for value in [ + "planned jms maintenance window", + "maintenance", + "1001", + "time_released", + "example.oracle.test/jms/maintenance", + ] + ), "JMS notice details not found." + + +@then("the response should contain JMS runtime compliance details") +def step_impl_jms_runtime_compliance(context): + result = context.response.json() + assert "content" in result["message"], "Response does not contain a content key." + content = result["message"]["content"].lower() + assert any( + value in content + for value in [ + "total_runtimes_in_fleet", + "17.0.10", + "update_required", + "nftc", + "/opt/java/jdk-17.0.10", + ] + ), "JMS runtime compliance details not found."