diff --git a/azure/1-infra.sh b/azure/1-infra.sh index 3bfdb4aa..572a5df9 100755 --- a/azure/1-infra.sh +++ b/azure/1-infra.sh @@ -34,6 +34,7 @@ source "$CONFIG_FILE" REQUIRED_VARS=( AZURE_SUBSCRIPTION_ID AZURE_LOCATION RESOURCE_GROUP PROJECT_NAME + POSTGRES_ADMIN_PASSWORD ) for var in "${REQUIRED_VARS[@]}"; do if [[ -z "${!var:-}" ]]; then @@ -51,6 +52,8 @@ echo " Subscripció : $AZURE_SUBSCRIPTION_ID" echo " Localització: $AZURE_LOCATION" echo " Grup recurs : $RESOURCE_GROUP" echo " ACR : $ACR_NAME" +echo " PostgreSQL : ${PROJECT_NAME}-pg" +echo " Storage : ${PROJECT_NAME}store" echo "════════════════════════════════════════════════════════" echo "" @@ -60,9 +63,11 @@ az account set --subscription "$AZURE_SUBSCRIPTION_ID" # ── Registrar proveïdors necessaris ────────────────────────────────────────── echo "→ Registrant proveïdors Azure (pot trigar uns minuts la primera vegada)..." -az provider register --namespace Microsoft.App --wait -az provider register --namespace Microsoft.OperationalInsights --wait -az provider register --namespace Microsoft.ContainerRegistry --wait +az provider register --namespace Microsoft.App --wait +az provider register --namespace Microsoft.OperationalInsights --wait +az provider register --namespace Microsoft.ContainerRegistry --wait +az provider register --namespace Microsoft.Storage --wait +az provider register --namespace Microsoft.DBforPostgreSQL --wait # ── Crear Resource Group ────────────────────────────────────────────────────── echo "→ Creant Resource Group '$RESOURCE_GROUP'..." @@ -73,26 +78,41 @@ az group create \ echo " ✓ Resource Group llest" # ── Desplegar infraestructura via Bicep ────────────────────────────────────── -echo "→ Desplegant infraestructura (ACR + Log Analytics + Container Apps Env)..." +echo "→ Desplegant infraestructura (ACR + Container Apps Env + Storage + PostgreSQL)..." INFRA_OUTPUT=$(az deployment group create \ --resource-group "$RESOURCE_GROUP" \ --template-file "${SCRIPT_DIR}/infra.bicep" \ --parameters \ projectName="$PROJECT_NAME" \ location="$AZURE_LOCATION" \ + postgresAdminPassword="$POSTGRES_ADMIN_PASSWORD" \ + postgresAdminUser="${POSTGRES_ADMIN_USER:-mirofish}" \ + postgresSku="${POSTGRES_SKU:-B_Standard_B1ms}" \ --output json) # Extreure outputs del desplegament -ACR_LOGIN_SERVER=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['acrLoginServer']['value'])") -ACR_NAME_OUT=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['acrName']['value'])") -ENV_ID=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['containerAppsEnvId']['value'])") +ACR_LOGIN_SERVER=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['acrLoginServer']['value'])") +ACR_NAME_OUT=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['acrName']['value'])") +ENV_ID=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['containerAppsEnvId']['value'])") +STORAGE_ACCOUNT_NAME=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['storageAccountName']['value'])") +FILE_SHARE_NAME=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['fileShareName']['value'])") +POSTGRES_HOST=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['postgresHost']['value'])") +STORAGE_CONNECTION_STRING=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['storageConnectionString']['value'])") +DATABASE_URL=$(echo "$INFRA_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['databaseUrl']['value'])") echo "" echo "════════════════════════════════════════════════════════" echo " Infraestructura creada correctament!" echo "════════════════════════════════════════════════════════" -echo " ACR Login Server : $ACR_LOGIN_SERVER" -echo " Container Apps Env ID: $ENV_ID" +echo " ACR Login Server : $ACR_LOGIN_SERVER" +echo " Container Apps Env ID : $ENV_ID" +echo " Storage Account : $STORAGE_ACCOUNT_NAME" +echo " File Share : $FILE_SHARE_NAME" +echo " PostgreSQL host : $POSTGRES_HOST" +echo "" +echo " Afegeix a config.sh (valors generats per Azure):" +echo " STORAGE_CONNECTION_STRING='$STORAGE_CONNECTION_STRING'" +echo " DATABASE_URL='$DATABASE_URL'" echo "" echo " Proper pas: bash azure/2-build-deploy.sh" echo "════════════════════════════════════════════════════════" diff --git a/azure/2-build-deploy.sh b/azure/2-build-deploy.sh index 2d339fe0..5c069d17 100755 --- a/azure/2-build-deploy.sh +++ b/azure/2-build-deploy.sh @@ -30,6 +30,7 @@ source "$CONFIG_FILE" REQUIRED_VARS=( AZURE_SUBSCRIPTION_ID RESOURCE_GROUP PROJECT_NAME DEMO_PASSWORD SECRET_KEY LLM_API_KEY LLM_BASE_URL LLM_MODEL_NAME + DATABASE_URL STORAGE_CONNECTION_STRING ) # Validate graph backend config GRAPH_BACKEND="${GRAPH_BACKEND:-zep}" @@ -157,6 +158,10 @@ DEPLOY_OUTPUT=$(az deployment group create \ reportAgentMaxToolCalls="${REPORT_AGENT_MAX_TOOL_CALLS:-5}" \ reportAgentMaxReflectionRounds="${REPORT_AGENT_MAX_REFLECTION_ROUNDS:-2}" \ reportAgentTemperature="${REPORT_AGENT_TEMPERATURE:-0.5}" \ + storageConnectionString="${STORAGE_CONNECTION_STRING:-}" \ + storageAccountName="${STORAGE_ACCOUNT_NAME:-}" \ + fileShareName="${FILE_SHARE_NAME:-mirofish-uploads}" \ + databaseUrl="${DATABASE_URL:-}" \ --output json) FQDN=$(echo "$DEPLOY_OUTPUT" | python3 -c "import sys,json; print(json.load(sys.stdin)['properties']['outputs']['containerAppFqdn']['value'])") diff --git a/azure/config.sh.example b/azure/config.sh.example index eb1bbd03..7361ae44 100644 --- a/azure/config.sh.example +++ b/azure/config.sh.example @@ -70,6 +70,32 @@ LLM_SMALL_API_KEY="" LLM_SMALL_BASE_URL="" LLM_SMALL_MODEL_NAME="" +# ── PostgreSQL Flexible Server ──────────────────────────────────────────────── +# Contrasenya de l'administrador de la BD PostgreSQL +# Genera-la amb: python -c "import secrets; print(secrets.token_urlsafe(24))" +POSTGRES_ADMIN_PASSWORD="" + +# Usuari administrador de PostgreSQL (per defecte: mirofish) +POSTGRES_ADMIN_USER="mirofish" + +# SKU de PostgreSQL: B_Standard_B1ms (dev/test) | GP_Standard_D2s_v3 (producció) +POSTGRES_SKU="B_Standard_B1ms" + +# ── Storage (generats per 1-infra.sh — afegir després d'executar-lo) ───────── +# Connection string d'Azure Files (output de 1-infra.sh) +# Format: DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net +STORAGE_CONNECTION_STRING="" + +# Nom del Storage Account (output de 1-infra.sh) +STORAGE_ACCOUNT_NAME="" + +# Nom del File Share (per defecte: mirofish-uploads) +FILE_SHARE_NAME="mirofish-uploads" + +# DATABASE_URL PostgreSQL (output de 1-infra.sh) +# Format: postgresql+psycopg2://mirofish:@/mirofish?sslmode=require +DATABASE_URL="" + # ── Simulació OASIS (valors per defecte recomanats) ─────────────────────────── OASIS_DEFAULT_MAX_ROUNDS="10" diff --git a/azure/container-app.bicep b/azure/container-app.bicep index e2ddc321..a9d4ae3a 100644 --- a/azure/container-app.bicep +++ b/azure/container-app.bicep @@ -61,6 +61,20 @@ param neo4jPassword string = '' @secure() param secretKey string +@description('Connection string del Storage Account per a Azure Files (output d\'infra.bicep)') +@secure() +param storageConnectionString string = '' + +@description('DATABASE_URL PostgreSQL (output d\'infra.bicep: postgresql+psycopg2://...)') +@secure() +param databaseUrl string = '' + +@description('Nom del Storage Account (output d\'infra.bicep)') +param storageAccountName string = '' + +@description('Nom del File Share d\'Azure Files (output d\'infra.bicep)') +param fileShareName string = 'mirofish-uploads' + // ─── Paràmetres LLM principal ───────────────────────────────────────────────── @description('URL base de l\'API LLM principal') @@ -144,11 +158,13 @@ var mandatorySecrets = [ { name: 'secret-key', value: secretKey } ] var optionalSecrets = concat( - empty(llmBoostApiKey) ? [] : [{ name: 'llm-boost-api-key', value: llmBoostApiKey }], - empty(llmEmbedApiKey) ? [] : [{ name: 'llm-embed-api-key', value: llmEmbedApiKey }], - empty(llmSmallApiKey) ? [] : [{ name: 'llm-small-api-key', value: llmSmallApiKey }], - empty(zepApiKey) ? [] : [{ name: 'zep-api-key', value: zepApiKey }], - empty(neo4jPassword) ? [] : [{ name: 'neo4j-password', value: neo4jPassword }] + empty(llmBoostApiKey) ? [] : [{ name: 'llm-boost-api-key', value: llmBoostApiKey }], + empty(llmEmbedApiKey) ? [] : [{ name: 'llm-embed-api-key', value: llmEmbedApiKey }], + empty(llmSmallApiKey) ? [] : [{ name: 'llm-small-api-key', value: llmSmallApiKey }], + empty(zepApiKey) ? [] : [{ name: 'zep-api-key', value: zepApiKey }], + empty(neo4jPassword) ? [] : [{ name: 'neo4j-password', value: neo4jPassword }], + empty(storageConnectionString) ? [] : [{ name: 'storage-connection-string', value: storageConnectionString }], + empty(databaseUrl) ? [] : [{ name: 'database-url', value: databaseUrl }] ) var allSecrets = concat(mandatorySecrets, optionalSecrets) @@ -174,13 +190,19 @@ var mandatoryEnv = [ { name: 'REPORT_AGENT_MAX_REFLECTION_ROUNDS', value: reportAgentMaxReflectionRounds } { name: 'REPORT_AGENT_TEMPERATURE', value: reportAgentTemperature } { name: 'FLASK_DEBUG', value: 'False' } + // Storage: si s'usa Azure Files, les dades OASIS es guarden al volum muntat + { name: 'OASIS_SIMULATION_DATA_DIR', value: empty(storageAccountName) ? '/app/backend/uploads/simulations' : '/mnt/uploads/simulations' } + { name: 'UPLOAD_FOLDER', value: empty(storageAccountName) ? '/app/backend/uploads' : '/mnt/uploads' } + { name: 'STORAGE_TYPE', value: empty(storageConnectionString) ? 'local' : 'azure' } ] var optionalEnv = concat( - empty(llmBoostApiKey) ? [] : [{ name: 'LLM_BOOST_API_KEY', secretRef: 'llm-boost-api-key' }], - empty(llmEmbedApiKey) ? [] : [{ name: 'LLM_EMBED_API_KEY', secretRef: 'llm-embed-api-key' }], - empty(llmSmallApiKey) ? [] : [{ name: 'LLM_SMALL_API_KEY', secretRef: 'llm-small-api-key' }], - empty(zepApiKey) ? [] : [{ name: 'ZEP_API_KEY', secretRef: 'zep-api-key' }], - empty(neo4jPassword) ? [] : [{ name: 'NEO4J_PASSWORD', secretRef: 'neo4j-password' }] + empty(llmBoostApiKey) ? [] : [{ name: 'LLM_BOOST_API_KEY', secretRef: 'llm-boost-api-key' }], + empty(llmEmbedApiKey) ? [] : [{ name: 'LLM_EMBED_API_KEY', secretRef: 'llm-embed-api-key' }], + empty(llmSmallApiKey) ? [] : [{ name: 'LLM_SMALL_API_KEY', secretRef: 'llm-small-api-key' }], + empty(zepApiKey) ? [] : [{ name: 'ZEP_API_KEY', secretRef: 'zep-api-key' }], + empty(neo4jPassword) ? [] : [{ name: 'NEO4J_PASSWORD', secretRef: 'neo4j-password' }], + empty(storageConnectionString) ? [] : [{ name: 'AZURE_STORAGE_CONNECTION_STRING', secretRef: 'storage-connection-string' }], + empty(databaseUrl) ? [] : [{ name: 'DATABASE_URL', secretRef: 'database-url' }] ) var allEnv = concat(mandatoryEnv, optionalEnv) @@ -230,6 +252,23 @@ resource containerApp 'Microsoft.App/containerApps@2023-05-01' = { cpu: json('0.5') memory: '1Gi' } + + // Muntar Azure Files quan s'ha configurat storage + volumeMounts: empty(storageAccountName) ? [] : [ + { + volumeName: 'uploads' + mountPath: '/mnt/uploads' + } + ] + } + ] + + // Volum Azure Files (registrat a l'entorn via infra.bicep) + volumes: empty(storageAccountName) ? [] : [ + { + name: 'uploads' + storageType: 'AzureFile' + storageName: 'uploads' } ] diff --git a/azure/infra.bicep b/azure/infra.bicep index ff8941e4..912f55d5 100644 --- a/azure/infra.bicep +++ b/azure/infra.bicep @@ -2,8 +2,10 @@ // MiroFish — Infraestructura base (executar una sola vegada) // // Crea: -// - Azure Container Registry (ACR) per emmagatzemar la imatge Docker -// - Container Apps Environment (plataforma d'execució) +// - Azure Container Registry (ACR) +// - Container Apps Environment +// - Storage Account + File Share (muntat al container per a dades OASIS) +// - Azure Database for PostgreSQL Flexible Server // // Executar amb: azure/1-infra.sh // ───────────────────────────────────────────────────────────────────────────── @@ -14,34 +16,123 @@ param projectName string = 'mirofish' @description('Localització Azure dels recursos') param location string = resourceGroup().location +@description('Contrasenya de l\'administrador de PostgreSQL') +@secure() +param postgresAdminPassword string + +@description('Nom de l\'usuari administrador de PostgreSQL') +param postgresAdminUser string = 'mirofish' + +@description('SKU de PostgreSQL (B_Standard_B1ms per dev; GP_Standard_D2s_v3 per pro)') +param postgresSku string = 'B_Standard_B1ms' + // ─── Azure Container Registry ───────────────────────────────────────────────── -// SKU Basic: suficient per a imatges privades sense geo-replicació resource acr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { - name: '${projectName}acr' // ACR no admet guions, tot minúscula + name: '${projectName}acr' location: location - sku: { - name: 'Basic' - } - properties: { - adminUserEnabled: true // necessari per a l'autenticació des dels scripts - } + sku: { name: 'Basic' } + properties: { adminUserEnabled: true } } // ─── Container Apps Environment ─────────────────────────────────────────────── +// Necessita el Storage Account abans per poder registrar el file share com a volum resource containerAppsEnv 'Microsoft.App/managedEnvironments@2023-05-01' = { name: '${projectName}-env' location: location - properties: {} - // TODO (ops): afegir appLogsConfiguration amb Log Analytics si es vol observabilitat - // TODO (ops): descomentar per integrar en VNet Hub-Spoke - // vnetConfiguration: { - // infrastructureSubnetId: '/subscriptions/.../subnets/container-apps-subnet' - // internal: true - // } + properties: { + appLogsConfiguration: { + destination: 'azure-monitor' + } + } + dependsOn: [storageAccount] +} + +// Registra el File Share dins l'entorn de Container Apps +resource envStorage 'Microsoft.App/managedEnvironments/storages@2023-05-01' = { + name: 'uploads' + parent: containerAppsEnv + properties: { + azureFile: { + accountName: storageAccount.name + accountKey: storageAccount.listKeys().keys[0].value + shareName: fileShare.name + accessMode: 'ReadWrite' + } + } +} + +// ─── Storage Account + File Share (dades OASIS persistents) ────────────────── +// Azure Files és necessari per a: +// - uploads/simulations/ (SQLite DBs, JSONL, IPC files de les simulacions OASIS) +// - uploads/projects/ (fitxers pujats per l'usuari) +// Standard LRS: suficient per a escenaris non-HA +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { + name: '${replace(projectName, '-', '')}store' // sense guions, màx 24 chars + location: location + sku: { name: 'Standard_LRS' } + kind: 'StorageV2' + properties: { + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + supportsHttpsTrafficOnly: true + } +} + +resource fileService 'Microsoft.Storage/storageAccounts/fileServices@2023-01-01' = { + name: 'default' + parent: storageAccount +} + +resource fileShare 'Microsoft.Storage/storageAccounts/fileServices/shares@2023-01-01' = { + name: 'mirofish-uploads' + parent: fileService + properties: { + shareQuota: 100 // GB; augmenta si les simulacions creixen + enabledProtocols: 'SMB' + } +} + +// ─── Azure Database for PostgreSQL Flexible Server ──────────────────────────── +// Flexible Server és el recomanat per a desplegaments nous (Single Server deprecated) +// La base de dades 'mirofish' es crea automàticament +resource postgresServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-06-01-preview' = { + name: '${projectName}-pg' + location: location + sku: { + name: postgresSku + tier: startsWith(postgresSku, 'B_') ? 'Burstable' : 'GeneralPurpose' + } + properties: { + administratorLogin: postgresAdminUser + administratorLoginPassword: postgresAdminPassword + version: '16' + storage: { storageSizeGB: 32 } + backup: { backupRetentionDays: 7, geoRedundantBackup: 'Disabled' } + highAvailability: { mode: 'Disabled' } + // Accés públic desactivat; usa firewall rule per a Container Apps o VNet + network: { publicNetworkAccess: 'Enabled' } + authConfig: { activeDirectoryAuth: 'Disabled', passwordAuth: 'Enabled' } + } +} + +resource postgresDb 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-06-01-preview' = { + name: 'mirofish' + parent: postgresServer + properties: { charset: 'UTF8', collation: 'en_US.utf8' } +} + +// Regla de firewall per permetre tràfic de serveis Azure (inclou Container Apps) +resource postgresFirewallAzure 'Microsoft.DBforPostgreSQL/flexibleServers/firewallRules@2023-06-01-preview' = { + name: 'allow-azure-services' + parent: postgresServer + properties: { + startIpAddress: '0.0.0.0' + endIpAddress: '0.0.0.0' + } } // ─── Outputs (usats pels scripts de deploy) ─────────────────────────────────── -@description('URL de login de l\'ACR (ex: mirofsihacr.azurecr.io)') +@description('URL de login de l\'ACR') output acrLoginServer string = acr.properties.loginServer @description('Nom del recurs ACR') @@ -49,3 +140,27 @@ output acrName string = acr.name @description('ID del Container Apps Environment') output containerAppsEnvId string = containerAppsEnv.id + +@description('Nom del Storage Account') +output storageAccountName string = storageAccount.name + +@description('Clau primària del Storage Account (per a AZURE_STORAGE_CONNECTION_STRING)') +@sensitive() +output storageAccountKey string = storageAccount.listKeys().keys[0].value + +@description('Connection string del Storage Account (per a AZURE_STORAGE_CONNECTION_STRING)') +@sensitive() +output storageConnectionString string = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value};EndpointSuffix=core.windows.net' + +@description('Nom del File Share d\'Azure Files') +output fileShareName string = fileShare.name + +@description('FQDN del servidor PostgreSQL') +output postgresHost string = postgresServer.properties.fullyQualifiedDomainName + +@description('Usuari administrador de PostgreSQL') +output postgresAdminUser string = postgresAdminUser + +@description('DATABASE_URL per a la Container App (postgresql+psycopg2://...)') +@sensitive() +output databaseUrl string = 'postgresql+psycopg2://${postgresAdminUser}:${postgresAdminPassword}@${postgresServer.properties.fullyQualifiedDomainName}/mirofish?sslmode=require' diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 6dce211c..48e4e6c0 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -44,6 +44,9 @@ dependencies = [ # Azure 存储 "azure-storage-blob>=12.19.0", + # PostgreSQL driver (necessari quan DATABASE_URL és postgres://) + "psycopg2-binary>=2.9.9", + # 安全 & 认证 "bcrypt>=4.1.0", "flask-jwt-extended>=4.6.0", diff --git a/backend/uv.lock b/backend/uv.lock index f8fa46c9..a0a8046d 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -1505,6 +1505,7 @@ dependencies = [ { name = "gunicorn" }, { name = "markdown" }, { name = "openai" }, + { name = "psycopg2-binary" }, { name = "pydantic" }, { name = "pyjwt" }, { name = "pymupdf" }, @@ -1549,6 +1550,7 @@ requires-dist = [ { name = "neo4j", marker = "extra == 'graphiti'", specifier = ">=5.26.0" }, { name = "openai", specifier = ">=1.0.0" }, { name = "pipreqs", marker = "extra == 'dev'", specifier = ">=0.5.0" }, + { name = "psycopg2-binary", specifier = ">=2.9.9" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pyjwt", specifier = ">=2.8.0" }, { name = "pymupdf", specifier = ">=1.24.0" }, @@ -2187,6 +2189,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/33/2d74d588408caedd065c2497bdb5ef83ce6082db01289a1e1147f6639802/psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8", size = 249898, upload-time = "2024-01-19T20:47:59.238Z" }, ] +[[package]] +name = "psycopg2-binary" +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/19/d4ce60954f3bb9d8e3bc5e5c4d1f2487de2d3851bf2391d54954c9df12a6/psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b", size = 3712338, upload-time = "2026-04-20T23:34:03.961Z" }, + { url = "https://files.pythonhosted.org/packages/53/71/c85409ee0d78890f0660eff262e815e7dd2bb741a17611d82e9e8cd9dc5e/psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326", size = 3822407, upload-time = "2026-04-20T23:34:05.977Z" }, + { url = "https://files.pythonhosted.org/packages/3c/ed/60486c2c7f0d4d1ede2bfb1ed27e2498477ce646bc7f6b2759906303117e/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06", size = 4578425, upload-time = "2026-04-20T23:34:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b9/656cb03fad9f4f49f2145c334b1126ee75189929ca4e6187d485a2d59951/psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8", size = 4273709, upload-time = "2026-04-20T23:34:10.974Z" }, + { url = "https://files.pythonhosted.org/packages/99/66/08cf0da0e25cc6fb142c89be45fc8418792858f0c4cbff5e24530ff02cd6/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3", size = 5893779, upload-time = "2026-04-20T23:34:13.905Z" }, + { url = "https://files.pythonhosted.org/packages/17/d7/eecd9ce8e146d3721115d82d3836efdbb712187e4590325df549989d18f4/psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39", size = 4109308, upload-time = "2026-04-20T23:34:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/b1dc289b362cc8d45697b57eefbd673186f49a4ea0906928988e3affcc98/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c", size = 3654405, upload-time = "2026-04-20T23:34:19.303Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/4c4aea6473214dbdbd0fbba11aa4691e76dc01722c55724c5951719865ff/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a", size = 3299187, upload-time = "2026-04-20T23:34:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/ba/5d/b03b99986446a4f57b170ed9a2579fb7ff9783ca0fa5226b19db99737fee/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c", size = 3047716, upload-time = "2026-04-20T23:34:23.077Z" }, + { url = "https://files.pythonhosted.org/packages/14/86/382ee4afbd1d97500c9d2862b20c2fdeddf4b7335e984df3fb4309f64108/psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be", size = 3349237, upload-time = "2026-04-20T23:34:25.211Z" }, + { url = "https://files.pythonhosted.org/packages/a8/16/9a57c75ba1eda7165c017342f526810d5f5a12647dde749c99ae9a7141d7/psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936", size = 2757036, upload-time = "2026-04-20T23:34:27.77Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, + { url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" }, + { url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" }, + { url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" }, + { url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" }, + { url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" }, + { url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" }, + { url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" }, + { url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" }, + { url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" }, + { url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" }, + { url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" }, + { url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" }, +] + [[package]] name = "ptyprocess" version = "0.7.0"