merge: quick/bigquery-functions — Funciones BigQuery Python, tipo BQClient, comando meta_bigq

This commit is contained in:
2026-04-07 18:45:38 +02:00
37 changed files with 3538 additions and 0 deletions
+457
View File
@@ -0,0 +1,457 @@
# /meta_bigq — Operar Metabase y BigQuery desde el registry
Eres un agente de datos. Tienes acceso a funciones Python del fn_registry para controlar **Metabase** (dashboards, cards, queries, usuarios) y **Google BigQuery** (datasets, tablas, queries, jobs, routines). Usa estas funciones directamente — no inventes llamadas HTTP manuales.
---
## Como ejecutar funciones
```bash
PYTHON="python/.venv/bin/python3"
# Ejecutar codigo inline
$PYTHON -c "
import sys; sys.path.insert(0, 'python/functions')
from metabase import metabase_auth, metabase_list_dashboards
client = metabase_auth('http://localhost:3000', 'admin@fnregistry.local', 'FnRegistry2024!')
print(metabase_list_dashboards(client))
"
# O con fn run para pipelines
./fn run init_metabase --project fn_registry
./fn run setup_metabase_volume
./fn run metabase_create_ops_dashboard docker_tui
```
Variables de entorno tipicas:
- `METABASE_URL` (default: `http://localhost:3000`)
- `METABASE_ADMIN_EMAIL` (default: `admin@fnregistry.local`)
- `METABASE_ADMIN_PASSWORD` (default: `FnRegistry2024!`)
- BigQuery usa ADC (`gcloud auth application-default login`) o `GOOGLE_APPLICATION_CREDENTIALS`
---
## METABASE — Referencia rapida
### Auth
```python
from metabase import metabase_auth, MetabaseClient
# Login con email/password
client = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
# O directo con API key
client = MetabaseClient("http://localhost:3000", "mb_api_key_xxxxx")
# Context manager
with metabase_auth(...) as client:
pass # se cierra solo
```
### Cards (preguntas)
```python
from metabase import (
metabase_list_cards, # (client, filter="", model_id=0) -> list[dict]
metabase_get_card, # (client, card_id) -> dict
metabase_create_card, # (client, name, dataset_query, display="table", collection_id=0, description="") -> dict
metabase_update_card, # (client, card_id, **fields) -> dict # fields: name, description, display, dataset_query, archived...
metabase_delete_card, # (client, card_id) -> None # IRREVERSIBLE, preferir archived=True
metabase_execute_card, # (client, card_id, parameters=None) -> dict # ejecuta query de card guardada
metabase_execute_query, # (client, database_id, sql, max_results=0) -> dict # query ad-hoc
)
# Crear card con SQL nativo
card = metabase_create_card(client, "Ventas por mes", {
"database": 1, "type": "native",
"native": {"query": "SELECT date_trunc('month', created_at) as mes, SUM(total) FROM orders GROUP BY 1"},
}, display="line")
# Actualizar query de una card
metabase_update_card(client, card["id"], dataset_query={
"database": 1, "type": "native",
"native": {"query": "SELECT ... nueva query ..."},
})
# Archivar (soft-delete)
metabase_update_card(client, 42, archived=True)
# Query ad-hoc sin guardar
result = metabase_execute_query(client, 1, "SELECT COUNT(*) FROM users")
# result["data"]["rows"] = [[42]]
```
**Filtros de list_cards:** `all`, `mine`, `fav`, `archived`, `recent`, `popular`, `database`, `table`
### Dashboards
```python
from metabase import (
metabase_list_dashboards, # (client, filter="") -> list[dict]
metabase_get_dashboard, # (client, dashboard_id) -> dict # incluye dashcards
metabase_create_dashboard, # (client, name, description="", collection_id=0) -> dict
metabase_update_dashboard, # (client, dashboard_id, **fields) -> dict
metabase_delete_dashboard, # (client, dashboard_id) -> None # IRREVERSIBLE
)
# Crear dashboard + agregar cards
dash = metabase_create_dashboard(client, "KPIs Operativos", description="Metricas diarias")
# Posicionar cards en el dashboard (dashcards es el estado COMPLETO)
metabase_update_dashboard(client, dash["id"], dashcards=[
{"id": -1, "card_id": card1["id"], "row": 0, "col": 0, "size_x": 6, "size_y": 4},
{"id": -2, "card_id": card2["id"], "row": 0, "col": 6, "size_x": 6, "size_y": 4},
{"id": -3, "card_id": card3["id"], "row": 4, "col": 0, "size_x": 12, "size_y": 6},
])
# id negativo = card nueva, id positivo = card existente, omitida = eliminada
```
**Filtros de list_dashboards:** `all`, `mine`, `archived`
### Databases
```python
from metabase import (
metabase_list_databases, # (client, include_tables=False) -> list
metabase_add_database, # (client, name, engine, details) -> dict
metabase_get_database, # (client, database_id) -> dict
)
# Agregar SQLite
metabase_add_database(client, "Operations DB", "sqlite", {"db": "/data/operations.db"})
# Agregar PostgreSQL
metabase_add_database(client, "DW", "postgres", {
"host": "localhost", "port": 5432, "dbname": "warehouse",
"user": "reader", "password": "secret",
})
```
### Usuarios
```python
from metabase import (
metabase_list_users, # (client, status="", query="", limit=0, offset=0) -> dict
metabase_get_user, # (client, user_id) -> dict
metabase_create_user, # (client, first_name, last_name, email, password="", group_ids=None) -> dict
metabase_update_user, # (client, user_id, **fields) -> dict
metabase_deactivate_user, # (client, user_id) -> None # soft-delete
)
```
### Setup y pipelines
```python
from metabase import metabase_setup
# Setup inicial de instancia nueva (obtiene setup-token automaticamente)
metabase_setup("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
```
```bash
# Pipelines ejecutables con fn run
./fn run init_metabase --project fn_registry # Docker: Postgres + Metabase
./fn run setup_metabase_volume # Copiar registry.db al contenedor
./fn run metabase_add_ops_db docker_tui # Registrar operations.db como database
./fn run metabase_create_ops_dashboard docker_tui # Dashboard operativo completo
./fn run metabase_fix_permissions # Arreglar permisos SQLite en Docker
```
---
## BIGQUERY — Referencia rapida
### Auth
```python
from bigquery import bq_auth, BQClient
# ADC (gcloud auth application-default login)
client = bq_auth()
# Proyecto explicito
client = bq_auth("my-project-id")
# Service account JSON
client = bq_auth(credentials_path="/path/to/sa.json")
# Context manager
with bq_auth("my-project") as client:
pass
```
### Datasets
```python
from bigquery import (
bq_create_dataset, # (client, dataset_id, location="US", description="", labels=None, default_table_expiration_ms=0) -> dict
bq_get_dataset, # (client, dataset_id) -> dict
bq_list_datasets, # (client) -> list[dict]
bq_update_dataset, # (client, dataset_id, description=None, labels=None, default_table_expiration_ms=None) -> dict
bq_delete_dataset, # (client, dataset_id, delete_contents=False) -> None
)
bq_create_dataset(client, "analytics", location="EU", description="Data warehouse")
bq_delete_dataset(client, "temp", delete_contents=True) # borra tablas incluidas
```
### Tables
```python
from bigquery import (
bq_create_table, # (client, dataset_id, table_id, schema, partitioning=None, clustering=None, description="", labels=None) -> dict
bq_get_table, # (client, dataset_id, table_id) -> dict # schema, num_rows, num_bytes, partitioning...
bq_list_tables, # (client, dataset_id) -> list[dict]
bq_update_table, # (client, dataset_id, table_id, schema=None, description=None, labels=None) -> dict
bq_delete_table, # (client, dataset_id, table_id) -> None
bq_preview_rows, # (client, dataset_id, table_id, max_results=10) -> dict # SIN COSTE de query
)
# Crear tabla con particionamiento
bq_create_table(client, "analytics", "events",
schema=[
{"name": "event_id", "type": "STRING", "mode": "REQUIRED"},
{"name": "user_id", "type": "STRING"},
{"name": "event_type", "type": "STRING"},
{"name": "created_at", "type": "TIMESTAMP"},
{"name": "payload", "type": "JSON"},
],
partitioning={"type": "DAY", "field": "created_at"},
clustering=["event_type", "user_id"],
)
# Preview sin coste (usa Storage Read API, no ejecuta query)
preview = bq_preview_rows(client, "analytics", "events", max_results=5)
# {"columns": [...], "rows": [[...], ...], "total_rows": 1234567}
# Schema: solo se pueden AGREGAR columnas, nunca eliminar
bq_update_table(client, "analytics", "events", schema=[
*existing_schema,
{"name": "new_col", "type": "STRING"},
])
```
**Tipos de schema:** `STRING`, `INT64`, `FLOAT64`, `BOOL`, `TIMESTAMP`, `DATE`, `DATETIME`, `BYTES`, `NUMERIC`, `JSON`, `RECORD`/`STRUCT`, `GEOGRAPHY`
**Modos:** `NULLABLE` (default), `REQUIRED`, `REPEATED`
### Queries y datos
```python
from bigquery import (
bq_query, # (client, sql, params=None, dry_run=False) -> dict
bq_insert_rows, # (client, dataset_id, table_id, rows) -> dict
bq_load_from_gcs, # (client, uri, dataset_id, table_id, source_format="CSV", write_disposition="WRITE_APPEND", autodetect=True, skip_leading_rows=0) -> dict
bq_load_from_file, # (client, file_path, dataset_id, table_id, ...) -> dict # mismos params que gcs
bq_export_to_gcs, # (client, dataset_id, table_id, destination_uri, destination_format="CSV", compression="NONE") -> dict
bq_copy_table, # (client, source_dataset, source_table, dest_dataset, dest_table, write_disposition="WRITE_EMPTY") -> dict
)
# Query simple
result = bq_query(client, "SELECT COUNT(*) as total FROM analytics.events")
# {"columns": ["total"], "rows": [[1234567]], "total_rows": 1, "bytes_processed": 0, "cache_hit": True}
# Query parametrizada (usa @nombre en SQL)
result = bq_query(client, "SELECT * FROM analytics.events WHERE event_type = @tipo LIMIT @n", params=[
{"name": "tipo", "type": "STRING", "value": "purchase"},
{"name": "n", "type": "INT64", "value": 100},
])
# Estimar coste ANTES de ejecutar (no procesa datos)
estimate = bq_query(client, "SELECT * FROM analytics.events", dry_run=True)
# {"total_bytes_processed": 5368709120, "total_bytes_billed": 5368709120}
gb = estimate["total_bytes_processed"] / (1024**3)
print(f"Esta query procesara {gb:.2f} GB (~${gb * 6.25:.2f} USD)")
# Streaming insert
bq_insert_rows(client, "analytics", "events", [
{"event_id": "e1", "user_id": "u1", "event_type": "click", "created_at": "2026-04-07T10:00:00Z"},
{"event_id": "e2", "user_id": "u2", "event_type": "purchase", "created_at": "2026-04-07T10:01:00Z"},
])
# {"inserted": 2, "errors": []}
# Cargar CSV desde GCS
bq_load_from_gcs(client, "gs://bucket/data/*.csv", "analytics", "events",
source_format="CSV", write_disposition="WRITE_TRUNCATE", skip_leading_rows=1)
# Cargar archivo local
bq_load_from_file(client, "/tmp/data.parquet", "analytics", "events",
source_format="PARQUET", write_disposition="WRITE_APPEND")
# Exportar a GCS
bq_export_to_gcs(client, "analytics", "events", "gs://bucket/export/events-*.csv",
destination_format="CSV", compression="GZIP")
# Copiar tabla
bq_copy_table(client, "analytics", "events", "analytics_backup", "events_20260407")
```
**write_disposition:** `WRITE_TRUNCATE` (reemplazar), `WRITE_APPEND` (agregar), `WRITE_EMPTY` (solo si vacia)
**source_format:** `CSV`, `NEWLINE_DELIMITED_JSON`, `AVRO`, `PARQUET`, `ORC`
### Jobs
```python
from bigquery import (
bq_list_jobs, # (client, state_filter="", max_results=50, all_users=False) -> list[dict]
bq_get_job, # (client, job_id) -> dict # state, bytes_processed, errors
bq_cancel_job, # (client, job_id) -> dict
)
# Ver jobs corriendo
running = bq_list_jobs(client, state_filter="running")
for j in running:
print(j["job_id"], j["job_type"], j["bytes_processed"])
# Cancelar un job pesado
bq_cancel_job(client, "job_abc123")
```
**state_filter:** `running`, `pending`, `done`
### Routines (UDFs / Procedures)
```python
from bigquery import (
bq_create_routine, # (client, dataset_id, routine_id, body, routine_type="SCALAR_FUNCTION", language="SQL", arguments=None, return_type="", description="") -> dict
bq_list_routines, # (client, dataset_id) -> list[dict]
bq_delete_routine, # (client, dataset_id, routine_id) -> None
)
# UDF SQL
bq_create_routine(client, "analytics", "double_value",
body="x * 2",
arguments=[{"name": "x", "data_type": "INT64"}],
return_type="INT64",
)
# Stored procedure
bq_create_routine(client, "analytics", "refresh_summary",
body="BEGIN INSERT INTO summary SELECT ... FROM events; END;",
routine_type="PROCEDURE",
)
# UDF JavaScript
bq_create_routine(client, "analytics", "parse_ua",
body="return uaParser.parse(ua).browser.name;",
language="JAVASCRIPT",
arguments=[{"name": "ua", "data_type": "STRING"}],
return_type="STRING",
)
```
---
## Flujos tipicos
### 1. Explorar BigQuery y visualizar en Metabase
```python
import sys; sys.path.insert(0, "python/functions")
from bigquery import bq_auth, bq_query
from metabase import metabase_auth, metabase_create_card, metabase_create_dashboard, metabase_update_dashboard
# 1. Explorar datos en BQ
bq = bq_auth("my-project")
result = bq_query(bq, "SELECT event_type, COUNT(*) as cnt FROM analytics.events GROUP BY 1 ORDER BY 2 DESC LIMIT 10")
print(result["columns"], result["rows"])
# 2. Registrar BQ como database en Metabase (si no esta)
# Metabase soporta BigQuery como engine nativo
# 3. Crear cards en Metabase apuntando a BQ
mb = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
card = metabase_create_card(mb, "Eventos por tipo", {
"database": 2, # ID de la database BQ en Metabase
"type": "native",
"native": {"query": "SELECT event_type, COUNT(*) as cnt FROM analytics.events GROUP BY 1 ORDER BY 2 DESC"},
}, display="bar")
# 4. Crear dashboard
dash = metabase_create_dashboard(mb, "Analytics Overview")
metabase_update_dashboard(mb, dash["id"], dashcards=[
{"id": -1, "card_id": card["id"], "row": 0, "col": 0, "size_x": 12, "size_y": 6},
])
```
### 2. ETL: archivo local -> BigQuery -> Metabase dashboard
```python
from bigquery import bq_auth, bq_load_from_file, bq_query, bq_preview_rows
from metabase import metabase_auth, metabase_execute_query
bq = bq_auth("my-project")
# Cargar datos
bq_load_from_file(bq, "/tmp/sales.csv", "warehouse", "sales",
source_format="CSV", write_disposition="WRITE_TRUNCATE", skip_leading_rows=1)
# Verificar
preview = bq_preview_rows(bq, "warehouse", "sales", max_results=3)
print(preview["total_rows"], "filas cargadas")
# Consultar via Metabase (si BQ esta registrado como database)
mb = metabase_auth("http://localhost:3000", "admin@fnregistry.local", "FnRegistry2024!")
result = metabase_execute_query(mb, 2, "SELECT region, SUM(amount) FROM sales GROUP BY 1")
```
### 3. Montar infraestructura desde cero
```bash
# 1. Levantar Metabase + Postgres
./fn run init_metabase --project fn_registry
# 2. Copiar registry.db al contenedor
./fn run setup_metabase_volume
# 3. Setup inicial
python/.venv/bin/python3 -c "
import sys; sys.path.insert(0, 'python/functions')
from metabase import metabase_setup
metabase_setup('http://localhost:3000', 'admin@fnregistry.local', 'FnRegistry2024!')
"
# 4. Registrar operations.db de una app
./fn run metabase_add_ops_db docker_tui
# 5. Dashboard operativo automatico
./fn run metabase_create_ops_dashboard docker_tui
```
### 4. Auditar costes de BigQuery
```python
from bigquery import bq_auth, bq_list_jobs, bq_query
bq = bq_auth("my-project")
# Jobs recientes completados
jobs = bq_list_jobs(bq, state_filter="done", max_results=20, all_users=True)
total_bytes = sum(j.get("bytes_processed") or 0 for j in jobs)
print(f"Ultimos 20 jobs: {total_bytes / (1024**3):.2f} GB procesados")
# Dry-run antes de queries caras
estimate = bq_query(bq, "SELECT * FROM analytics.events WHERE created_at > '2026-01-01'", dry_run=True)
gb = estimate["total_bytes_processed"] / (1024**3)
cost = gb * 6.25 # $6.25/TB on-demand
print(f"Coste estimado: ${cost:.2f} USD ({gb:.1f} GB)")
```
---
## Buscar mas funciones
Si necesitas algo que no esta aqui, busca en el registry:
```bash
# FTS5 por nombre o descripcion
./fn search "lo que buscas"
# Ver detalles de una funcion
./fn show <id>
# Inline desde Python
sqlite3 registry.db "SELECT id, description FROM functions WHERE id IN (SELECT id FROM functions_fts WHERE functions_fts MATCH 'description:export*') ORDER BY name;"
```
$ARGUMENTS
+28
View File
@@ -0,0 +1,28 @@
from .client import BQClient, bq_auth
from .tables import (
bq_create_table,
bq_get_table,
bq_list_tables,
bq_update_table,
bq_delete_table,
bq_preview_rows,
)
from .datasets import (
bq_create_dataset,
bq_get_dataset,
bq_list_datasets,
bq_update_dataset,
bq_delete_dataset,
)
from .queries import bq_query, bq_insert_rows, bq_load_from_gcs, bq_load_from_file, bq_export_to_gcs, bq_copy_table
from .jobs import bq_get_job, bq_list_jobs, bq_cancel_job
from .routines import bq_create_routine, bq_list_routines, bq_delete_routine
__all__ = [
"BQClient", "bq_auth",
"bq_create_table", "bq_get_table", "bq_list_tables", "bq_update_table", "bq_delete_table", "bq_preview_rows",
"bq_create_dataset", "bq_get_dataset", "bq_list_datasets", "bq_update_dataset", "bq_delete_dataset",
"bq_query", "bq_insert_rows", "bq_load_from_gcs", "bq_load_from_file", "bq_export_to_gcs", "bq_copy_table",
"bq_get_job", "bq_list_jobs", "bq_cancel_job",
"bq_create_routine", "bq_list_routines", "bq_delete_routine",
]
+56
View File
@@ -0,0 +1,56 @@
---
name: bq_auth
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_auth(project_id: str = '', credentials_path: str = '') -> BQClient"
description: "Autentica contra Google BigQuery con ADC o service account JSON. Retorna un BQClient listo para usar con todas las funciones CRUD."
tags: [bigquery, gcp, auth, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: project_id
desc: "ID del proyecto GCP (vacio = detectar de credenciales/entorno)"
- name: credentials_path
desc: "ruta a archivo JSON de service account (vacio = Application Default Credentials)"
output: "BQClient: cliente autenticado con proyecto resuelto"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/client.py"
---
## Ejemplo
```python
from bigquery import bq_auth
# ADC (gcloud auth application-default login)
client = bq_auth()
# Proyecto explicito
client = bq_auth("my-project-id")
# Service account
client = bq_auth(credentials_path="/path/to/service-account.json")
# Context manager
with bq_auth() as client:
# client se cierra automaticamente
pass
```
## Notas
Tres modos de autenticacion:
- Sin argumentos: usa Application Default Credentials (ADC) — requiere `gcloud auth application-default login`
- Con project_id: usa ADC pero fuerza el proyecto
- Con credentials_path: lee el JSON de service account directamente
El BQClient wrappea `google.cloud.bigquery.Client` y expone `_client` para que las funciones del modulo lo usen internamente.
@@ -0,0 +1,52 @@
---
name: bq_cancel_job
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_cancel_job(client: BQClient, job_id: str) -> dict"
description: "Cancela un job en ejecucion. Retorna el estado tras la cancelacion."
tags: [bigquery, gcp, job, cancel, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado de BigQuery obtenido con bq_auth"
- name: job_id
desc: "ID del job a cancelar (formato: 'proyecto:region.job_id' o solo 'job_id')"
output: "dict con campos: job_id (string), state (string, tipicamente 'DONE' tras la cancelacion)"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/jobs.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.jobs import bq_list_jobs, bq_cancel_job
client = bq_auth("my-project")
# Cancelar todos los jobs en ejecucion
running = bq_list_jobs(client, state_filter="running")
for job in running:
result = bq_cancel_job(client, job["job_id"])
print(f"Cancelado {result['job_id']}: {result['state']}")
```
## Notas
Lanza `google.api_core.exceptions.NotFound` si el job_id no existe en el proyecto.
BigQuery no garantiza cancelacion inmediata: el job puede seguir procesando brevemente antes de detenerse. El estado retornado refleja el estado al momento de la respuesta de la API, que tipicamente es `"DONE"`.
Cancelar un job ya completado o ya cancelado no genera error; la API lo acepta de forma idempotente y retorna el estado actual.
Solo el usuario que creo el job o un administrador del proyecto puede cancelarlo.
@@ -0,0 +1,72 @@
---
name: bq_copy_table
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_copy_table(client: BQClient, source_dataset: str, source_table: str, dest_dataset: str, dest_table: str, write_disposition: str = 'WRITE_EMPTY') -> dict"
description: "Copia una tabla BigQuery a otro dataset o tabla dentro del mismo proyecto usando copy_table del SDK. Espera la finalizacion del CopyJob."
tags: [bigquery, gcp, copy, table, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente BQClient autenticado contra el proyecto GCP"
- name: source_dataset
desc: "ID del dataset de origen en BigQuery"
- name: source_table
desc: "ID de la tabla de origen en BigQuery"
- name: dest_dataset
desc: "ID del dataset de destino en BigQuery (puede ser el mismo que el origen)"
- name: dest_table
desc: "ID de la tabla de destino; si no existe, BigQuery la crea automaticamente"
- name: write_disposition
desc: "comportamiento si la tabla destino ya existe: WRITE_EMPTY falla, WRITE_APPEND agrega, WRITE_TRUNCATE sobreescribe"
output: "dict con {job_id: ID del CopyJob, status: DONE o FAILED}"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/queries.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.queries import bq_copy_table
client = bq_auth("my-project")
# Copia de produccion a staging (falla si ya existe)
result = bq_copy_table(
client,
"production", "users",
"staging", "users_backup",
)
print(f"Copia completada: {result['status']} — job: {result['job_id']}")
# Copia sobreescribiendo destino
result = bq_copy_table(
client,
"raw", "events_2024",
"processed", "events_latest",
write_disposition="WRITE_TRUNCATE",
)
```
## Notas
`copy_table` solo funciona dentro del mismo proyecto GCP. Para copiar entre proyectos,
usar `bq_export_to_gcs` + `bq_load_from_gcs`.
Si la tabla destino no existe, BigQuery la crea con el schema de la tabla origen
independientemente del `write_disposition`.
El CopyJob es asincrono; `job.result()` bloquea hasta completar. La copia no mueve
datos fisicamente — BigQuery usa referencias de bloques internamente hasta que se
modifica la copia.
@@ -0,0 +1,59 @@
---
name: bq_create_dataset
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_create_dataset(client: BQClient, dataset_id: str, location: str = 'US', description: str = '', labels: dict[str, str] | None = None, default_table_expiration_ms: int = 0) -> dict"
description: "Crea un dataset en Google BigQuery con ubicacion, descripcion y labels. Usa client._client.create_dataset() del SDK oficial."
tags: [bigquery, gcp, dataset, create, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "instancia autenticada de BQClient"
- name: dataset_id
desc: "nombre del dataset dentro del proyecto (sin prefijo de proyecto)"
- name: location
desc: "ubicacion geografica del dataset: US, EU, us-central1, europe-west1, etc."
- name: description
desc: "descripcion opcional del dataset"
- name: labels
desc: "dict de labels key-value para categorizar el dataset"
- name: default_table_expiration_ms
desc: "tiempo de expiracion por defecto para tablas en milisegundos; 0 = sin expiracion"
output: "dict con dataset_id, project, full_id, location, description, labels, created, modified, default_table_expiration_ms"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/datasets.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.datasets import bq_create_dataset
client = bq_auth("my-project")
ds = bq_create_dataset(
client,
"analytics",
location="EU",
description="Data warehouse principal",
labels={"env": "prod", "team": "data"},
)
print(ds["full_id"], ds["location"])
# my-project.analytics EU
```
## Notas
Lanza `google.api_core.exceptions.Conflict` (409) si el dataset ya existe.
El `full_id` tiene formato `{project}.{dataset_id}` y puede usarse directamente como referencia en otras llamadas al SDK.
`default_table_expiration_ms` aplica a todas las tablas nuevas del dataset; las tablas existentes no se ven afectadas.
@@ -0,0 +1,80 @@
---
name: bq_create_routine
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_create_routine(client: BQClient, dataset_id: str, routine_id: str, body: str, routine_type: str = 'SCALAR_FUNCTION', language: str = 'SQL', arguments: list[dict] | None = None, return_type: str = '', description: str = '') -> dict"
description: "Crea una routine (UDF scalar, tabla o stored procedure) en BigQuery. Soporta SQL, JavaScript y Python."
tags: [bigquery, gcp, routine, udf, create, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "instancia autenticada de BQClient"
- name: dataset_id
desc: "nombre del dataset donde se crea la routine"
- name: routine_id
desc: "nombre/identificador de la routine dentro del dataset"
- name: body
desc: "cuerpo de la routine: expresion SQL, bloque JavaScript o codigo Python segun el lenguaje"
- name: routine_type
desc: "tipo de routine: SCALAR_FUNCTION para UDFs escalares, TABLE_VALUED_FUNCTION para UDFs de tabla, PROCEDURE para stored procedures"
- name: language
desc: "lenguaje de implementacion: SQL, JAVASCRIPT o PYTHON"
- name: arguments
desc: "lista de argumentos, cada uno como dict con claves 'name' y 'data_type' (ej: INT64, STRING, FLOAT64)"
- name: return_type
desc: "tipo de dato que retorna la funcion (ej: INT64, STRING); ignorado para PROCEDURE"
- name: description
desc: "descripcion opcional de la routine"
output: "dict con routine_id, dataset_id, project, routine_type, language, body, description, created y modified (ISO 8601)"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/routines.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.routines import bq_create_routine
client = bq_auth("my-project")
# UDF escalar SQL
fn = bq_create_routine(
client,
dataset_id="analytics",
routine_id="double_value",
body="x * 2",
arguments=[{"name": "x", "data_type": "INT64"}],
return_type="INT64",
description="Duplica un entero",
)
print(fn["routine_id"], fn["routine_type"], fn["language"])
# double_value SCALAR_FUNCTION SQL
# Stored procedure SQL
bq_create_routine(
client,
dataset_id="analytics",
routine_id="refresh_summary",
body="INSERT INTO summary SELECT * FROM raw WHERE date = CURRENT_DATE();",
routine_type="PROCEDURE",
)
```
## Notas
Lanza `google.api_core.exceptions.Conflict` (409) si la routine ya existe. Para actualizar una routine existente, eliminarla primero con `bq_delete_routine` y recrearla, o usar `client._client.update_routine()` directamente.
Los `data_type` de los argumentos deben ser constantes de `bigquery.StandardSqlTypeNames`: `INT64`, `FLOAT64`, `STRING`, `BOOL`, `BYTES`, `DATE`, `DATETIME`, `TIMESTAMP`, `TIME`, `NUMERIC`, `BIGNUMERIC`, `JSON`, `ARRAY`, `STRUCT`.
Las routines JavaScript permiten librerias externas via `imported_libraries` (no expuesto en este wrapper).
@@ -0,0 +1,68 @@
---
name: bq_create_table
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_create_table(client: BQClient, dataset_id: str, table_id: str, schema: list[dict], partitioning: dict | None = None, clustering: list[str] | None = None, description: str = '', labels: dict | None = None) -> dict"
description: "Crea una tabla en BigQuery con schema, particionamiento opcional y clustering. Usa client._client.create_table() del SDK oficial."
tags: [bigquery, gcp, table, create, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado BQClient obtenido con bq_auth"
- name: dataset_id
desc: "ID del dataset de BigQuery donde crear la tabla"
- name: table_id
desc: "nombre (ID) de la tabla a crear"
- name: schema
desc: "lista de dicts con definicion de columnas: [{name, type, mode, description}]. Tipos: STRING, INTEGER, FLOAT, BOOLEAN, DATE, TIMESTAMP, RECORD, etc."
- name: partitioning
desc: "dict opcional con tipo y campo de particion: {type: DAY|MONTH|YEAR|HOUR, field: nombre_col}. None = sin particion"
- name: clustering
desc: "lista de hasta 4 columnas para clustering (ordenacion fisica). None = sin clustering"
- name: description
desc: "descripcion legible de la tabla (vacio = sin descripcion)"
- name: labels
desc: "etiquetas clave-valor para la tabla, ej: {env: prod, team: data}"
output: "dict con metadata de la tabla creada: table_id, dataset_id, project, full_id, schema, num_rows, num_bytes, created, modified, type, partitioning, clustering, description, labels"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/tables.py"
---
## Ejemplo
```python
from bigquery import bq_auth, bq_create_table
client = bq_auth("mi-proyecto")
tabla = bq_create_table(
client,
dataset_id="ventas_ds",
table_id="transacciones",
schema=[
{"name": "id", "type": "INTEGER", "mode": "REQUIRED", "description": "ID unico"},
{"name": "fecha", "type": "DATE", "mode": "NULLABLE", "description": "Fecha de la transaccion"},
{"name": "monto", "type": "FLOAT", "mode": "NULLABLE", "description": "Monto en USD"},
{"name": "pais", "type": "STRING", "mode": "NULLABLE", "description": "Codigo de pais"},
],
partitioning={"type": "MONTH", "field": "fecha"},
clustering=["pais"],
description="Transacciones de ventas por mes",
labels={"env": "prod", "team": "data"},
)
print(tabla["full_id"])
```
## Notas
El schema se convierte internamente de dicts a objetos `bigquery.SchemaField`. El particionamiento `TIME` soporta DAY, MONTH, YEAR y HOUR sobre columnas DATE/DATETIME/TIMESTAMP. Si `field` se omite en `partitioning`, BigQuery usa la pseudo-columna `_PARTITIONTIME`. El clustering requiere que las columnas existan en el schema y mejora rendimiento en filtros frecuentes.
@@ -0,0 +1,51 @@
---
name: bq_delete_dataset
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_delete_dataset(client: BQClient, dataset_id: str, delete_contents: bool = False) -> None"
description: "Elimina un dataset de Google BigQuery. IRREVERSIBLE. Usa client._client.delete_dataset() del SDK oficial."
tags: [bigquery, gcp, dataset, delete, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "instancia autenticada de BQClient"
- name: dataset_id
desc: "nombre del dataset a eliminar (sin prefijo de proyecto)"
- name: delete_contents
desc: "si True elimina todas las tablas y vistas del dataset antes de borrarlo; si False falla si el dataset contiene objetos"
output: "None"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/datasets.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.datasets import bq_delete_dataset
client = bq_auth("my-project")
# Eliminar dataset vacio
bq_delete_dataset(client, "temp_dataset")
# Eliminar dataset con todas sus tablas
bq_delete_dataset(client, "temp_analytics", delete_contents=True)
```
## Notas
IRREVERSIBLE. Todos los datos del dataset se pierden permanentemente.
Lanza `google.api_core.exceptions.NotFound` (404) si el dataset no existe.
Lanza `google.api_core.exceptions.BadRequest` (400) si el dataset contiene tablas y `delete_contents=False`.
Por seguridad el valor por defecto de `delete_contents` es `False` — requiere confirmacion explicita para borrar contenido.
@@ -0,0 +1,51 @@
---
name: bq_delete_routine
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_delete_routine(client: BQClient, dataset_id: str, routine_id: str) -> None"
description: "Elimina una routine de un dataset."
tags: [bigquery, gcp, routine, udf, delete, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: true
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "instancia autenticada de BQClient"
- name: dataset_id
desc: "nombre del dataset que contiene la routine"
- name: routine_id
desc: "nombre/identificador de la routine a eliminar"
output: "None; lanza NotFound si la routine no existe"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/routines.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.routines import bq_delete_routine
client = bq_auth("my-project")
bq_delete_routine(client, "analytics", "double_value")
# La routine queda eliminada permanentemente
```
## Notas
Lanza `google.api_core.exceptions.NotFound` (404) si la routine no existe. La operacion es permanente e irreversible.
Para eliminar y recrear una routine actualizada, combinar con `bq_create_routine`:
```python
bq_delete_routine(client, "analytics", "double_value")
bq_create_routine(client, "analytics", "double_value", body="x * 3", ...)
```
@@ -0,0 +1,51 @@
---
name: bq_delete_table
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_delete_table(client: BQClient, dataset_id: str, table_id: str) -> None"
description: "Elimina permanentemente una tabla de BigQuery. IRREVERSIBLE. Usa client._client.delete_table() del SDK oficial."
tags: [bigquery, gcp, table, delete, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: true
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado BQClient obtenido con bq_auth"
- name: dataset_id
desc: "ID del dataset que contiene la tabla"
- name: table_id
desc: "nombre (ID) de la tabla a eliminar"
output: "None. Lanza excepcion si la tabla no existe o no hay permisos"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/tables.py"
---
## Ejemplo
```python
from bigquery import bq_auth, bq_delete_table
client = bq_auth("mi-proyecto")
# Eliminar tabla temporal
bq_delete_table(client, "mi_dataset", "tabla_temporal_2024")
# Verificar que no existe (capturar excepcion)
from google.api_core.exceptions import NotFound
try:
bq_delete_table(client, "mi_dataset", "tabla_que_no_existe")
except NotFound as e:
print(f"No encontrada: {e}")
```
## Notas
La eliminacion es PERMANENTE — BigQuery no tiene papelera de reciclaje para tablas. Considerar exportar los datos a GCS antes de eliminar si hay posibilidad de necesitarlos. Si el dataset tiene `defaultTableExpirationMs` configurado, las tablas se pueden dejar expirar automaticamente en vez de eliminar manualmente. Requiere permiso `bigquery.tables.delete` sobre el dataset.
@@ -0,0 +1,73 @@
---
name: bq_export_to_gcs
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_export_to_gcs(client: BQClient, dataset_id: str, table_id: str, destination_uri: str, destination_format: str = 'CSV', compression: str = 'NONE') -> dict"
description: "Exporta una tabla BigQuery a Google Cloud Storage usando extract_table del SDK. Soporta CSV, JSON, Avro y Parquet con compresion opcional."
tags: [bigquery, gcp, export, gcs, google-cloud, python, etl]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente BQClient autenticado contra el proyecto GCP"
- name: dataset_id
desc: "ID del dataset de origen en BigQuery"
- name: table_id
desc: "ID de la tabla de origen en BigQuery"
- name: destination_uri
desc: "URI de destino en GCS; para tablas grandes usar wildcard: gs://bucket/prefix_*.csv"
- name: destination_format
desc: "formato de exportacion: CSV, NEWLINE_DELIMITED_JSON, AVRO, PARQUET"
- name: compression
desc: "algoritmo de compresion: NONE, GZIP, SNAPPY (solo Parquet/Avro), DEFLATE"
output: "dict con {job_id: ID del ExtractJob, destination_uri: URI de destino, status: DONE o FAILED}"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/queries.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.queries import bq_export_to_gcs
client = bq_auth("my-project")
# Exportar como CSV comprimido con GZIP
result = bq_export_to_gcs(
client,
"my_dataset", "users",
"gs://my-bucket/exports/users_*.csv.gz",
compression="GZIP",
)
print(f"Exportado: {result['destination_uri']} — job: {result['job_id']}")
# Exportar como Parquet con compresion Snappy
result = bq_export_to_gcs(
client,
"my_dataset", "events",
"gs://my-bucket/exports/events_*.parquet",
destination_format="PARQUET",
compression="SNAPPY",
)
print(f"Status: {result['status']}")
```
## Notas
Para tablas grandes BigQuery genera multiples archivos automaticamente cuando se usa
wildcard (`*`) en el URI. Sin wildcard falla si la tabla supera 1 GB.
`SNAPPY` solo es valido para formatos binarios (PARQUET, AVRO). Usar `GZIP` para CSV
y NEWLINE_DELIMITED_JSON.
El ExtractJob es asincrono en BigQuery; `job.result()` bloquea hasta completar.
@@ -0,0 +1,45 @@
---
name: bq_get_dataset
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_get_dataset(client: BQClient, dataset_id: str) -> dict"
description: "Obtiene los detalles completos de un dataset de Google BigQuery. Usa client._client.get_dataset() del SDK oficial."
tags: [bigquery, gcp, dataset, get, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "instancia autenticada de BQClient"
- name: dataset_id
desc: "nombre del dataset a consultar (sin prefijo de proyecto)"
output: "dict con dataset_id, project, full_id, location, description, labels, created, modified, default_table_expiration_ms"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/datasets.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.datasets import bq_get_dataset
client = bq_auth("my-project")
ds = bq_get_dataset(client, "analytics")
print(ds["location"], ds["description"])
print(ds["created"]) # ISO 8601: "2024-01-15T10:30:00+00:00"
```
## Notas
Lanza `google.api_core.exceptions.NotFound` (404) si el dataset no existe.
Los campos `created` y `modified` son strings ISO 8601 con timezone UTC, o `None` si el SDK no los retorna.
`labels` es un dict vacio `{}` si el dataset no tiene labels.
+60
View File
@@ -0,0 +1,60 @@
---
name: bq_get_job
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_get_job(client: BQClient, job_id: str) -> dict"
description: "Obtiene detalles completos de un job por su ID incluyendo estado, bytes procesados y errores. Incluye campos adicionales respecto a bq_list_jobs: destination_table, query y lista de errores."
tags: [bigquery, gcp, job, get, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado de BigQuery obtenido con bq_auth"
- name: job_id
desc: "ID del job a consultar (formato: 'proyecto:region.job_id' o solo 'job_id')"
output: "dict con campos: job_id, job_type, state, created, started, ended, user_email, bytes_processed, error, y opcionalmente destination_table, query, errors"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/jobs.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.jobs import bq_get_job
client = bq_auth("my-project")
job = bq_get_job(client, "job_abc123")
print(job["state"], job["bytes_processed"])
# Inspeccionar query SQL del job
if job.get("query"):
print("SQL:", job["query"][:200])
# Ver errores detallados
if job["error"]:
print("Error principal:", job["error"])
for err in job.get("errors", []):
print(" -", err)
```
## Notas
Lanza `google.api_core.exceptions.NotFound` si el job_id no existe en el proyecto.
A diferencia de `bq_list_jobs`, este metodo retorna campos adicionales cuando estan disponibles:
- `destination_table`: tabla destino (solo en query/load jobs con destino explicito)
- `query`: texto SQL del job (solo en query jobs)
- `errors`: lista completa de errores (cada uno como dict con `reason`, `message`, `location`)
Los campos `created`, `started` y `ended` se serializan como strings ISO 8601.
+48
View File
@@ -0,0 +1,48 @@
---
name: bq_get_table
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_get_table(client: BQClient, dataset_id: str, table_id: str) -> dict"
description: "Obtiene los metadatos completos de una tabla BigQuery incluyendo schema, estadisticas y configuracion. Usa client._client.get_table() del SDK oficial."
tags: [bigquery, gcp, table, get, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado BQClient obtenido con bq_auth"
- name: dataset_id
desc: "ID del dataset que contiene la tabla"
- name: table_id
desc: "nombre (ID) de la tabla a consultar"
output: "dict con metadata completa: table_id, dataset_id, project, full_id, schema (lista de dicts), num_rows, num_bytes, created (ISO 8601), modified (ISO 8601), type, partitioning, clustering, description, labels"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/tables.py"
---
## Ejemplo
```python
from bigquery import bq_auth, bq_get_table
client = bq_auth("mi-proyecto")
tabla = bq_get_table(client, "ventas_ds", "transacciones")
print(tabla["full_id"]) # mi-proyecto.ventas_ds.transacciones
print(tabla["num_rows"]) # filas totales
print(tabla["num_bytes"]) # bytes almacenados
for col in tabla["schema"]:
print(col["name"], col["type"], col["mode"])
```
## Notas
`num_rows` y `num_bytes` son estadisticas actualizadas por BigQuery periodicamente (pueden tener un pequeno retraso). El campo `type` puede ser TABLE, VIEW, MATERIALIZED_VIEW o EXTERNAL. `partitioning` es None si la tabla no tiene particionamiento. `created` y `modified` estan en formato ISO 8601.
@@ -0,0 +1,60 @@
---
name: bq_insert_rows
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_insert_rows(client: BQClient, dataset_id: str, table_id: str, rows: list[dict]) -> dict"
description: "Inserta filas en una tabla BigQuery usando streaming insert (insert_rows_json). Retorna el conteo de filas insertadas y errores por fila."
tags: [bigquery, gcp, insert, streaming, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente BQClient autenticado contra el proyecto GCP"
- name: dataset_id
desc: "ID del dataset de destino en BigQuery"
- name: table_id
desc: "ID de la tabla de destino en BigQuery"
- name: rows
desc: "lista de dicts con los datos a insertar; las claves deben coincidir con las columnas de la tabla"
output: "dict con {inserted: N filas insertadas sin error, errors: lista de errores por fila retornada por la API}"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/queries.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.queries import bq_insert_rows
client = bq_auth("my-project")
result = bq_insert_rows(client, "my_dataset", "events", [
{"event_id": "abc1", "user_id": 42, "ts": "2024-01-01T12:00:00Z", "action": "click"},
{"event_id": "abc2", "user_id": 99, "ts": "2024-01-01T12:01:00Z", "action": "view"},
])
print(f"Insertadas: {result['inserted']}")
if result["errors"]:
print("Errores:", result["errors"])
```
## Notas
El streaming insert tiene disponibilidad casi inmediata pero no es transaccional.
Las filas pueden aparecer duplicadas si el job se reintenta — BigQuery no garantiza
exactamente-una-vez con insert_rows_json.
`errors` es la lista retornada directamente por la API. Cada elemento es un dict con
`index` (posicion de la fila fallida) y `errors` (lista de mensajes de error).
Para cargas masivas o garantias ACID, preferir `bq_load_from_gcs` o `bq_load_from_file`.
@@ -0,0 +1,45 @@
---
name: bq_list_datasets
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_list_datasets(client: BQClient) -> list[dict]"
description: "Lista todos los datasets del proyecto de Google BigQuery. Usa client._client.list_datasets() del SDK oficial."
tags: [bigquery, gcp, dataset, list, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "instancia autenticada de BQClient"
output: "lista de dicts con dataset_id, project y full_id por cada dataset del proyecto"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/datasets.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.datasets import bq_list_datasets
client = bq_auth("my-project")
datasets = bq_list_datasets(client)
for ds in datasets:
print(ds["dataset_id"], ds["full_id"])
# analytics my-project.analytics
# raw_data my-project.raw_data
```
## Notas
Retorna solo campos basicos (dataset_id, project, full_id). Para obtener detalles completos (location, description, labels) usar `bq_get_dataset` sobre cada item.
El SDK retorna un iterador; esta funcion lo materializa en una lista completa.
Si el proyecto no tiene datasets, retorna lista vacia `[]`.
+61
View File
@@ -0,0 +1,61 @@
---
name: bq_list_jobs
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_list_jobs(client: BQClient, state_filter: str = '', max_results: int = 50, all_users: bool = False) -> list[dict]"
description: "Lista jobs del proyecto con filtro por estado (running, pending, done). Retorna una lista de dicts planos con metadatos de cada job."
tags: [bigquery, gcp, job, list, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado de BigQuery obtenido con bq_auth"
- name: state_filter
desc: "filtro por estado del job: 'running', 'pending' o 'done'; vacio = todos los estados"
- name: max_results
desc: "numero maximo de jobs a retornar (por defecto 50)"
- name: all_users
desc: "si True lista jobs de todos los usuarios del proyecto; requiere permisos de administrador"
output: "lista de dicts con campos: job_id, job_type, state, created, started, ended, user_email, bytes_processed, error"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/jobs.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.jobs import bq_list_jobs
client = bq_auth("my-project")
# Todos los jobs recientes
jobs = bq_list_jobs(client, max_results=20)
# Solo jobs en ejecucion
running = bq_list_jobs(client, state_filter="running")
for j in running:
print(j["job_id"], j["state"], j["job_type"])
# Jobs completados de todos los usuarios
done = bq_list_jobs(client, state_filter="done", all_users=True, max_results=100)
```
## Notas
Los campos `created`, `started` y `ended` se serializan como strings ISO 8601 (o `None` si no estan disponibles).
El campo `bytes_processed` solo esta disponible en query jobs; para otros tipos de job (load, export, copy) se retorna `None`.
El campo `error` contiene el error_result como string si el job fallo, o `None` si no hay error.
Para obtener el texto completo del SQL de un job concreto, usar `bq_get_job` que incluye el campo `query`.
@@ -0,0 +1,49 @@
---
name: bq_list_routines
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_list_routines(client: BQClient, dataset_id: str) -> list[dict]"
description: "Lista routines de un dataset incluyendo tipo y lenguaje."
tags: [bigquery, gcp, routine, udf, list, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "instancia autenticada de BQClient"
- name: dataset_id
desc: "nombre del dataset cuyas routines se quieren listar"
output: "lista de dicts con routine_id, dataset_id, project, routine_type y language para cada routine del dataset"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/routines.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.routines import bq_list_routines
client = bq_auth("my-project")
routines = bq_list_routines(client, "analytics")
for r in routines:
print(r["routine_id"], r["routine_type"], r["language"])
# double_value SCALAR_FUNCTION SQL
# refresh_summary PROCEDURE SQL
# parse_json SCALAR_FUNCTION JAVASCRIPT
```
## Notas
Retorna lista vacia si el dataset no tiene routines. Lanza `google.api_core.exceptions.NotFound` si el dataset no existe.
El listado solo incluye metadata basica (routine_id, tipo, lenguaje). Para obtener el cuerpo y argumentos completos de una routine especifica, usar `client._client.get_routine(f"{project}.{dataset}.{routine_id}")`.
@@ -0,0 +1,48 @@
---
name: bq_list_tables
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_list_tables(client: BQClient, dataset_id: str) -> list[dict]"
description: "Lista todas las tablas (y vistas) de un dataset BigQuery con informacion resumida. Usa client._client.list_tables() del SDK oficial."
tags: [bigquery, gcp, table, list, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado BQClient obtenido con bq_auth"
- name: dataset_id
desc: "ID del dataset a listar"
output: "lista de dicts, uno por objeto del dataset. Cada dict contiene: table_id (nombre), full_id (project.dataset.table), type (TABLE, VIEW, MATERIALIZED_VIEW, EXTERNAL)"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/tables.py"
---
## Ejemplo
```python
from bigquery import bq_auth, bq_list_tables
client = bq_auth("mi-proyecto")
tablas = bq_list_tables(client, "ventas_ds")
for t in tablas:
print(t["table_id"], t["type"])
# transacciones TABLE
# vista_mensual VIEW
# Filtrar solo tablas fisicas
solo_tablas = [t for t in tablas if t["type"] == "TABLE"]
```
## Notas
Retorna objetos resumidos (`ListTableItem`), no los metadatos completos. Para obtener schema, estadisticas y configuracion completa usar `bq_get_table`. El listado incluye tablas, vistas, vistas materializadas y tablas externas (EXTERNAL). El orden de retorno no esta garantizado.
@@ -0,0 +1,75 @@
---
name: bq_load_from_file
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_load_from_file(client: BQClient, file_path: str, dataset_id: str, table_id: str, source_format: str = 'CSV', write_disposition: str = 'WRITE_APPEND', autodetect: bool = True, skip_leading_rows: int = 0) -> dict"
description: "Carga datos desde un archivo local a una tabla BigQuery usando load_table_from_file del SDK. Equivalente a bq_load_from_gcs pero para disco local."
tags: [bigquery, gcp, load, file, google-cloud, python, etl]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente BQClient autenticado contra el proyecto GCP"
- name: file_path
desc: "ruta absoluta o relativa al archivo local a cargar"
- name: dataset_id
desc: "ID del dataset de destino en BigQuery"
- name: table_id
desc: "ID de la tabla de destino en BigQuery"
- name: source_format
desc: "formato del archivo fuente: CSV, NEWLINE_DELIMITED_JSON, AVRO, PARQUET, ORC"
- name: write_disposition
desc: "comportamiento si la tabla ya existe: WRITE_APPEND agrega, WRITE_TRUNCATE reemplaza, WRITE_EMPTY falla si hay datos"
- name: autodetect
desc: "si True, BigQuery infiere el schema automaticamente desde los datos"
- name: skip_leading_rows
desc: "numero de filas a ignorar al inicio del archivo (tipicamente 1 para saltar cabeceras CSV)"
output: "dict con {job_id: ID del LoadJob, rows_loaded: filas cargadas, status: DONE o FAILED}"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/queries.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.queries import bq_load_from_file
client = bq_auth("my-project")
# Cargar CSV local con cabecera
result = bq_load_from_file(
client,
"/tmp/export_users.csv",
"my_dataset", "users",
skip_leading_rows=1,
write_disposition="WRITE_TRUNCATE",
)
print(f"Cargadas {result['rows_loaded']} filas — job: {result['job_id']}")
# Cargar JSONL local
result = bq_load_from_file(
client,
"/data/events.jsonl",
"my_dataset", "events",
source_format="NEWLINE_DELIMITED_JSON",
)
```
## Notas
El archivo se abre en modo binario (`rb`) y se sube directamente al job de BigQuery.
Para archivos muy grandes, preferir `bq_load_from_gcs` — subir primero a GCS y luego
cargar desde ahi es mas eficiente y permite paralelismo.
La funcion bloquea hasta que el job termina (`job.result()`). Los archivos Parquet y
Avro no admiten `skip_leading_rows` — ese parametro solo aplica para CSV.
@@ -0,0 +1,77 @@
---
name: bq_load_from_gcs
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_load_from_gcs(client: BQClient, uri: str | list[str], dataset_id: str, table_id: str, source_format: str = 'CSV', write_disposition: str = 'WRITE_APPEND', autodetect: bool = True, skip_leading_rows: int = 0) -> dict"
description: "Carga datos desde uno o varios URIs de Google Cloud Storage a una tabla BigQuery configurando un LoadJob. Espera la finalizacion del job."
tags: [bigquery, gcp, load, gcs, google-cloud, python, etl]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente BQClient autenticado contra el proyecto GCP"
- name: uri
desc: "URI de GCS de origen (gs://bucket/file.csv) o lista de URIs; soporta wildcards como gs://bucket/prefix_*.csv"
- name: dataset_id
desc: "ID del dataset de destino en BigQuery"
- name: table_id
desc: "ID de la tabla de destino en BigQuery"
- name: source_format
desc: "formato del archivo fuente: CSV, NEWLINE_DELIMITED_JSON, AVRO, PARQUET, ORC"
- name: write_disposition
desc: "comportamiento si la tabla ya existe: WRITE_APPEND agrega, WRITE_TRUNCATE reemplaza, WRITE_EMPTY falla si hay datos"
- name: autodetect
desc: "si True, BigQuery infiere el schema automaticamente desde los datos"
- name: skip_leading_rows
desc: "numero de filas a ignorar al inicio del archivo (tipicamente 1 para saltar cabeceras CSV)"
output: "dict con {job_id: ID del LoadJob, rows_loaded: filas cargadas, status: DONE o FAILED}"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/queries.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.queries import bq_load_from_gcs
client = bq_auth("my-project")
# Cargar un archivo CSV con cabecera
result = bq_load_from_gcs(
client,
"gs://my-bucket/data/users_2024.csv",
"my_dataset", "users",
skip_leading_rows=1,
)
print(f"Cargadas {result['rows_loaded']} filas — job: {result['job_id']}")
# Cargar multiples archivos Parquet reemplazando la tabla
result = bq_load_from_gcs(
client,
["gs://my-bucket/export/part_001.parquet", "gs://my-bucket/export/part_002.parquet"],
"my_dataset", "events",
source_format="PARQUET",
write_disposition="WRITE_TRUNCATE",
)
```
## Notas
El job se ejecuta de forma asincrona en BigQuery; `job.result()` bloquea hasta completar.
Los wildcards en el URI (`gs://bucket/prefix_*.csv`) son resueltos por GCS — BigQuery
acepta la lista de archivos resultante como una sola carga atomica.
`autodetect=True` es conveniente pero puede inferir tipos incorrectamente para columnas
con valores nulos o mixtos. Para produccion, definir el schema explicitamente via
`job_config.schema`.
@@ -0,0 +1,50 @@
---
name: bq_preview_rows
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_preview_rows(client: BQClient, dataset_id: str, table_id: str, max_results: int = 10) -> dict"
description: "Obtiene una muestra de filas de una tabla BigQuery sin ejecutar query SQL, sin coste de procesamiento. Usa client._client.list_rows() del SDK oficial."
tags: [bigquery, gcp, table, preview, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado BQClient obtenido con bq_auth"
- name: dataset_id
desc: "ID del dataset que contiene la tabla"
- name: table_id
desc: "nombre (ID) de la tabla a previsualizar"
- name: max_results
desc: "numero maximo de filas a retornar (default: 10)"
output: "dict con: columns (lista de nombres de columnas), rows (lista de listas con valores), total_rows (int con total de filas en la tabla completa)"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/tables.py"
---
## Ejemplo
```python
from bigquery import bq_auth, bq_preview_rows
client = bq_auth("mi-proyecto")
preview = bq_preview_rows(client, "ventas_ds", "transacciones", max_results=5)
print(preview["columns"]) # ["id", "fecha", "monto", "pais"]
print(f"Total filas: {preview['total_rows']}")
for row in preview["rows"]:
print(row) # [1, datetime.date(2024, 1, 15), 99.5, "MX"]
```
## Notas
`list_rows()` usa la Storage Read API internamente y NO genera un job de query — por tanto no se contabiliza en el uso de bytes procesados. Ideal para inspeccionar rapidamente la estructura y contenido de una tabla. El orden de las filas retornadas no esta garantizado (depende del almacenamiento interno de BigQuery). Para muestras reproducibles o con filtros, usar una query SQL con `LIMIT`. `total_rows` refleja el conteo de la tabla en el momento de la llamada a `get_table()`, que puede tener un pequeno retraso respecto al dato real.
+69
View File
@@ -0,0 +1,69 @@
---
name: bq_query
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_query(client: BQClient, sql: str, params: list[dict] | None = None, dry_run: bool = False) -> dict"
description: "Ejecuta una query SQL en BigQuery con soporte para parametros tipados y modo dry-run para estimacion de costos."
tags: [bigquery, gcp, query, sql, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente BQClient autenticado contra el proyecto GCP"
- name: sql
desc: "query SQL a ejecutar; usar @nombre para referencias a parametros tipados"
- name: params
desc: "lista de parametros tipados, cada uno con {name, type, value}; tipos: STRING, INT64, FLOAT64, BOOL, DATE, TIMESTAMP"
- name: dry_run
desc: "si True, estima bytes procesados/facturados sin ejecutar la query"
output: "si dry_run=True: {total_bytes_processed, total_bytes_billed}; si False: {columns, rows, total_rows, bytes_processed, cache_hit}"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/queries.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.queries import bq_query
client = bq_auth("my-project")
# Query simple
result = bq_query(client, "SELECT COUNT(*) as n FROM `my_project.dataset.table`")
print(result["columns"]) # ["n"]
print(result["rows"]) # [[12345]]
# Con parametros
result = bq_query(
client,
"SELECT * FROM `dataset.users` WHERE status = @s AND age > @a",
params=[
{"name": "s", "type": "STRING", "value": "active"},
{"name": "a", "type": "INT64", "value": 18},
],
)
# Dry-run para estimar costo
est = bq_query(client, "SELECT * FROM `dataset.big_table`", dry_run=True)
print(f"Procesaria {est['total_bytes_processed'] / 1e9:.2f} GB")
```
## Notas
Usa `bigquery.ScalarQueryParameter` para parametros — solo soporta escalares. Para arrays
usar `bigquery.ArrayQueryParameter` directamente si se necesita.
En dry_run=True el job se crea pero no se ejecuta; `job.result()` no se llama. BigQuery
retorna la estimacion de bytes sin cargo.
`cache_hit=True` indica que el resultado provino de la cache de BigQuery (sin costo).
@@ -0,0 +1,59 @@
---
name: bq_update_dataset
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_update_dataset(client: BQClient, dataset_id: str, description: str | None = None, labels: dict[str, str] | None = None, default_table_expiration_ms: int | None = None) -> dict"
description: "Actualiza campos de un dataset de Google BigQuery. Solo modifica los campos pasados explicitamente (no-None). Usa client._client.update_dataset() del SDK oficial."
tags: [bigquery, gcp, dataset, update, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "instancia autenticada de BQClient"
- name: dataset_id
desc: "nombre del dataset a actualizar (sin prefijo de proyecto)"
- name: description
desc: "nueva descripcion del dataset; None = no modificar"
- name: labels
desc: "nuevos labels key-value; None = no modificar; dict vacio {} = eliminar todos los labels"
- name: default_table_expiration_ms
desc: "nueva expiracion por defecto de tablas en ms; None = no modificar; 0 = eliminar expiracion"
output: "dict con el dataset actualizado: dataset_id, project, full_id, location, description, labels, created, modified, default_table_expiration_ms"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/datasets.py"
---
## Ejemplo
```python
from bigquery.client import bq_auth
from bigquery.datasets import bq_update_dataset
client = bq_auth("my-project")
# Actualizar solo la descripcion
ds = bq_update_dataset(client, "analytics", description="Data warehouse actualizado")
# Actualizar labels
ds = bq_update_dataset(client, "analytics", labels={"env": "prod", "version": "2"})
# Eliminar expiracion de tablas
ds = bq_update_dataset(client, "analytics", default_table_expiration_ms=0)
print(ds["description"])
```
## Notas
Lanza `google.api_core.exceptions.NotFound` (404) si el dataset no existe.
Si no se pasa ningun campo (todos None), retorna el dataset sin modificar (no llama a update_dataset).
Para eliminar todos los labels pasar `labels={}`. Para eliminar la expiracion de tablas pasar `default_table_expiration_ms=0`.
El SDK hace un GET interno antes del PATCH para obtener el estado actual; esta funcion replica ese patron explicitamente.
@@ -0,0 +1,65 @@
---
name: bq_update_table
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: impure
signature: "def bq_update_table(client: BQClient, dataset_id: str, table_id: str, schema: list[dict] | None = None, description: str | None = None, labels: dict | None = None) -> dict"
description: "Actualiza metadatos de una tabla BigQuery: schema (solo adicion de columnas), descripcion y etiquetas. Usa client._client.update_table() del SDK oficial."
tags: [bigquery, gcp, table, update, google-cloud, python]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [google-cloud-bigquery]
params:
- name: client
desc: "cliente autenticado BQClient obtenido con bq_auth"
- name: dataset_id
desc: "ID del dataset que contiene la tabla"
- name: table_id
desc: "nombre (ID) de la tabla a actualizar"
- name: schema
desc: "schema completo nuevo (columnas existentes + nuevas al final). BigQuery SOLO permite agregar columnas, no eliminar ni renombrar. None = no modificar"
- name: description
desc: "nueva descripcion de la tabla. None = no modificar"
- name: labels
desc: "nuevas etiquetas clave-valor (reemplaza las existentes). None = no modificar"
output: "dict con la metadata actualizada de la tabla (misma estructura que bq_get_table)"
tested: false
tests: []
test_file_path: ""
file_path: "python/functions/bigquery/tables.py"
---
## Ejemplo
```python
from bigquery import bq_auth, bq_update_table
client = bq_auth("mi-proyecto")
# Actualizar descripcion y etiquetas
tabla = bq_update_table(
client, "ventas_ds", "transacciones",
description="Transacciones de ventas — actualizado",
labels={"env": "prod", "team": "data", "version": "2"},
)
# Agregar columna nueva al schema existente
tabla = bq_update_table(
client, "ventas_ds", "transacciones",
schema=[
{"name": "id", "type": "INTEGER", "mode": "REQUIRED"},
{"name": "fecha", "type": "DATE", "mode": "NULLABLE"},
{"name": "monto", "type": "FLOAT", "mode": "NULLABLE"},
{"name": "nueva_col", "type": "STRING", "mode": "NULLABLE"}, # nueva
],
)
```
## Notas
BigQuery tiene restricciones estrictas sobre modificacion de schema: se pueden agregar columnas NULLABLE o REPEATED al final, pero NO se pueden eliminar columnas, renombrar columnas ni cambiar el tipo de una columna existente. Si se necesita ese tipo de cambio, la alternativa es crear una tabla nueva con `CREATE TABLE AS SELECT`. Los campos `None` no generan actualizacion — solo se envian al API los campos que cambian.
+63
View File
@@ -0,0 +1,63 @@
"""Cliente base para Google BigQuery."""
from dataclasses import dataclass, field
from google.cloud import bigquery
from google.oauth2 import service_account
@dataclass
class BQClient:
"""Cliente para Google BigQuery.
Attributes:
project_id: ID del proyecto GCP.
_client: Cliente oficial de BigQuery (interno).
"""
project_id: str
_client: bigquery.Client = field(repr=False)
def close(self) -> None:
"""Cierra el cliente BigQuery."""
self._client.close()
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def bq_auth(project_id: str = "", credentials_path: str = "") -> BQClient:
"""Autentica contra Google BigQuery.
Tres modos de autenticacion:
1. ADC (Application Default Credentials): sin argumentos, usa gcloud auth
2. Service account JSON: con credentials_path
3. Proyecto explicito: con project_id (usa ADC para credenciales)
Args:
project_id: ID del proyecto GCP. Vacio = detectar de credenciales.
credentials_path: Ruta a archivo JSON de service account. Vacio = ADC.
Returns:
BQClient autenticado listo para usar.
Raises:
google.auth.exceptions.DefaultCredentialsError: Si no hay credenciales configuradas.
FileNotFoundError: Si credentials_path no existe.
Example:
>>> client = bq_auth() # ADC
>>> client = bq_auth("my-project") # ADC con proyecto explicito
>>> client = bq_auth(credentials_path="/path/to/sa.json") # Service account
"""
if credentials_path:
creds = service_account.Credentials.from_service_account_file(credentials_path)
proj = project_id or creds.project_id
client = bigquery.Client(credentials=creds, project=proj)
elif project_id:
client = bigquery.Client(project=project_id)
else:
client = bigquery.Client()
return BQClient(project_id=client.project, _client=client)
+176
View File
@@ -0,0 +1,176 @@
"""CRUD de datasets en Google BigQuery."""
from .client import BQClient
from google.cloud import bigquery
def bq_create_dataset(
client: BQClient,
dataset_id: str,
location: str = "US",
description: str = "",
labels: dict[str, str] | None = None,
default_table_expiration_ms: int = 0,
) -> dict:
"""Crea un dataset en BigQuery.
Args:
client: Cliente autenticado.
dataset_id: ID del dataset (solo el nombre, sin proyecto).
location: Ubicacion geografica (US, EU, us-central1, etc.).
description: Descripcion opcional.
labels: Labels key-value opcionales.
default_table_expiration_ms: Expiracion por defecto de tablas en ms. 0 = sin expiracion.
Returns:
Dict con: dataset_id, project, full_id, location, description, labels, created, modified,
default_table_expiration_ms.
Raises:
google.api_core.exceptions.Conflict: Si el dataset ya existe (409).
Example:
>>> ds = bq_create_dataset(client, "analytics", location="EU", description="Data warehouse")
"""
ref = f"{client.project_id}.{dataset_id}"
ds = bigquery.Dataset(ref)
ds.location = location
if description:
ds.description = description
if labels:
ds.labels = labels
if default_table_expiration_ms > 0:
ds.default_table_expiration_ms = default_table_expiration_ms
created = client._client.create_dataset(ds)
return _dataset_to_dict(created)
def bq_get_dataset(client: BQClient, dataset_id: str) -> dict:
"""Obtiene los detalles de un dataset.
Args:
client: Cliente autenticado.
dataset_id: ID del dataset.
Returns:
Dict con: dataset_id, project, full_id, location, description, labels,
created, modified, default_table_expiration_ms.
Raises:
google.api_core.exceptions.NotFound: Si el dataset no existe (404).
Example:
>>> ds = bq_get_dataset(client, "analytics")
>>> print(ds["location"], ds["description"])
"""
ref = f"{client.project_id}.{dataset_id}"
ds = client._client.get_dataset(ref)
return _dataset_to_dict(ds)
def bq_list_datasets(client: BQClient) -> list[dict]:
"""Lista todos los datasets del proyecto.
Args:
client: Cliente autenticado.
Returns:
Lista de dicts con: dataset_id, project, full_id.
Example:
>>> datasets = bq_list_datasets(client)
>>> for ds in datasets:
... print(ds["dataset_id"], ds["full_id"])
"""
return [
{
"dataset_id": ds.dataset_id,
"project": ds.project,
"full_id": f"{ds.project}.{ds.dataset_id}",
}
for ds in client._client.list_datasets()
]
def bq_update_dataset(
client: BQClient,
dataset_id: str,
description: str | None = None,
labels: dict[str, str] | None = None,
default_table_expiration_ms: int | None = None,
) -> dict:
"""Actualiza campos de un dataset.
Solo se modifican los campos pasados (no-None).
Args:
client: Cliente autenticado.
dataset_id: ID del dataset.
description: Nueva descripcion (None = no cambiar).
labels: Nuevos labels (None = no cambiar).
default_table_expiration_ms: Nueva expiracion de tablas en ms (None = no cambiar).
Returns:
Dict con el dataset actualizado.
Raises:
google.api_core.exceptions.NotFound: Si el dataset no existe.
Example:
>>> bq_update_dataset(client, "analytics", description="Updated description")
"""
ref = f"{client.project_id}.{dataset_id}"
ds = client._client.get_dataset(ref)
fields = []
if description is not None:
ds.description = description
fields.append("description")
if labels is not None:
ds.labels = labels
fields.append("labels")
if default_table_expiration_ms is not None:
ds.default_table_expiration_ms = default_table_expiration_ms
fields.append("default_table_expiration_ms")
if not fields:
return _dataset_to_dict(ds)
updated = client._client.update_dataset(ds, fields)
return _dataset_to_dict(updated)
def bq_delete_dataset(
client: BQClient,
dataset_id: str,
delete_contents: bool = False,
) -> None:
"""Elimina un dataset.
Args:
client: Cliente autenticado.
dataset_id: ID del dataset.
delete_contents: Si True, elimina todas las tablas del dataset.
Si False y el dataset tiene tablas, falla.
Raises:
google.api_core.exceptions.NotFound: Si el dataset no existe.
google.api_core.exceptions.BadRequest: Si tiene tablas y delete_contents=False.
Example:
>>> bq_delete_dataset(client, "temp_analytics", delete_contents=True)
"""
ref = f"{client.project_id}.{dataset_id}"
client._client.delete_dataset(ref, delete_contents=delete_contents)
def _dataset_to_dict(ds) -> dict:
"""Convierte un objeto Dataset del SDK a dict plano."""
return {
"dataset_id": ds.dataset_id,
"project": ds.project,
"full_id": f"{ds.project}.{ds.dataset_id}",
"location": ds.location,
"description": ds.description or "",
"labels": dict(ds.labels) if ds.labels else {},
"created": ds.created.isoformat() if ds.created else None,
"modified": ds.modified.isoformat() if ds.modified else None,
"default_table_expiration_ms": ds.default_table_expiration_ms,
}
+105
View File
@@ -0,0 +1,105 @@
"""Gestion de jobs en Google BigQuery."""
from .client import BQClient
def bq_list_jobs(
client: BQClient,
state_filter: str = "",
max_results: int = 50,
all_users: bool = False,
) -> list[dict]:
"""Lista jobs del proyecto con filtro opcional por estado.
Args:
client: Cliente autenticado.
state_filter: Filtro: "running", "pending", "done". Vacio = todos.
max_results: Numero maximo de jobs a retornar.
all_users: Si True, lista jobs de todos los usuarios (requiere permisos).
Returns:
Lista de dicts con: job_id, job_type, state, created, started, ended,
user_email, bytes_processed, error.
Example:
>>> jobs = bq_list_jobs(client, state_filter="running")
>>> for j in jobs:
... print(j["job_id"], j["state"], j["job_type"])
"""
kwargs = {"max_results": max_results, "all_users": all_users}
if state_filter:
kwargs["state_filter"] = state_filter
return [_job_to_dict(job) for job in client._client.list_jobs(**kwargs)]
def bq_get_job(client: BQClient, job_id: str) -> dict:
"""Obtiene detalles de un job por su ID.
Args:
client: Cliente autenticado.
job_id: ID del job.
Returns:
Dict con: job_id, job_type, state, created, started, ended,
user_email, bytes_processed, destination_table, query, error, errors.
Raises:
google.api_core.exceptions.NotFound: Si el job no existe.
Example:
>>> job = bq_get_job(client, "job_abc123")
>>> print(job["state"], job["bytes_processed"])
>>> if job["error"]:
... print("Error:", job["error"])
"""
job = client._client.get_job(job_id)
result = _job_to_dict(job)
# Agregar detalles extra disponibles en get pero no en list
if hasattr(job, 'destination') and job.destination:
result["destination_table"] = str(job.destination)
if hasattr(job, 'query') and job.query:
result["query"] = job.query
if job.errors:
result["errors"] = [dict(e) for e in job.errors]
return result
def bq_cancel_job(client: BQClient, job_id: str) -> dict:
"""Cancela un job en ejecucion.
Args:
client: Cliente autenticado.
job_id: ID del job a cancelar.
Returns:
Dict con: job_id, state (tras cancelacion).
Raises:
google.api_core.exceptions.NotFound: Si el job no existe.
Example:
>>> result = bq_cancel_job(client, "job_abc123")
>>> print(result["state"]) # "DONE" (cancelled)
"""
job = client._client.cancel_job(job_id)
return {"job_id": job.job_id, "state": job.state}
def _job_to_dict(job) -> dict:
"""Convierte un objeto Job del SDK a dict plano."""
result = {
"job_id": job.job_id,
"job_type": job.job_type,
"state": job.state,
"created": job.created.isoformat() if job.created else None,
"started": job.started.isoformat() if job.started else None,
"ended": job.ended.isoformat() if job.ended else None,
"user_email": job.user_email,
"error": str(job.error_result) if job.error_result else None,
}
# bytes_processed solo disponible en query jobs
try:
result["bytes_processed"] = job.total_bytes_processed
except AttributeError:
result["bytes_processed"] = None
return result
+397
View File
@@ -0,0 +1,397 @@
"""Queries y operaciones de datos para Google BigQuery."""
from .client import BQClient
from google.cloud import bigquery
def bq_query(
client: BQClient,
sql: str,
params: list[dict] | None = None,
dry_run: bool = False,
) -> dict:
"""Ejecuta una query SQL en BigQuery con soporte para parametros y dry-run.
Si dry_run=True, estima el costo sin ejecutar la query. Si params no es
None, los convierte a ScalarQueryParameter usando la sintaxis @nombre en SQL.
Args:
client: Cliente autenticado de BigQuery.
sql: Query SQL a ejecutar. Usar @nombre para referencias a parametros.
params: Lista de parametros. Cada elemento: {"name": "x", "type": "STRING", "value": "hello"}.
Tipos soportados: STRING, INT64, FLOAT64, BOOL, DATE, TIMESTAMP.
dry_run: Si True, retorna estimacion de bytes sin ejecutar.
Returns:
Si dry_run=True: {"total_bytes_processed": N, "total_bytes_billed": N}.
Si dry_run=False: {"columns": [...], "rows": [[...], ...],
"total_rows": N, "bytes_processed": N, "cache_hit": bool}.
Raises:
google.api_core.exceptions.GoogleAPICallError: Si la query falla.
ValueError: Si un tipo de parametro no es soportado.
Example:
>>> result = bq_query(client, "SELECT COUNT(*) as n FROM `project.dataset.table`")
>>> print(result["rows"])
>>> # Con parametros:
>>> result = bq_query(client, "SELECT * FROM t WHERE status = @s",
... params=[{"name": "s", "type": "STRING", "value": "active"}])
"""
job_config = bigquery.QueryJobConfig()
if params:
query_params = []
for p in params:
query_params.append(
bigquery.ScalarQueryParameter(p["name"], p["type"], p["value"])
)
job_config.query_parameters = query_params
if dry_run:
job_config.dry_run = True
job_config.use_query_cache = False
job = client._client.query(sql, job_config=job_config)
return {
"total_bytes_processed": job.total_bytes_processed,
"total_bytes_billed": job.total_bytes_billed,
}
job = client._client.query(sql, job_config=job_config)
result = job.result()
columns = [field.name for field in result.schema]
rows = [list(row.values()) for row in result]
return {
"columns": columns,
"rows": rows,
"total_rows": result.total_rows,
"bytes_processed": job.total_bytes_processed,
"cache_hit": job.cache_hit,
}
def bq_insert_rows(
client: BQClient,
dataset_id: str,
table_id: str,
rows: list[dict],
) -> dict:
"""Inserta filas en una tabla BigQuery usando el streaming insert.
Usa la API de streaming (insert_rows_json) para insercion en tiempo real.
Los errores de filas individuales se retornan en el campo "errors" sin
lanzar excepcion — revisar siempre ese campo.
Args:
client: Cliente autenticado de BigQuery.
dataset_id: ID del dataset de destino.
table_id: ID de la tabla de destino.
rows: Lista de dicts con los datos a insertar. Cada dict debe tener
claves que coincidan con las columnas de la tabla.
Returns:
{"inserted": N, "errors": [...]} donde errors es la lista de errores
de streaming insert retornada por la API (vacia si todo OK).
Raises:
google.api_core.exceptions.NotFound: Si la tabla no existe.
google.api_core.exceptions.GoogleAPICallError: Si la API falla.
Example:
>>> result = bq_insert_rows(client, "my_dataset", "my_table", [
... {"id": 1, "name": "Alice", "created_at": "2024-01-01"},
... {"id": 2, "name": "Bob", "created_at": "2024-01-02"},
... ])
>>> if result["errors"]:
... print("Errores:", result["errors"])
"""
table_ref = client._client.dataset(dataset_id).table(table_id)
errors = client._client.insert_rows_json(table_ref, rows)
return {
"inserted": len(rows) - len(errors),
"errors": errors,
}
def bq_load_from_gcs(
client: BQClient,
uri: str | list[str],
dataset_id: str,
table_id: str,
source_format: str = "CSV",
write_disposition: str = "WRITE_APPEND",
autodetect: bool = True,
skip_leading_rows: int = 0,
) -> dict:
"""Carga datos desde Google Cloud Storage a una tabla BigQuery.
Configura y ejecuta un LoadJob desde uno o varios URIs de GCS. Espera
la finalizacion del job con job.result().
Args:
client: Cliente autenticado de BigQuery.
uri: URI de GCS. Puede ser un string ("gs://bucket/file.csv") o una
lista de strings para multiples archivos.
dataset_id: ID del dataset de destino.
table_id: ID de la tabla de destino.
source_format: Formato del archivo: "CSV", "NEWLINE_DELIMITED_JSON",
"AVRO", "PARQUET", "ORC". Default: "CSV".
write_disposition: Comportamiento si la tabla existe: "WRITE_APPEND",
"WRITE_TRUNCATE", "WRITE_EMPTY". Default: "WRITE_APPEND".
autodetect: Si True, detecta el schema automaticamente. Default: True.
skip_leading_rows: Numero de filas a saltar al inicio (cabeceras CSV).
Returns:
{"job_id": str, "rows_loaded": N, "status": "DONE"|"FAILED"}.
Raises:
google.api_core.exceptions.GoogleAPICallError: Si el job falla.
google.cloud.exceptions.NotFound: Si el bucket o dataset no existe.
Example:
>>> result = bq_load_from_gcs(
... client, "gs://my-bucket/data/*.csv",
... "my_dataset", "my_table",
... skip_leading_rows=1,
... )
>>> print(f"Cargadas {result['rows_loaded']} filas, job: {result['job_id']}")
"""
format_map = {
"CSV": bigquery.SourceFormat.CSV,
"NEWLINE_DELIMITED_JSON": bigquery.SourceFormat.NEWLINE_DELIMITED_JSON,
"AVRO": bigquery.SourceFormat.AVRO,
"PARQUET": bigquery.SourceFormat.PARQUET,
"ORC": bigquery.SourceFormat.ORC,
}
disposition_map = {
"WRITE_APPEND": bigquery.WriteDisposition.WRITE_APPEND,
"WRITE_TRUNCATE": bigquery.WriteDisposition.WRITE_TRUNCATE,
"WRITE_EMPTY": bigquery.WriteDisposition.WRITE_EMPTY,
}
job_config = bigquery.LoadJobConfig(
source_format=format_map.get(source_format, bigquery.SourceFormat.CSV),
write_disposition=disposition_map.get(source_format, bigquery.WriteDisposition.WRITE_APPEND),
autodetect=autodetect,
skip_leading_rows=skip_leading_rows,
)
job_config.write_disposition = disposition_map.get(write_disposition, bigquery.WriteDisposition.WRITE_APPEND)
table_ref = client._client.dataset(dataset_id).table(table_id)
uris = uri if isinstance(uri, list) else [uri]
job = client._client.load_table_from_uri(uris, table_ref, job_config=job_config)
result = job.result()
return {
"job_id": job.job_id,
"rows_loaded": result.output_rows,
"status": "DONE" if job.state == "DONE" and not job.errors else "FAILED",
}
def bq_load_from_file(
client: BQClient,
file_path: str,
dataset_id: str,
table_id: str,
source_format: str = "CSV",
write_disposition: str = "WRITE_APPEND",
autodetect: bool = True,
skip_leading_rows: int = 0,
) -> dict:
"""Carga datos desde un archivo local a una tabla BigQuery.
Abre el archivo y usa load_table_from_file del SDK. Equivalente a
bq_load_from_gcs pero para archivos en disco local.
Args:
client: Cliente autenticado de BigQuery.
file_path: Ruta absoluta o relativa al archivo local a cargar.
dataset_id: ID del dataset de destino.
table_id: ID de la tabla de destino.
source_format: Formato del archivo: "CSV", "NEWLINE_DELIMITED_JSON",
"AVRO", "PARQUET", "ORC". Default: "CSV".
write_disposition: Comportamiento si la tabla existe: "WRITE_APPEND",
"WRITE_TRUNCATE", "WRITE_EMPTY". Default: "WRITE_APPEND".
autodetect: Si True, detecta el schema automaticamente. Default: True.
skip_leading_rows: Numero de filas a saltar al inicio (cabeceras CSV).
Returns:
{"job_id": str, "rows_loaded": N, "status": "DONE"|"FAILED"}.
Raises:
FileNotFoundError: Si el archivo local no existe.
google.api_core.exceptions.GoogleAPICallError: Si el job falla.
Example:
>>> result = bq_load_from_file(
... client, "/tmp/data.csv",
... "my_dataset", "my_table",
... skip_leading_rows=1,
... )
>>> print(f"Cargadas {result['rows_loaded']} filas")
"""
format_map = {
"CSV": bigquery.SourceFormat.CSV,
"NEWLINE_DELIMITED_JSON": bigquery.SourceFormat.NEWLINE_DELIMITED_JSON,
"AVRO": bigquery.SourceFormat.AVRO,
"PARQUET": bigquery.SourceFormat.PARQUET,
"ORC": bigquery.SourceFormat.ORC,
}
disposition_map = {
"WRITE_APPEND": bigquery.WriteDisposition.WRITE_APPEND,
"WRITE_TRUNCATE": bigquery.WriteDisposition.WRITE_TRUNCATE,
"WRITE_EMPTY": bigquery.WriteDisposition.WRITE_EMPTY,
}
job_config = bigquery.LoadJobConfig(
source_format=format_map.get(source_format, bigquery.SourceFormat.CSV),
write_disposition=disposition_map.get(write_disposition, bigquery.WriteDisposition.WRITE_APPEND),
autodetect=autodetect,
skip_leading_rows=skip_leading_rows,
)
table_ref = client._client.dataset(dataset_id).table(table_id)
with open(file_path, "rb") as f:
job = client._client.load_table_from_file(f, table_ref, job_config=job_config)
result = job.result()
return {
"job_id": job.job_id,
"rows_loaded": result.output_rows,
"status": "DONE" if job.state == "DONE" and not job.errors else "FAILED",
}
def bq_export_to_gcs(
client: BQClient,
dataset_id: str,
table_id: str,
destination_uri: str,
destination_format: str = "CSV",
compression: str = "NONE",
) -> dict:
"""Exporta una tabla BigQuery a Google Cloud Storage.
Usa extract_table del SDK para crear un ExtractJob. Espera la finalizacion
del job con job.result().
Args:
client: Cliente autenticado de BigQuery.
dataset_id: ID del dataset de origen.
table_id: ID de la tabla de origen.
destination_uri: URI de destino en GCS. Para multiples archivos usar
wildcard: "gs://bucket/prefix_*.csv".
destination_format: Formato de exportacion: "CSV", "NEWLINE_DELIMITED_JSON",
"AVRO", "PARQUET". Default: "CSV".
compression: Compresion: "NONE", "GZIP", "SNAPPY", "DEFLATE". Default: "NONE".
Returns:
{"job_id": str, "destination_uri": str, "status": "DONE"|"FAILED"}.
Raises:
google.api_core.exceptions.NotFound: Si la tabla no existe.
google.api_core.exceptions.GoogleAPICallError: Si el job falla.
Example:
>>> result = bq_export_to_gcs(
... client, "my_dataset", "my_table",
... "gs://my-bucket/export/data_*.csv",
... compression="GZIP",
... )
>>> print(f"Exportado a {result['destination_uri']}, job: {result['job_id']}")
"""
format_map = {
"CSV": bigquery.DestinationFormat.CSV,
"NEWLINE_DELIMITED_JSON": bigquery.DestinationFormat.NEWLINE_DELIMITED_JSON,
"AVRO": bigquery.DestinationFormat.AVRO,
"PARQUET": bigquery.DestinationFormat.PARQUET,
}
compression_map = {
"NONE": bigquery.Compression.NONE,
"GZIP": bigquery.Compression.GZIP,
"SNAPPY": bigquery.Compression.SNAPPY,
"DEFLATE": bigquery.Compression.DEFLATE,
}
job_config = bigquery.ExtractJobConfig(
destination_format=format_map.get(destination_format, bigquery.DestinationFormat.CSV),
compression=compression_map.get(compression, bigquery.Compression.NONE),
)
table_ref = client._client.dataset(dataset_id).table(table_id)
job = client._client.extract_table(table_ref, destination_uri, job_config=job_config)
job.result()
return {
"job_id": job.job_id,
"destination_uri": destination_uri,
"status": "DONE" if job.state == "DONE" and not job.errors else "FAILED",
}
def bq_copy_table(
client: BQClient,
source_dataset: str,
source_table: str,
dest_dataset: str,
dest_table: str,
write_disposition: str = "WRITE_EMPTY",
) -> dict:
"""Copia una tabla BigQuery a otro dataset o tabla dentro del mismo proyecto.
Usa copy_table del SDK para crear un CopyJob. Espera la finalizacion
del job con job.result(). No copia datos entre proyectos distintos.
Args:
client: Cliente autenticado de BigQuery.
source_dataset: ID del dataset de origen.
source_table: ID de la tabla de origen.
dest_dataset: ID del dataset de destino.
dest_table: ID de la tabla de destino.
write_disposition: Comportamiento si la tabla destino existe: "WRITE_EMPTY"
(falla si existe), "WRITE_APPEND", "WRITE_TRUNCATE".
Default: "WRITE_EMPTY".
Returns:
{"job_id": str, "status": "DONE"|"FAILED"}.
Raises:
google.api_core.exceptions.NotFound: Si la tabla de origen no existe.
google.api_core.exceptions.Conflict: Si destino existe y write_disposition es WRITE_EMPTY.
google.api_core.exceptions.GoogleAPICallError: Si el job falla.
Example:
>>> result = bq_copy_table(
... client,
... "production", "users",
... "staging", "users_backup",
... write_disposition="WRITE_TRUNCATE",
... )
>>> print(f"Copia completada: {result['status']}, job: {result['job_id']}")
"""
disposition_map = {
"WRITE_APPEND": bigquery.WriteDisposition.WRITE_APPEND,
"WRITE_TRUNCATE": bigquery.WriteDisposition.WRITE_TRUNCATE,
"WRITE_EMPTY": bigquery.WriteDisposition.WRITE_EMPTY,
}
job_config = bigquery.CopyJobConfig(
write_disposition=disposition_map.get(write_disposition, bigquery.WriteDisposition.WRITE_EMPTY),
)
source_ref = client._client.dataset(source_dataset).table(source_table)
dest_ref = client._client.dataset(dest_dataset).table(dest_table)
job = client._client.copy_table(source_ref, dest_ref, job_config=job_config)
job.result()
return {
"job_id": job.job_id,
"status": "DONE" if job.state == "DONE" and not job.errors else "FAILED",
}
+134
View File
@@ -0,0 +1,134 @@
"""Gestion de routines (UDFs y stored procedures) en Google BigQuery."""
from .client import BQClient
from google.cloud import bigquery
def bq_create_routine(
client: BQClient,
dataset_id: str,
routine_id: str,
body: str,
routine_type: str = "SCALAR_FUNCTION",
language: str = "SQL",
arguments: list[dict] | None = None,
return_type: str = "",
description: str = "",
) -> dict:
"""Crea una routine (UDF o stored procedure) en BigQuery.
Args:
client: Cliente autenticado.
dataset_id: ID del dataset donde crear la routine.
routine_id: ID/nombre de la routine.
body: Cuerpo de la routine (SQL, JavaScript o Python).
routine_type: Tipo: "SCALAR_FUNCTION", "TABLE_VALUED_FUNCTION", "PROCEDURE".
language: Lenguaje: "SQL", "JAVASCRIPT", "PYTHON".
arguments: Lista de argumentos, cada uno: {"name": "x", "data_type": "STRING"}.
return_type: Tipo de retorno para funciones (ej: "STRING", "INT64"). No aplica a procedures.
description: Descripcion opcional.
Returns:
Dict con: routine_id, dataset_id, project, routine_type, language, body, created, modified.
Raises:
google.api_core.exceptions.Conflict: Si la routine ya existe.
Example:
>>> bq_create_routine(client, "analytics", "double_value",
... body="x * 2",
... arguments=[{"name": "x", "data_type": "INT64"}],
... return_type="INT64")
"""
ref = bigquery.RoutineReference.from_string(
f"{client.project_id}.{dataset_id}.{routine_id}"
)
routine = bigquery.Routine(ref)
routine.type_ = routine_type
routine.language = language
routine.body = body
if description:
routine.description = description
if arguments:
routine.arguments = [
bigquery.RoutineArgument(
name=arg["name"],
data_type=bigquery.StandardSqlDataType(
type_kind=getattr(
bigquery.StandardSqlTypeNames, arg["data_type"]
)
),
)
for arg in arguments
]
if return_type:
routine.return_type = bigquery.StandardSqlDataType(
type_kind=getattr(bigquery.StandardSqlTypeNames, return_type)
)
created = client._client.create_routine(routine)
return _routine_to_dict(created)
def bq_list_routines(client: BQClient, dataset_id: str) -> list[dict]:
"""Lista routines de un dataset.
Args:
client: Cliente autenticado.
dataset_id: ID del dataset.
Returns:
Lista de dicts con: routine_id, dataset_id, project, routine_type, language.
Example:
>>> routines = bq_list_routines(client, "analytics")
>>> for r in routines:
... print(r["routine_id"], r["routine_type"], r["language"])
"""
ref = f"{client.project_id}.{dataset_id}"
return [
{
"routine_id": r.routine_id,
"dataset_id": dataset_id,
"project": client.project_id,
"routine_type": r.type_,
"language": r.language,
}
for r in client._client.list_routines(ref)
]
def bq_delete_routine(
client: BQClient,
dataset_id: str,
routine_id: str,
) -> None:
"""Elimina una routine.
Args:
client: Cliente autenticado.
dataset_id: ID del dataset.
routine_id: ID de la routine a eliminar.
Raises:
google.api_core.exceptions.NotFound: Si la routine no existe.
Example:
>>> bq_delete_routine(client, "analytics", "double_value")
"""
ref = f"{client.project_id}.{dataset_id}.{routine_id}"
client._client.delete_routine(ref)
def _routine_to_dict(routine) -> dict:
"""Convierte un objeto Routine del SDK a dict plano."""
return {
"routine_id": routine.routine_id,
"dataset_id": routine.dataset_id,
"project": routine.project,
"routine_type": routine.type_,
"language": routine.language,
"body": routine.body,
"description": routine.description or "",
"created": routine.created.isoformat() if routine.created else None,
"modified": routine.modified.isoformat() if routine.modified else None,
}
+345
View File
@@ -0,0 +1,345 @@
"""CRUD de tablas en Google BigQuery."""
from .client import BQClient
from google.cloud import bigquery
# ---------------------------------------------------------------------------
# Helpers de serializacion
# ---------------------------------------------------------------------------
def _schema_field_to_dict(field: bigquery.SchemaField) -> dict:
"""Convierte un SchemaField a dict serializable."""
result = {
"name": field.name,
"type": field.field_type,
"mode": field.mode,
"description": field.description or "",
}
if field.fields:
result["fields"] = [_schema_field_to_dict(f) for f in field.fields]
return result
def _schema_to_dicts(schema: list) -> list[dict]:
"""Convierte una lista de SchemaField a lista de dicts."""
return [_schema_field_to_dict(f) for f in schema]
def _dict_to_schema_field(d: dict) -> bigquery.SchemaField:
"""Convierte un dict a SchemaField."""
nested = [_dict_to_schema_field(f) for f in d.get("fields", [])]
return bigquery.SchemaField(
name=d["name"],
field_type=d.get("type", "STRING"),
mode=d.get("mode", "NULLABLE"),
description=d.get("description", ""),
fields=nested,
)
def _table_to_dict(table: bigquery.Table) -> dict:
"""Convierte un objeto Table del SDK a dict plano serializable."""
partitioning = None
if table.time_partitioning:
partitioning = {
"type": table.time_partitioning.type_,
"field": table.time_partitioning.field or "",
}
elif table.range_partitioning:
partitioning = {
"type": "RANGE",
"field": table.range_partitioning.field,
}
clustering = None
if table.clustering_fields:
clustering = list(table.clustering_fields)
return {
"table_id": table.table_id,
"dataset_id": table.dataset_id,
"project": table.project,
"full_id": table.full_table_id or f"{table.project}.{table.dataset_id}.{table.table_id}",
"schema": _schema_to_dicts(table.schema or []),
"num_rows": table.num_rows,
"num_bytes": table.num_bytes,
"created": table.created.isoformat() if table.created else None,
"modified": table.modified.isoformat() if table.modified else None,
"type": table.table_type or "TABLE",
"partitioning": partitioning,
"clustering": clustering,
"description": table.description or "",
"labels": dict(table.labels or {}),
}
# ---------------------------------------------------------------------------
# Funciones CRUD
# ---------------------------------------------------------------------------
def bq_create_table(
client: BQClient,
dataset_id: str,
table_id: str,
schema: list[dict],
partitioning: dict | None = None,
clustering: list[str] | None = None,
description: str = "",
labels: dict | None = None,
) -> dict:
"""Crea una tabla en BigQuery con schema, particionamiento y clustering opcionales.
Usa `client._client.create_table()` del SDK oficial.
Args:
client: Cliente autenticado BQClient.
dataset_id: ID del dataset donde crear la tabla.
table_id: ID (nombre) de la tabla a crear.
schema: Lista de dicts con definicion de columnas. Cada dict:
{"name": "col", "type": "STRING", "mode": "NULLABLE", "description": "..."}
Tipos validos: STRING, INTEGER, FLOAT, BOOLEAN, BYTES, DATE, DATETIME,
TIME, TIMESTAMP, RECORD, NUMERIC, BIGNUMERIC, JSON, GEOGRAPHY.
Modos: NULLABLE, REQUIRED, REPEATED.
partitioning: Dict de configuracion de particion o None. Ejemplo:
{"type": "DAY", "field": "created_at"}
Tipos: DAY, MONTH, YEAR, HOUR. Field vacio usa pseudo-columna _PARTITIONTIME.
clustering: Lista de columnas para clustering (max 4) o None.
Ejemplo: ["country", "status"]
description: Descripcion de la tabla.
labels: Etiquetas clave-valor para la tabla. Ejemplo: {"env": "prod"}.
Returns:
Dict con metadata de la tabla creada: table_id, dataset_id, project,
full_id, schema, num_rows, num_bytes, created, modified, type,
partitioning, clustering, description, labels.
Raises:
google.api_core.exceptions.Conflict: Si la tabla ya existe.
google.api_core.exceptions.NotFound: Si el dataset no existe.
google.api_core.exceptions.BadRequest: Si el schema es invalido.
Example:
>>> table = bq_create_table(client, "mi_dataset", "ventas", [
... {"name": "id", "type": "INTEGER", "mode": "REQUIRED"},
... {"name": "fecha", "type": "DATE", "mode": "NULLABLE"},
... {"name": "monto", "type": "FLOAT", "mode": "NULLABLE"},
... ], partitioning={"type": "MONTH", "field": "fecha"},
... clustering=["id"])
>>> print(table["full_id"])
"""
table_ref = f"{client.project_id}.{dataset_id}.{table_id}"
bq_schema = [_dict_to_schema_field(f) for f in schema]
table = bigquery.Table(table_ref, schema=bq_schema)
if partitioning:
table.time_partitioning = bigquery.TimePartitioning(
type_=partitioning.get("type", "DAY"),
field=partitioning.get("field") or None,
)
if clustering:
table.clustering_fields = clustering
if description:
table.description = description
if labels:
table.labels = labels
created = client._client.create_table(table)
return _table_to_dict(created)
def bq_get_table(client: BQClient, dataset_id: str, table_id: str) -> dict:
"""Obtiene los metadatos completos de una tabla BigQuery.
Usa `client._client.get_table()` del SDK oficial.
Args:
client: Cliente autenticado BQClient.
dataset_id: ID del dataset que contiene la tabla.
table_id: ID (nombre) de la tabla.
Returns:
Dict con: table_id, dataset_id, project, full_id, schema (lista de dicts),
num_rows, num_bytes, created (ISO 8601), modified (ISO 8601), type
(TABLE, VIEW, MATERIALIZED_VIEW, EXTERNAL), partitioning (dict o None),
clustering (lista de strings o None), description, labels.
Raises:
google.api_core.exceptions.NotFound: Si la tabla no existe.
Example:
>>> tabla = bq_get_table(client, "mi_dataset", "ventas")
>>> print(tabla["num_rows"], tabla["schema"])
"""
table_ref = f"{client.project_id}.{dataset_id}.{table_id}"
table = client._client.get_table(table_ref)
return _table_to_dict(table)
def bq_list_tables(client: BQClient, dataset_id: str) -> list[dict]:
"""Lista todas las tablas de un dataset BigQuery.
Usa `client._client.list_tables()` del SDK oficial.
Args:
client: Cliente autenticado BQClient.
dataset_id: ID del dataset a listar.
Returns:
Lista de dicts resumidos, uno por tabla. Cada dict contiene:
table_id, full_id, type (TABLE, VIEW, MATERIALIZED_VIEW, EXTERNAL).
Raises:
google.api_core.exceptions.NotFound: Si el dataset no existe.
Example:
>>> tablas = bq_list_tables(client, "mi_dataset")
>>> for t in tablas:
... print(t["table_id"], t["type"])
"""
dataset_ref = f"{client.project_id}.{dataset_id}"
tables = client._client.list_tables(dataset_ref)
return [
{
"table_id": t.table_id,
"full_id": f"{t.project}.{t.dataset_id}.{t.table_id}",
"type": t.table_type or "TABLE",
}
for t in tables
]
def bq_update_table(
client: BQClient,
dataset_id: str,
table_id: str,
schema: list[dict] | None = None,
description: str | None = None,
labels: dict | None = None,
) -> dict:
"""Actualiza metadatos de una tabla BigQuery.
Usa `client._client.update_table()` del SDK oficial. Solo modifica los
campos no-None. Para schema, BigQuery SOLO permite agregar columnas nuevas
al final — no se pueden eliminar ni modificar columnas existentes.
Args:
client: Cliente autenticado BQClient.
dataset_id: ID del dataset que contiene la tabla.
table_id: ID (nombre) de la tabla a actualizar.
schema: Lista de dicts con el schema completo nuevo (incluye columnas
existentes + nuevas). Solo se permiten adiciones. None = sin cambios.
description: Nueva descripcion de la tabla. None = sin cambios.
labels: Nuevas etiquetas clave-valor. None = sin cambios.
Returns:
Dict con la metadata actualizada de la tabla (misma estructura que bq_get_table).
Raises:
google.api_core.exceptions.NotFound: Si la tabla no existe.
google.api_core.exceptions.BadRequest: Si se intenta eliminar columnas.
Example:
>>> tabla = bq_update_table(client, "mi_dataset", "ventas",
... description="Tabla de ventas actualizada",
... labels={"env": "prod", "team": "data"})
>>> tabla = bq_update_table(client, "mi_dataset", "ventas",
... schema=[
... {"name": "id", "type": "INTEGER", "mode": "REQUIRED"},
... {"name": "nueva_col", "type": "STRING", "mode": "NULLABLE"},
... ])
"""
table_ref = f"{client.project_id}.{dataset_id}.{table_id}"
table = client._client.get_table(table_ref)
fields_to_update = []
if schema is not None:
table.schema = [_dict_to_schema_field(f) for f in schema]
fields_to_update.append("schema")
if description is not None:
table.description = description
fields_to_update.append("description")
if labels is not None:
table.labels = labels
fields_to_update.append("labels")
if not fields_to_update:
return _table_to_dict(table)
updated = client._client.update_table(table, fields_to_update)
return _table_to_dict(updated)
def bq_delete_table(client: BQClient, dataset_id: str, table_id: str) -> None:
"""Elimina permanentemente una tabla de BigQuery.
Usa `client._client.delete_table()` del SDK oficial. IRREVERSIBLE — no hay
papelera de reciclaje. Considerar exportar datos antes de eliminar.
Args:
client: Cliente autenticado BQClient.
dataset_id: ID del dataset que contiene la tabla.
table_id: ID (nombre) de la tabla a eliminar.
Raises:
google.api_core.exceptions.NotFound: Si la tabla no existe.
Example:
>>> bq_delete_table(client, "mi_dataset", "tabla_temporal")
"""
table_ref = f"{client.project_id}.{dataset_id}.{table_id}"
client._client.delete_table(table_ref)
def bq_preview_rows(
client: BQClient,
dataset_id: str,
table_id: str,
max_results: int = 10,
) -> dict:
"""Obtiene una muestra de filas de una tabla BigQuery sin ejecutar query.
Usa `client._client.list_rows()` del SDK oficial — no genera costes de
procesamiento de bytes (no es una query SQL). Ideal para vista previa rapida.
Args:
client: Cliente autenticado BQClient.
dataset_id: ID del dataset que contiene la tabla.
table_id: ID (nombre) de la tabla a previsualizar.
max_results: Numero maximo de filas a retornar. Default: 10.
Returns:
Dict con:
- columns: lista de strings con nombres de columnas
- rows: lista de listas con valores de cada fila
- total_rows: numero total de filas en la tabla (no en el preview)
Raises:
google.api_core.exceptions.NotFound: Si la tabla no existe.
Example:
>>> preview = bq_preview_rows(client, "mi_dataset", "ventas", max_results=5)
>>> print(preview["columns"])
>>> for row in preview["rows"]:
... print(row)
>>> print(f"Total en tabla: {preview['total_rows']}")
"""
table_ref = f"{client.project_id}.{dataset_id}.{table_id}"
table = client._client.get_table(table_ref)
rows_iter = client._client.list_rows(table, max_results=max_results)
columns = [f.name for f in rows_iter.schema]
rows = [list(row.values()) for row in rows_iter]
return {
"columns": columns,
"rows": rows,
"total_rows": table.num_rows,
}
+2
View File
@@ -6,6 +6,8 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"cryptography>=46.0.6",
"google-cloud-bigquery>=3.25",
"google-cloud-bigquery-storage>=2.27",
"httpx",
"openpyxl>=3.1.5",
"python-docx>=1.2.0",
+31
View File
@@ -0,0 +1,31 @@
---
name: BQClient
lang: py
domain: infra
version: "1.0.0"
algebraic: product
definition: |
@dataclass
class BQClient:
project_id: str
_client: bigquery.Client
description: "Cliente para Google BigQuery. Contiene el project_id y el cliente oficial del SDK. Se obtiene con bq_auth()."
tags: [bigquery, gcp, client, google-cloud]
uses_types: []
file_path: "python/types/infra/bq_client.py"
---
## Ejemplo
```python
from bigquery import bq_auth, BQClient
client: BQClient = bq_auth("my-project")
print(client.project_id) # "my-project"
client.close()
```
## Notas
Wrapper sobre `google.cloud.bigquery.Client`. El campo `_client` es interno y no serializable.
Se usa como context manager: `with bq_auth() as client: ...`
+17
View File
@@ -0,0 +1,17 @@
"""Tipo BQClient para Google BigQuery."""
from dataclasses import dataclass, field
# Nota: la implementacion real esta en python/functions/bigquery/client.py
# Este archivo existe solo como referencia del tipo para el registry.
@dataclass
class BQClient:
"""Cliente para Google BigQuery.
Attributes:
project_id: ID del proyecto GCP.
_client: Cliente oficial de BigQuery (interno, no serializable).
"""
project_id: str
_client: object = field(repr=False, default=None)
+359
View File
@@ -1,6 +1,11 @@
version = 1
revision = 3
requires-python = ">=3.12"
resolution-markers = [
"python_full_version >= '3.14'",
"python_full_version == '3.13.*'",
"python_full_version < '3.13'",
]
[[package]]
name = "anyio"
@@ -81,6 +86,79 @@ wheels = [
{ 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 = "charset-normalizer"
version = "3.4.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
@@ -158,6 +236,8 @@ version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "cryptography" },
{ name = "google-cloud-bigquery" },
{ name = "google-cloud-bigquery-storage" },
{ name = "httpx" },
{ name = "openpyxl" },
{ name = "python-docx" },
@@ -172,6 +252,8 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=46.0.6" },
{ name = "google-cloud-bigquery", specifier = ">=3.25" },
{ name = "google-cloud-bigquery-storage", specifier = ">=2.27" },
{ name = "httpx" },
{ name = "openpyxl", specifier = ">=3.1.5" },
{ name = "python-docx", specifier = ">=1.2.0" },
@@ -181,6 +263,190 @@ requires-dist = [
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=9.0.2" }]
[[package]]
name = "google-api-core"
version = "2.30.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-auth" },
{ name = "googleapis-common-protos" },
{ name = "proto-plus" },
{ name = "protobuf" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/2e/83ca41eb400eb228f9279ec14ed66f6475218b59af4c6daec2d5a509fe83/google_api_core-2.30.2.tar.gz", hash = "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", size = 176862, upload-time = "2026-04-02T21:23:44.876Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/e1/ebd5100cbb202e561c0c8b59e485ef3bd63fa9beb610f3fdcaea443f0288/google_api_core-2.30.2-py3-none-any.whl", hash = "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594", size = 173236, upload-time = "2026-04-02T21:23:06.395Z" },
]
[package.optional-dependencies]
grpc = [
{ name = "grpcio" },
{ name = "grpcio-status" },
]
[[package]]
name = "google-auth"
version = "2.49.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyasn1-modules" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" },
]
[[package]]
name = "google-cloud-bigquery"
version = "3.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core", extra = ["grpc"] },
{ name = "google-auth" },
{ name = "google-cloud-core" },
{ name = "google-resumable-media" },
{ name = "packaging" },
{ name = "python-dateutil" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/13/6515c7aab55a4a0cf708ffd309fb9af5bab54c13e32dc22c5acd6497193c/google_cloud_bigquery-3.41.0.tar.gz", hash = "sha256:2217e488b47ed576360c9b2cc07d59d883a54b83167c0ef37f915c26b01a06fe", size = 513434, upload-time = "2026-03-30T22:50:55.347Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/40/33/1d3902efadef9194566d499d61507e1f038454e0b55499d2d7f8ab2a4fee/google_cloud_bigquery-3.41.0-py3-none-any.whl", hash = "sha256:2a5b5a737b401cbd824a6e5eac7554100b878668d908e6548836b5d8aaa4dcaa", size = 262343, upload-time = "2026-03-30T22:48:45.444Z" },
]
[[package]]
name = "google-cloud-bigquery-storage"
version = "2.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core", extra = ["grpc"] },
{ name = "google-auth" },
{ name = "grpcio" },
{ name = "proto-plus" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/13/31/5c6fa9e7b8e266a765ec80d13a2b2852cb0a6d3733572e7dbdc0cb39003c/google_cloud_bigquery_storage-2.37.0.tar.gz", hash = "sha256:f88ee7f1e49db1e639da3d9a8b79835ca4bc47afbb514fb2adfc0ccb41a7fd97", size = 310578, upload-time = "2026-03-30T22:51:13.418Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/74/0e/2950d4d0160300f51c7397a080b1685d3e25b40badb2c96f03d58d0ee868/google_cloud_bigquery_storage-2.37.0-py3-none-any.whl", hash = "sha256:1e319c27ef60fc31030f6e0b52e5e891e1cdd50551effe8c6f673a4c3c56fcb6", size = 306678, upload-time = "2026-03-30T22:47:42.333Z" },
]
[[package]]
name = "google-cloud-core"
version = "2.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-api-core" },
{ name = "google-auth" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/24/6ca08b0a03c7b0c620427503ab00353a4ae806b848b93bcea18b6b76fde6/google_cloud_core-2.5.1.tar.gz", hash = "sha256:3dc94bdec9d05a31d9f355045ed0f369fbc0d8c665076c734f065d729800f811", size = 36078, upload-time = "2026-03-30T22:50:08.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/73/d9/5bb050cb32826466aa9b25f79e2ca2879fe66cb76782d4ed798dd7506151/google_cloud_core-2.5.1-py3-none-any.whl", hash = "sha256:ea62cdf502c20e3e14be8a32c05ed02113d7bef454e40ff3fab6fe1ec9f1f4e7", size = 29452, upload-time = "2026-03-30T22:48:31.567Z" },
]
[[package]]
name = "google-crc32c"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" },
{ url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" },
{ url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" },
{ url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" },
{ url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" },
{ url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" },
{ url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" },
{ url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" },
{ url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" },
{ url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" },
{ url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" },
{ url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" },
{ url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" },
{ url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" },
{ url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" },
]
[[package]]
name = "google-resumable-media"
version = "2.8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "google-crc32c" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3f/d1/b1ea14b93b6b78f57fc580125de44e9f593ab88dd2460f1a8a8d18f74754/google_resumable_media-2.8.2.tar.gz", hash = "sha256:f3354a182ebd193ae3f42e3ef95e6c9b10f128320de23ac7637236713b1acd70", size = 2164510, upload-time = "2026-03-30T23:34:25.369Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/f8/50bfaf4658431ff9de45c5c3935af7ab01157a4903c603cd0eee6e78e087/google_resumable_media-2.8.2-py3-none-any.whl", hash = "sha256:82b6d8ccd11765268cdd2a2123f417ec806b8eef3000a9a38dfe3033da5fb220", size = 81511, upload-time = "2026-03-30T23:34:09.671Z" },
]
[[package]]
name = "googleapis-common-protos"
version = "1.74.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" },
]
[[package]]
name = "grpcio"
version = "1.80.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" },
{ url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" },
{ url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" },
{ url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" },
{ url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" },
{ url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" },
{ url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" },
{ url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" },
{ url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" },
{ url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" },
{ url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" },
{ url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" },
{ url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" },
{ url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" },
{ url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" },
{ url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" },
{ url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" },
{ url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" },
{ url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" },
{ url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" },
{ url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" },
{ url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" },
{ url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" },
{ url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" },
{ url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" },
{ url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" },
{ url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" },
{ url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" },
{ url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" },
{ url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" },
]
[[package]]
name = "grpcio-status"
version = "1.80.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "googleapis-common-protos" },
{ name = "grpcio" },
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/ed/105f619bdd00cb47a49aa2feea6232ea2bbb04199d52a22cc6a7d603b5cb/grpcio_status-1.80.0.tar.gz", hash = "sha256:df73802a4c89a3ea88aa2aff971e886fccce162bc2e6511408b3d67a144381cd", size = 13901, upload-time = "2026-03-30T08:54:34.784Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/80/58cd2dfc19a07d022abe44bde7c365627f6c7cb6f692ada6c65ca437d09a/grpcio_status-1.80.0-py3-none-any.whl", hash = "sha256:4b56990363af50dbf2c2ebb80f1967185c07d87aa25aa2bea45ddb75fc181dbe", size = 14638, upload-time = "2026-03-30T08:54:01.569Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
@@ -346,6 +612,54 @@ 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 = "proto-plus"
version = "1.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "protobuf" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" },
]
[[package]]
name = "protobuf"
version = "6.33.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/66/70/e908e9c5e52ef7c3a6c7902c9dfbb34c7e29c25d2f81ade3856445fd5c94/protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135", size = 444531, upload-time = "2026-03-18T19:05:00.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fc/9f/2f509339e89cfa6f6a4c4ff50438db9ca488dec341f7e454adad60150b00/protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3", size = 425739, upload-time = "2026-03-18T19:04:48.373Z" },
{ url = "https://files.pythonhosted.org/packages/76/5d/683efcd4798e0030c1bab27374fd13a89f7c2515fb1f3123efdfaa5eab57/protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326", size = 437089, upload-time = "2026-03-18T19:04:50.381Z" },
{ url = "https://files.pythonhosted.org/packages/5c/01/a3c3ed5cd186f39e7880f8303cc51385a198a81469d53d0fdecf1f64d929/protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a", size = 427737, upload-time = "2026-03-18T19:04:51.866Z" },
{ url = "https://files.pythonhosted.org/packages/ee/90/b3c01fdec7d2f627b3a6884243ba328c1217ed2d978def5c12dc50d328a3/protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2", size = 324610, upload-time = "2026-03-18T19:04:53.096Z" },
{ url = "https://files.pythonhosted.org/packages/9b/ca/25afc144934014700c52e05103c2421997482d561f3101ff352e1292fb81/protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3", size = 339381, upload-time = "2026-03-18T19:04:54.616Z" },
{ url = "https://files.pythonhosted.org/packages/16/92/d1e32e3e0d894fe00b15ce28ad4944ab692713f2e7f0a99787405e43533a/protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593", size = 323436, upload-time = "2026-03-18T19:04:55.768Z" },
{ url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
@@ -380,6 +694,18 @@ 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 = "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-docx"
version = "1.2.0"
@@ -393,6 +719,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/00/1e03a4989fa5795da308cd774f05b704ace555a70f9bf9d3be057b680bcf/python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7", size = 252987, upload-time = "2025-06-16T20:46:22.506Z" },
]
[[package]]
name = "requests"
version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" },
]
[[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 = "typing-extensions"
version = "4.15.0"
@@ -402,6 +752,15 @@ 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 = "urllib3"
version = "2.6.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
[[package]]
name = "xlrd"
version = "2.0.2"
BIN
View File
Binary file not shown.