From ecf5713dd8a2b381693246b2b879700a77ca7947 Mon Sep 17 00:00:00 2001 From: Moose CLI Date: Sun, 14 Sep 2025 10:45:39 -0700 Subject: [PATCH 01/11] chore(cli): commit code generation outputs --- .gitignore | 3 + .../analytical-moose-foobar-py/.gitignore | 57 ++++++++++++ .../.vscode/extensions.json | 9 ++ .../.vscode/settings.json | 17 ++++ .../analytical-moose-foobar-py/README.md | 48 ++++++++++ .../app/__init__.py | 0 .../app/apis/__init__.py | 0 .../app/external_models.py | 91 +++++++++++++++++++ .../app/ingest/__init__.py | 0 .../analytical-moose-foobar-py/app/main.py | 57 ++++++++++++ .../app/scripts/__init__.py | 0 .../app/views/__init__.py | 0 .../moose.config.toml | 68 ++++++++++++++ .../requirements.txt | 5 + .../analytical-moose-foobar-py/setup.py | 12 +++ .../template.config.toml | 26 ++++++ 16 files changed, 393 insertions(+) create mode 100644 ufa-lite/services/analytical-moose-foobar-py/.gitignore create mode 100644 ufa-lite/services/analytical-moose-foobar-py/.vscode/extensions.json create mode 100644 ufa-lite/services/analytical-moose-foobar-py/.vscode/settings.json create mode 100644 ufa-lite/services/analytical-moose-foobar-py/README.md create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/external_models.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/ingest/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/main.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/scripts/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/views/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/moose.config.toml create mode 100644 ufa-lite/services/analytical-moose-foobar-py/requirements.txt create mode 100644 ufa-lite/services/analytical-moose-foobar-py/setup.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/template.config.toml diff --git a/.gitignore b/.gitignore index 93aa4834..8d2fe974 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ nohup.out services/*/temp_migration/ services/*/dev-*.log services/*/.moose/ + +# Python virtual environments +venv/ diff --git a/ufa-lite/services/analytical-moose-foobar-py/.gitignore b/ufa-lite/services/analytical-moose-foobar-py/.gitignore new file mode 100644 index 00000000..19dcb0d1 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/.gitignore @@ -0,0 +1,57 @@ +# Moose specific +.moose +.sloan + +# Python bytecode and cache +__pycache__ +*.pyc +*.pyo +*.pyd +.pytest_cache +.mypy_cache +.hypothesis +.coverage + +# Python virtual environments +.Python +env +.venv +venv +ENV +env.bak + +# IDE and editor files +.spyderproject +.ropeproject +.idea +*.ipynb_checkpoints +.cache +.cursor + +# Build and distribution +*.so +*.egg +*.egg-info +dist +build +develop-eggs +downloads +eggs +lib +lib64 +parts +sdist +var +wheels +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Coverage reports +cover +*.cover + +# OS specific +.DS_Store + diff --git a/ufa-lite/services/analytical-moose-foobar-py/.vscode/extensions.json b/ufa-lite/services/analytical-moose-foobar-py/.vscode/extensions.json new file mode 100644 index 00000000..dbc86167 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "frigus02.vscode-sql-tagged-template-literals-syntax-only", + "mtxr.sqltools", + "ultram4rine.sqltools-clickhouse-driver", + "jeppeandersen.vscode-kafka", + "rangav.vscode-thunder-client" + ] +} diff --git a/ufa-lite/services/analytical-moose-foobar-py/.vscode/settings.json b/ufa-lite/services/analytical-moose-foobar-py/.vscode/settings.json new file mode 100644 index 00000000..48a46372 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "sqltools.connections": [ + { + "server": "localhost", + "port": 18123, + "useHTTPS": false, + "database": "local", + "username": "panda", + "enableTls": false, + "password": "pandapass", + "driver": "ClickHouse", + "name": "moose clickhouse" + } + ], + "python.analysis.extraPaths": [".moose/versions"], + "python.analysis.typeCheckingMode": "basic" +} diff --git a/ufa-lite/services/analytical-moose-foobar-py/README.md b/ufa-lite/services/analytical-moose-foobar-py/README.md new file mode 100644 index 00000000..7f0ec736 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/README.md @@ -0,0 +1,48 @@ +# Template: Python (Empty) + +This is an empty Python-based Moose template that provides a minimal foundation for building data-intensive applications using Python. + +[![PyPI Version](https://img.shields.io/pypi/v/moose-cli?logo=python)](https://pypi.org/project/moose-cli/) +[![Moose Community](https://img.shields.io/badge/slack-moose_community-purple.svg?logo=slack)](https://join.slack.com/t/moose-community/shared_invite/zt-2fjh5n3wz-cnOmM9Xe9DYAgQrNu8xKxg) +[![Docs](https://img.shields.io/badge/quick_start-docs-blue.svg)](https://docs.fiveonefour.com/moose/getting-started/quickstart) +[![MIT license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE) + +## Getting Started + +### Prerequisites + +* [Docker Desktop](https://www.docker.com/products/docker-desktop/) +* [Python](https://www.python.org/downloads/) (version 3.8+) +* [An Anthropic API Key](https://docs.anthropic.com/en/api/getting-started) +* [Cursor](https://www.cursor.com/) or [Claude Desktop](https://claude.ai/download) + +### Installation + +1. Install Moose CLI: `pip install moose-cli` +2. Create project: `moose init python-empty` +3. Install dependencies: `cd && pip install -r requirements.txt` +4. Run Moose: `moose dev` + +You are ready to go! You can start editing the app by modifying primitives in the `app` subdirectory. + +## Learn More + +To learn more about Moose, take a look at the following resources: + +- [Moose Documentation](https://docs.fiveonefour.com/moose) - learn about Moose. +- [Sloan Documentation](https://docs.fiveonefour.com/sloan) - learn about Sloan, the MCP interface for data engineering. + +## Community + +You can join the Moose community [on Slack](https://join.slack.com/t/moose-community/shared_invite/zt-2fjh5n3wz-cnOmM9Xe9DYAgQrNu8xKxg). Check out the [MooseStack repo on GitHub](https://github.com/514-labs/moosestack). + +## Deploy on Boreal + +The easiest way to deploy your MooseStack Applications is to use [Boreal](https://www.fiveonefour.com/boreal) from 514 Labs, the creators of Moose. + +[Sign up](https://www.boreal.cloud/sign-up). + +## License + +This template is MIT licensed. + diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/external_models.py b/ufa-lite/services/analytical-moose-foobar-py/app/external_models.py new file mode 100644 index 00000000..c849ca31 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/external_models.py @@ -0,0 +1,91 @@ +# AUTO-GENERATED FILE. DO NOT EDIT. +# This file will be replaced when you run `moose db pull`. + +from pydantic import BaseModel, Field +from typing import Optional, Any, Annotated +import datetime +import ipaddress +from uuid import UUID +from enum import IntEnum, Enum +from moose_lib import Key, IngestPipeline, IngestPipelineConfig, OlapTable, OlapConfig, clickhouse_datetime64, clickhouse_decimal, ClickhouseSize, StringToEnumMixin +from moose_lib import clickhouse_default, LifeCycle +from moose_lib.blocks import MergeTreeEngine, ReplacingMergeTreeEngine, AggregatingMergeTreeEngine, SummingMergeTreeEngine, S3QueueEngine + +class _peerdb_raw_mirror_a4be3c5e__1df3__45e4__805b__cb363b330b4e(BaseModel): + UNDERSCORE_PREFIXED_peerdb_uid: UUID = Field(alias="_peerdb_uid") + UNDERSCORE_PREFIXED_peerdb_timestamp: Annotated[int, "int64"] = Field(alias="_peerdb_timestamp") + UNDERSCORE_PREFIXED_peerdb_destination_table_name: str = Field(alias="_peerdb_destination_table_name") + UNDERSCORE_PREFIXED_peerdb_data: str = Field(alias="_peerdb_data") + UNDERSCORE_PREFIXED_peerdb_record_type: Annotated[int, "int32"] = Field(alias="_peerdb_record_type") + UNDERSCORE_PREFIXED_peerdb_match_data: str = Field(alias="_peerdb_match_data") + UNDERSCORE_PREFIXED_peerdb_batch_id: Annotated[int, "int64"] = Field(alias="_peerdb_batch_id") + UNDERSCORE_PREFIXED_peerdb_unchanged_toast_columns: str = Field(alias="_peerdb_unchanged_toast_columns") + +class bar(BaseModel): + id: Key[UUID] + foo_id: UUID + value: Annotated[int, "int32"] + label: Optional[str] = None + notes: Optional[str] = None + is_enabled: bool + created_at: clickhouse_datetime64(6) + updated_at: clickhouse_datetime64(6) + UNDERSCORE_PREFIXED_peerdb_synced_at: Annotated[clickhouse_datetime64(9), clickhouse_default("now64()")] = Field(alias="_peerdb_synced_at") + UNDERSCORE_PREFIXED_peerdb_is_deleted: Annotated[int, "int8"] = Field(alias="_peerdb_is_deleted") + UNDERSCORE_PREFIXED_peerdb_version: Annotated[int, "int64"] = Field(alias="_peerdb_version") + +class foo(BaseModel): + id: Key[UUID] + name: str + description: Optional[str] = None + status: str + priority: Annotated[int, "int32"] + is_active: bool + metadata: Optional[str] = None + tags: list[str] + score: Optional[clickhouse_decimal(10, 2)] = None + large_text: Optional[str] = None + created_at: clickhouse_datetime64(6) + updated_at: clickhouse_datetime64(6) + UNDERSCORE_PREFIXED_peerdb_synced_at: Annotated[clickhouse_datetime64(9), clickhouse_default("now64()")] = Field(alias="_peerdb_synced_at") + UNDERSCORE_PREFIXED_peerdb_is_deleted: Annotated[int, "int8"] = Field(alias="_peerdb_is_deleted") + UNDERSCORE_PREFIXED_peerdb_version: Annotated[int, "int64"] = Field(alias="_peerdb_version") + +class users(BaseModel): + id: Key[UUID] + role: str + created_at: clickhouse_datetime64(6) + updated_at: clickhouse_datetime64(6) + UNDERSCORE_PREFIXED_peerdb_synced_at: Annotated[clickhouse_datetime64(9), clickhouse_default("now64()")] = Field(alias="_peerdb_synced_at") + UNDERSCORE_PREFIXED_peerdb_is_deleted: Annotated[int, "int8"] = Field(alias="_peerdb_is_deleted") + UNDERSCORE_PREFIXED_peerdb_version: Annotated[int, "int64"] = Field(alias="_peerdb_version") + +peerdb_raw_mirror_a_4_be_3_c_5_e_1_df_3_45_e_4_805_b_cb_363_b_330_b_4_e_model = OlapTable[_peerdb_raw_mirror_a4be3c5e__1df3__45e4__805b__cb363b330b4e]("_peerdb_raw_mirror_a4be3c5e__1df3__45e4__805b__cb363b330b4e", OlapConfig( + order_by_fields=["_peerdb_batch_id", "_peerdb_destination_table_name"], + life_cycle=LifeCycle.EXTERNALLY_MANAGED, + engine=MergeTreeEngine(), + settings={"index_granularity": "8192"}, +)) + +bar_model = OlapTable[bar]("bar", OlapConfig( + order_by_fields=["id"], + life_cycle=LifeCycle.EXTERNALLY_MANAGED, + engine=ReplacingMergeTreeEngine(ver="_peerdb_version"), + settings={"index_granularity": "8192"}, +)) + +foo_model = OlapTable[foo]("foo", OlapConfig( + order_by_fields=["id"], + life_cycle=LifeCycle.EXTERNALLY_MANAGED, + engine=ReplacingMergeTreeEngine(ver="_peerdb_version"), + settings={"index_granularity": "8192"}, +)) + +users_model = OlapTable[users]("users", OlapConfig( + order_by_fields=["id"], + life_cycle=LifeCycle.EXTERNALLY_MANAGED, + engine=ReplacingMergeTreeEngine(ver="_peerdb_version"), + settings={"index_granularity": "8192"}, +)) + + diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/ingest/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/ingest/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/main.py b/ufa-lite/services/analytical-moose-foobar-py/app/main.py new file mode 100644 index 00000000..9223c67b --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/main.py @@ -0,0 +1,57 @@ +# Welcome to your new Moose analytical backend! 🦌 + +# Getting Started Guide: + +# 1. Data Modeling +# First, plan your data structure and create your data models +# → See: docs.fiveonefour.com/moose/building/data-modeling +# Learn about type definitions and data validation + +# 2. Set Up Ingestion +# Create ingestion pipelines to receive your data via REST APIs +# → See: docs.fiveonefour.com/moose/building/ingestion +# Learn about IngestPipeline, data formats, and validation + +# 3. Create Workflows +# Build data processing pipelines to transform and analyze your data +# → See: docs.fiveonefour.com/moose/building/workflows +# Learn about task scheduling and data processing + +# 4. Configure Consumption APIs +# Set up queries and real-time analytics for your data +# → See: docs.fiveonefour.com/moose/building/consumption-apis + +# Need help? Check out the quickstart guide: +# → docs.fiveonefour.com/moose/getting-started/quickstart + +from external_models import * + + +from pydantic import BaseModel, Field +from typing import Optional, Any, Annotated +import datetime +import ipaddress +from uuid import UUID +from enum import IntEnum, Enum +from moose_lib import Key, IngestPipeline, IngestPipelineConfig, OlapTable, OlapConfig, clickhouse_datetime64, clickhouse_decimal, ClickhouseSize, StringToEnumMixin +from moose_lib import clickhouse_default, LifeCycle +from moose_lib.blocks import MergeTreeEngine, ReplacingMergeTreeEngine, AggregatingMergeTreeEngine, SummingMergeTreeEngine, S3QueueEngine + +class dish(BaseModel): + id: Annotated[int, "uint32"] + name: str + description: str + menus_appeared: Annotated[int, "uint32"] + times_appeared: Annotated[int, "int32"] + first_appeared: Annotated[int, "uint16"] + last_appeared: Annotated[int, "uint16"] + lowest_price: clickhouse_decimal(18, 3) + highest_price: clickhouse_decimal(18, 3) + +dish_model = OlapTable[dish]("dish", OlapConfig( + order_by_fields=["id"], + engine=MergeTreeEngine(), + settings={"index_granularity": "8192"}, +)) + + diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/scripts/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/views/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml b/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml new file mode 100644 index 00000000..8a5ae42e --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml @@ -0,0 +1,68 @@ +language = "Python" + +[redpanda_config] +broker = "localhost:19092" +message_timeout_ms = 1000 +retention_ms = 30000 +replication_factor = 1 + +[clickhouse_config] +db_name = "local" +user = "panda" +password = "pandapass" +use_ssl = false +host = "localhost" +host_port = 18123 +native_port = 9000 + +[http_server_config] +host = "localhost" +port = 4000 +management_port = 5001 +proxy_port = 4001 +max_request_body_size = 10485760 + +[redis_config] +url = "redis://127.0.0.1:6379" +key_prefix = "MS" +last_key_prefix = "MS" +port = 6379 +tls = false +hostname = "127.0.0.1" + +[git_config] +main_branch_name = "main" + +[temporal_config] +db_user = "temporal" +db_password = "temporal" +db_port = 5432 +namespace = "default" +temporal_host = "localhost" +temporal_port = 7233 +temporal_version = "1.22.3" +temporal_region = "us-west1" +admin_tools_version = "1.22.3" +ui_version = "2.21.3" +ui_port = 8080 +ui_cors_origins = "http://localhost:3000" +config_path = "config/dynamicconfig/development-sql.yaml" +postgresql_version = "13" +client_cert = "" +client_key = "" +ca_cert = "" +api_key = "" + +[supported_old_versions] + +[authentication] + +[features] +streaming_engine = false +workflows = false +data_model_v2 = true +olap = true +ddl_plan = false + +[typescript_config] +package_manager = "npm" diff --git a/ufa-lite/services/analytical-moose-foobar-py/requirements.txt b/ufa-lite/services/analytical-moose-foobar-py/requirements.txt new file mode 100644 index 00000000..6c427d42 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/requirements.txt @@ -0,0 +1,5 @@ +kafka-python-ng==2.2.2 +clickhouse-connect==0.7.16 +requests==2.32.4 +moose-cli +moose-lib \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/setup.py b/ufa-lite/services/analytical-moose-foobar-py/setup.py new file mode 100644 index 00000000..b6873d26 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/setup.py @@ -0,0 +1,12 @@ + +from setuptools import setup + +with open('requirements.txt') as f: + requirements = [line.strip() for line in f if line.strip() and not line.startswith('#')] + +setup( + name='analytical-moose-foobar-py', + version='0.0', + install_requires=requirements, + python_requires='>=3.12', +) diff --git a/ufa-lite/services/analytical-moose-foobar-py/template.config.toml b/ufa-lite/services/analytical-moose-foobar-py/template.config.toml new file mode 100644 index 00000000..266c0c29 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/template.config.toml @@ -0,0 +1,26 @@ +language = "python" # Must be typescript or python +description = "Empty python project." +post_install_print = """ +Deploy on Boreal + +The easiest way to deploy your MooseStack Applications is to use Boreal +from the creators of MooseStack. + +https://boreal.cloud + +--------------------------------------------------------- + +📂 Go to your project directory: + $ cd {project_dir} + +🥄 Create a virtual environment (optional, recommended): + $ python3 -m venv .venv + $ source .venv/bin/activate + +📦 Install Dependencies: + $ pip install -r ./requirements.txt + +🛠️ Start dev server: + $ moose dev +""" +default_sloan_telemetry="standard" From 0d6e67e8a0e4f2ad696a37260596c9a1833b8b5a Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 11:22:24 -0700 Subject: [PATCH 02/11] fix external import --- ufa-lite/services/analytical-moose-foobar-py/app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/main.py b/ufa-lite/services/analytical-moose-foobar-py/app/main.py index 9223c67b..adddab8a 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/main.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/main.py @@ -24,7 +24,7 @@ # Need help? Check out the quickstart guide: # → docs.fiveonefour.com/moose/getting-started/quickstart -from external_models import * +from .external_models import * from pydantic import BaseModel, Field From 53d0b2a4bb9ecd182b8f1062c6c8271bdf103259 Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 11:35:36 -0700 Subject: [PATCH 03/11] claude md --- CLAUDE.md | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..a09cf2f7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,224 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Area Code is a starter repository with multi-modal backend capabilities combining transactional (PostgreSQL), analytical (ClickHouse), and search (Elasticsearch) systems. Built with Turborepo, it includes two main applications: User Facing Analytics (UFA) and Operational Data Warehouse (ODW), plus a lightweight UFA-Lite variant. + +## Package Manager & Node Version + +- **Always use pnpm** (never npm) +- **Required Node version: 20+** (required for Moose framework) +- Package manager: `pnpm@10.14.0` + +## Development Commands + +### UFA (User Facing Analytics) +```bash +# Start all UFA services +pnpm ufa:dev + +# Clean all UFA services and dependencies +pnpm ufa:dev:clean + +# Seed databases with sample data (1M foo records, 100K bar records) +pnpm ufa:dev:seed + +# Seed specific services +pnpm ufa:dev:seed:transactional-supabase-foobar +pnpm ufa:dev:seed:analytical-moose-foobar +pnpm ufa:dev:seed:retrieval-elasticsearch-foobar +``` + +### UFA-Lite (without Elasticsearch) +```bash +# Start UFA-Lite services +pnpm ufa-lite:dev + +# Clean UFA-Lite services +pnpm ufa-lite:dev:clean + +# Seed UFA-Lite databases +pnpm ufa-lite:dev:seed +``` + +### ODW (Operational Data Warehouse) +```bash +# Start ODW services +pnpm odw:dev + +# Clean ODW services +pnpm odw:dev:clean + +# Seed ODW databases +pnpm odw:dev:seed +``` + +### Individual Service Development +```bash +# Frontend only +pnpm --filter web-frontend-foobar dev + +# Transactional API only +pnpm --filter transactional-supabase-foobar dev + +# Analytical API only +pnpm --filter analytical-moose-foobar dev + +# Search API only (UFA only) +pnpm --filter retrieval-elasticsearch-foobar dev +``` + +### Testing and Quality +```bash +# Build all packages +turbo build + +# Lint all packages +turbo lint + +# Type checking (service-specific) +pnpm --filter typecheck +``` + +## Project Structure + +This is a **Turborepo monorepo** with workspaces organized as: + +``` +area-code/ +├── ufa/ # Full-featured UFA +│ ├── apps/ # User-facing applications +│ │ └── web-frontend-foobar/ +│ ├── services/ # Backend services +│ │ ├── transactional-supabase-foobar/ # PostgreSQL + Fastify +│ │ ├── analytical-moose-foobar/ # ClickHouse + Moose +│ │ ├── retrieval-elasticsearch-foobar/ # Elasticsearch +│ │ └── sync-supabase-moose-foobar/ # Data sync workflows +│ └── packages/ # Shared packages +│ ├── models/ # Data models +│ ├── ui/ # UI components +│ ├── eslint-config/ # ESLint config +│ ├── typescript-config/ +│ └── tailwind-config/ +├── ufa-lite/ # Lightweight UFA (no Elasticsearch) +│ ├── apps/ +│ └── services/ +└── odw/ # Operational Data Warehouse + ├── apps/ + ├── services/ + └── packages/ +``` + +## Architecture Components + +### UFA Stack +- **Frontend**: Vite + React 19 + TypeScript + TanStack (Router, Query, Form, Table) + Tailwind CSS +- **Transactional**: PostgreSQL + Fastify + Drizzle ORM + Supabase Realtime +- **Analytical**: ClickHouse + Moose framework (API & Ingest) +- **Search**: Elasticsearch (UFA only, not in UFA-Lite) +- **Sync & Streaming**: Moose Workflows (Temporal) + Moose Stream (Redpanda) + +### Service Ports +**UFA Ports:** +- Frontend: http://localhost:5173 +- Transactional API: http://localhost:8080 +- Analytical Moose: http://localhost:4000 (proxy 4001, management 5001) +- Retrieval API: http://localhost:8081 +- Sync Moose: http://localhost:4100 (management 5101) + +**UFA-Lite Ports:** +- Transactional API: http://localhost:8082 +- Analytical Moose: http://localhost:4410 (proxy 4411, management 5411) +- Sync Moose: http://localhost:4400 (management 5401) + +### Key Technologies +- **Moose**: Framework for analytical APIs, streaming pipelines, and workflows +- **Drizzle ORM**: TypeScript-first ORM for PostgreSQL +- **Supabase**: PostgreSQL with realtime subscriptions +- **ClickHouse**: Analytical database +- **Elasticsearch**: Search engine (UFA only) +- **Temporal**: Workflow orchestration +- **Redpanda**: Event streaming + +## Development Workflow + +1. **Installation**: `pnpm install` (workspace root) +2. **Start Services**: Use appropriate `pnpm :dev` command +3. **Seed Data**: Use `pnpm :dev:seed` for sample data +4. **Individual Development**: Use `--filter` for specific services + +### Moose Service Development +- Moose services support **hot reload** - no manual restart needed +- Test workflows: `moose workflow run ` +- Moose CLI commands: `moose-cli dev`, `moose-cli build`, `moose-cli clean` + +### Package Naming Convention +- Prefix shared packages with `@workspace/` +- Examples: `@workspace/models`, `@workspace/ui`, `@repo/eslint-config` + +## Environment Setup + +### Required for AI Chat Feature (Optional) +Create `.env.local` in `services/transactional-supabase-foobar/`: +```bash +ANTHROPIC_API_KEY=your-api-key-here +``` + +### UFA-Lite Frontend Environment +Create `.env.development.local` in `ufa-lite/apps/web-frontend-foobar/`: +```bash +VITE_ENABLE_SEARCH=false +VITE_TRANSACTIONAL_API_BASE=http://localhost:8082 +VITE_ANALYTICAL_CONSUMPTION_API_BASE=http://localhost:4410 +VITE_SUPABASE_URL=http://localhost:54321 +VITE_SUPABASE_ANON_KEY=dev-anon-key +``` + +## Database Management + +### Transactional Services (PostgreSQL/Supabase) +```bash +# Start database +pnpm --filter transactional-supabase-foobar db:start + +# Stop database +pnpm --filter transactional-supabase-foobar db:stop + +# Run migrations +pnpm --filter transactional-supabase-foobar db:migrate + +# Generate migrations +pnpm --filter transactional-supabase-foobar db:generate +``` + +## Troubleshooting + +### Memory Requirements +- Elasticsearch requires 4GB+ RAM +- Tested on Mac M3/M4 Pro with 18GB+ RAM + +### Reset Environment +```bash +# Clean all services and dependencies +pnpm :dev:clean + +# Restart development +pnpm :dev +``` + +### Common Issues +1. **Node Version**: Ensure Node 20+ for Moose compatibility +2. **Memory**: Insufficient RAM for Elasticsearch +3. **Port Conflicts**: Check that required ports are available +4. **Service Dependencies**: Ensure all services start in correct order + +## Important Notes + +- **Never use npm** - always use pnpm +- **Don't override .env files** - use .env.local for local overrides +- Moose services require Node 20+ +- UFA-Lite omits Elasticsearch for lighter resource usage +- All stacks share containers (Postgres, ClickHouse, Redpanda, Temporal) +- Use `tmux-agent-cmd.sh` wrapper for command execution if available \ No newline at end of file From ee4384461bf9cc93b958c5e946e951e65ab8d07e Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 13:52:57 -0700 Subject: [PATCH 04/11] working foo bar apis --- .vscode/settings.json | 17 +- .../app/apis/__init__.py | 22 +++ .../app/apis/bar/__init__.py | 1 + .../app/apis/bar/consumption/__init__.py | 8 + .../bar/consumption/bar_average_value_api.py | 53 ++++++ .../app/apis/bar/consumption/bar_base_api.py | 156 +++++++++++++++++ .../app/apis/foo/__init__.py | 1 + .../app/apis/foo/consumption/__init__.py | 12 ++ .../app/apis/foo/consumption/foo_base_api.py | 163 ++++++++++++++++++ .../consumption/foo_cube_aggregations_api.py | 142 +++++++++++++++ .../foo/consumption/foo_filters_values_api.py | 82 +++++++++ .../consumption/foo_score_over_time_api.py | 78 +++++++++ .../analytical-moose-foobar-py/app/main.py | 9 + 13 files changed, 743 insertions(+), 1 deletion(-) create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/__init__.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py create mode 100644 ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 5a16e855..d66a2a14 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -26,5 +26,20 @@ ".*[Cc]lasses.*" ], "tailwindCSS.rootFontSize": 16, - "tailwindCSS.hooverPreview": true + "tailwindCSS.hooverPreview": true, + "sqltools.connections": [ + { + "server": "http://localhost", + "port": 18123, + "database": "default", + "useJWT": false, + "requestTimeout": 30000, + "enableTls": false, + "previewLimit": 50, + "password": "pandapass", + "username": "panda", + "driver": "ClickHouse", + "name": "local" + } + ] } diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/__init__.py index e69de29b..4c5284a9 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/__init__.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/__init__.py @@ -0,0 +1,22 @@ +# All APIs for the analytical service +from .foo.consumption import ( + foo_consumption_api, + foo_cube_aggregations_api, + foo_filters_values_api, + foo_score_over_time_api +) +from .bar.consumption import ( + bar_consumption_api, + bar_average_value_api +) + +__all__ = [ + # Foo APIs + "foo_consumption_api", + "foo_cube_aggregations_api", + "foo_filters_values_api", + "foo_score_over_time_api", + # Bar APIs + "bar_consumption_api", + "bar_average_value_api" +] \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/__init__.py new file mode 100644 index 00000000..2d12f3ef --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/__init__.py @@ -0,0 +1 @@ +# Bar APIs module \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/__init__.py new file mode 100644 index 00000000..83f0b3d6 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/__init__.py @@ -0,0 +1,8 @@ +# Bar consumption APIs +from .bar_base_api import bar_consumption_api +from .bar_average_value_api import bar_average_value_api + +__all__ = [ + "bar_consumption_api", + "bar_average_value_api" +] \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py new file mode 100644 index 00000000..58e2854e --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py @@ -0,0 +1,53 @@ +from moose_lib import Api +from typing import Dict, Any +from pydantic import BaseModel +from app.external_models import bar_model +import time + + +class EmptyParams(BaseModel): + """Empty parameters for endpoints with no input parameters""" + pass + + +class GetBarsAverageValueResponse(BaseModel): + averageValue: float + queryTime: int + count: int + + +def bar_average_value_api_handler( + context, + params: EmptyParams +) -> GetBarsAverageValueResponse: + """ + API to get average value of all bars + """ + start_time = time.time() + + query = """ + SELECT + AVG(value) as averageValue, + COUNT(*) as count + FROM bar + WHERE value IS NOT NULL + """ + + results = context.query(query, {}) + + query_time = int((time.time() - start_time) * 1000) + + result = results[0] if results else {"averageValue": 0, "count": 0} + + return GetBarsAverageValueResponse( + averageValue=float(result["averageValue"]), + queryTime=query_time, + count=int(result["count"]) + ) + + +# Create the API instance +bar_average_value_api = Api[EmptyParams, GetBarsAverageValueResponse]( + name="bar-average-value", + query_function=bar_average_value_api_handler +) \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py new file mode 100644 index 00000000..671796b1 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py @@ -0,0 +1,156 @@ +from moose_lib import Api +from typing import Optional, List +from pydantic import BaseModel, Field +from app.external_models import bar, bar_model +import time +from datetime import datetime, timezone + + +def safe_datetime_convert(value) -> str: + """ + Safely convert a value to JavaScript-compatible ISO datetime string. + Handles various input formats from ClickHouse. + """ + if value is None or value == "": + return "1970-01-01T00:00:00.000Z" # Return epoch time for null/empty values + + if isinstance(value, datetime): + # Ensure timezone info and return ISO format with Z suffix + if value.tzinfo is None: + # Assume UTC if no timezone info + value = value.replace(tzinfo=timezone.utc) + return value.isoformat().replace('+00:00', 'Z') + + if isinstance(value, str): + # Try to parse and reformat the string + try: + # Try ISO format first + dt = datetime.fromisoformat(value.replace('Z', '+00:00')) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat().replace('+00:00', 'Z') + except ValueError: + # Try other common formats + try: + dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat().replace('+00:00', 'Z') + except ValueError: + try: + dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f') + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat().replace('+00:00', 'Z') + except ValueError: + # Last resort - return current time in ISO format + return datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + + # If it's some other type, try to convert to string first + try: + dt = datetime.fromisoformat(str(value).replace('Z', '+00:00')) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat().replace('+00:00', 'Z') + except (ValueError, TypeError): + # Final fallback - return epoch time + return "1970-01-01T00:00:00.000Z" + + +class GetBarsParams(BaseModel): + limit: Optional[int] = 10 + offset: Optional[int] = 0 + sort_by: Optional[str] = "created_at" + sort_order: Optional[str] = "DESC" + + +class BarWithCDC(BaseModel): + id: str + foo_id: str + value: int + label: Optional[str] = None + notes: Optional[str] = None + is_enabled: bool + created_at: str + updated_at: str + _peerdb_synced_at: str + _peerdb_is_deleted: int + _peerdb_version: int + + +class PaginationInfo(BaseModel): + limit: int + offset: int + total: int + hasMore: bool + + +class GetBarsResponse(BaseModel): + data: List[BarWithCDC] + pagination: PaginationInfo + queryTime: int + + +def bar_consumption_api_handler(context, params: GetBarsParams) -> GetBarsResponse: + """ + Consumption API for bar data following Moose documentation pattern + """ + # Convert sort_order to uppercase for consistency + upper_sort_order = params.sort_order.upper() + + # Get total count + count_query = f"SELECT count() as total FROM bar" + count_result = context.query(count_query, {}) + total_count = count_result[0]["total"] if count_result else 0 + + start_time = time.time() + + # Build dynamic query including CDC fields + query = f""" + SELECT * + FROM bar + ORDER BY {params.sort_by} {upper_sort_order} + LIMIT {params.limit} + OFFSET {params.offset} + """ + + results = context.query(query, {}) + + query_time = int((time.time() - start_time) * 1000) # Convert to milliseconds + + # Create pagination metadata + has_more = params.offset + len(results) < total_count + + # Convert results to response format + data = [ + BarWithCDC( + id=str(row["id"]), + foo_id=str(row["foo_id"]), + value=int(row["value"]), + label=row.get("label"), + notes=row.get("notes"), + is_enabled=bool(row["is_enabled"]), + created_at=safe_datetime_convert(row["created_at"]), + updated_at=safe_datetime_convert(row["updated_at"]), + _peerdb_synced_at=safe_datetime_convert(row["_peerdb_synced_at"]), + _peerdb_is_deleted=int(row["_peerdb_is_deleted"]), + _peerdb_version=int(row["_peerdb_version"]) + ) + for row in results + ] + + return GetBarsResponse( + data=data, + pagination=PaginationInfo( + limit=params.limit, + offset=params.offset, + total=total_count, + hasMore=has_more + ), + queryTime=query_time + ) + + +# Create the API instance +bar_consumption_api = Api[GetBarsParams, GetBarsResponse]( + name="bar", + query_function=bar_consumption_api_handler +) \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/__init__.py new file mode 100644 index 00000000..906d034b --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/__init__.py @@ -0,0 +1 @@ +# Foo APIs module \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/__init__.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/__init__.py new file mode 100644 index 00000000..9f9775b5 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/__init__.py @@ -0,0 +1,12 @@ +# Foo consumption APIs +from .foo_base_api import foo_consumption_api +from .foo_cube_aggregations_api import foo_cube_aggregations_api +from .foo_filters_values_api import foo_filters_values_api +from .foo_score_over_time_api import foo_score_over_time_api + +__all__ = [ + "foo_consumption_api", + "foo_cube_aggregations_api", + "foo_filters_values_api", + "foo_score_over_time_api" +] \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py new file mode 100644 index 00000000..6b01d928 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py @@ -0,0 +1,163 @@ +from moose_lib import Api +from typing import Optional, List +from pydantic import BaseModel +from app.external_models import foo, foo_model +import time +from datetime import datetime, timezone + + +def safe_datetime_convert(value) -> str: + """ + Safely convert a value to JavaScript-compatible ISO datetime string. + Handles various input formats from ClickHouse. + """ + if value is None or value == "": + return "1970-01-01T00:00:00.000Z" # Return epoch time for null/empty values + + if isinstance(value, datetime): + # Ensure timezone info and return ISO format with Z suffix + if value.tzinfo is None: + # Assume UTC if no timezone info + value = value.replace(tzinfo=timezone.utc) + return value.isoformat().replace('+00:00', 'Z') + + if isinstance(value, str): + # Try to parse and reformat the string + try: + # Try ISO format first + dt = datetime.fromisoformat(value.replace('Z', '+00:00')) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat().replace('+00:00', 'Z') + except ValueError: + # Try other common formats + try: + dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat().replace('+00:00', 'Z') + except ValueError: + try: + dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f') + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat().replace('+00:00', 'Z') + except ValueError: + # Last resort - return current time in ISO format + return datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') + + # If it's some other type, try to convert to string first + try: + dt = datetime.fromisoformat(str(value).replace('Z', '+00:00')) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat().replace('+00:00', 'Z') + except (ValueError, TypeError): + # Final fallback - return epoch time + return "1970-01-01T00:00:00.000Z" + + +class GetFoosParams(BaseModel): + limit: Optional[int] = 10 + offset: Optional[int] = 0 + sort_by: Optional[str] = "created_at" + sort_order: Optional[str] = "DESC" + + +class FooWithCDCForConsumption(BaseModel): + id: str + name: str + description: Optional[str] = None + status: str + priority: int + is_active: bool + metadata: Optional[str] = None + tags: List[str] + score: Optional[float] = None + large_text: Optional[str] = None + created_at: str + updated_at: str + _peerdb_synced_at: str + _peerdb_is_deleted: int + _peerdb_version: int + + +class PaginationInfo(BaseModel): + limit: int + offset: int + total: int + hasMore: bool + + +class GetFoosResponse(BaseModel): + data: List[FooWithCDCForConsumption] + pagination: PaginationInfo + queryTime: int + + +def foo_consumption_api_handler(context, params: GetFoosParams) -> GetFoosResponse: + """ + Consumption API for foo data following Moose documentation pattern + """ + # Convert sort_order to uppercase for consistency + upper_sort_order = params.sort_order.upper() + + # Get total count + count_query = f"SELECT count() as total FROM foo" + count_result = context.query(count_query, {}) + total_count = count_result[0]["total"] if count_result else 0 + + start_time = time.time() + + # Build dynamic query including CDC fields + query = f""" + SELECT * + FROM foo + ORDER BY {params.sort_by} {upper_sort_order} + LIMIT {params.limit} + OFFSET {params.offset} + """ + + results = context.query(query, {}) + + query_time = int((time.time() - start_time) * 1000) # Convert to milliseconds + + # Create pagination metadata (matching transactional API format) + has_more = params.offset + len(results) < total_count + + # Convert results to response format + data = [ + FooWithCDCForConsumption( + id=str(row["id"]), + name=row["name"], + description=row.get("description"), + status=str(row["status"]), # Convert status to string for consumption + priority=row["priority"], + is_active=row["is_active"], + metadata=row.get("metadata"), + tags=row["tags"] or [], + score=float(row["score"]) if row.get("score") is not None else None, + large_text=row.get("large_text"), + created_at=safe_datetime_convert(row["created_at"]), + updated_at=safe_datetime_convert(row["updated_at"]), + _peerdb_synced_at=safe_datetime_convert(row["_peerdb_synced_at"]), + _peerdb_is_deleted=row["_peerdb_is_deleted"], + _peerdb_version=row["_peerdb_version"] + ) + for row in results + ] + + return GetFoosResponse( + data=data, + pagination=PaginationInfo( + limit=params.limit, + offset=params.offset, + total=total_count, + hasMore=has_more + ), + queryTime=query_time + ) + +# Create the API instance +foo_consumption_api = Api[GetFoosParams, GetFoosResponse]( + name="foo", + query_function=foo_consumption_api_handler +) \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py new file mode 100644 index 00000000..c860c831 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py @@ -0,0 +1,142 @@ +from moose_lib import Api +from typing import Optional, List +from pydantic import BaseModel +from app.external_models import foo_model +import time +from datetime import datetime, timedelta + + +class GetFooCubeAggregationsParams(BaseModel): + months: Optional[int] = 6 + status: Optional[str] = None + tag: Optional[str] = None + priority: Optional[int] = None + limit: Optional[int] = 20 + offset: Optional[int] = 0 + sort_by: Optional[str] = None + sort_order: Optional[str] = "ASC" + + +class FooCubeAggregationRow(BaseModel): + month: Optional[str] = None + status: Optional[str] = None + tag: Optional[str] = None + priority: Optional[int] = None + n: int + avgScore: float + p50: float + p90: float + + +class GetFooCubeAggregationsResponse(BaseModel): + data: List[FooCubeAggregationRow] + queryTime: int + pagination: dict + + +def foo_cube_aggregations_api_handler( + context, + params: GetFooCubeAggregationsParams +) -> GetFooCubeAggregationsResponse: + """ + Cube aggregations API for foo data with monthly grouping and statistical analysis + """ + start_time = time.time() + + # Calculate date range + end_date = datetime.now() + start_date = end_date - timedelta(days=max(1, min(params.months, 36)) * 30) + + start_date_str = start_date.strftime("%Y-%m-%d") + end_date_str = end_date.strftime("%Y-%m-%d") + + # Build optional WHERE fragments + where_clauses = [ + f"toDate(created_at) >= toDate('{start_date_str}')", + f"toDate(created_at) <= toDate('{end_date_str}')", + "score IS NOT NULL" + ] + + if params.status: + where_clauses.append(f"status = '{params.status}'") + + if params.priority is not None: + where_clauses.append(f"priority = {params.priority}") + + limited = max(1, min(params.limit, 200)) + paged_offset = max(0, params.offset) + + # Map sort column safely + sort_column = { + "month": "month", + "status": "status", + "tag": "tag", + "priority": "priority", + "n": "n", + "avgScore": "avgScore", + "avg_score": "avgScore", + "p50": "p50", + "p90": "p90" + }.get(params.sort_by, "month, status, tag, priority") + + sort_dir = "DESC" if params.sort_order.upper() == "DESC" else "ASC" + + # Build the main query + where_clause = " AND ".join(where_clauses) + having_clause = f"HAVING tag = '{params.tag}'" if params.tag else "" + + query = f""" + SELECT + formatDateTime(toStartOfMonth(created_at), '%Y-%m-01') AS month, + status, + arrayJoin(tags) AS tag, + priority, + count() AS n, + avg(score) AS avgScore, + quantileTDigest(0.5)(toFloat64(score)) AS p50, + quantileTDigest(0.9)(toFloat64(score)) AS p90, + COUNT() OVER() AS total + FROM foo + WHERE {where_clause} + GROUP BY month, status, tag, priority + {having_clause} + ORDER BY {sort_column} {sort_dir} + LIMIT {limited} OFFSET {paged_offset} + """ + + results = context.query(query, {}) + + query_time = int((time.time() - start_time) * 1000) + total = results[0]["total"] if results else 0 + + # Convert results to response format + data = [ + FooCubeAggregationRow( + month=row["month"], + status=row["status"], + tag=row["tag"], + priority=int(row["priority"]) if row["priority"] is not None else None, + n=int(row["n"]), + avgScore=float(row["avgScore"]), + p50=float(row["p50"]), + p90=float(row["p90"]) + ) + for row in results + ] + + return GetFooCubeAggregationsResponse( + data=data, + queryTime=query_time, + pagination={ + "limit": limited, + "offset": paged_offset, + "total": total + } + ) + + +# Create the API instance +foo_cube_aggregations_api = Api[GetFooCubeAggregationsParams, GetFooCubeAggregationsResponse]( + name="foo-cube-aggregations", + query_function=foo_cube_aggregations_api_handler +) \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py new file mode 100644 index 00000000..56f840d5 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py @@ -0,0 +1,82 @@ +from moose_lib import Api +from typing import Optional, List +from pydantic import BaseModel +from datetime import datetime, timedelta + + +class GetFooFiltersValuesParams(BaseModel): + months: Optional[int] = 6 + + +class GetFooFiltersValuesResponse(BaseModel): + status: List[str] + tags: List[str] + priorities: List[int] + + +def foo_filters_values_api_handler( + context, + params: GetFooFiltersValuesParams +) -> GetFooFiltersValuesResponse: + """ + API to get distinct filter values for foo data + """ + # Calculate date range + end_date = datetime.now() + start_date = end_date - timedelta(days=max(1, min(params.months, 36)) * 30) + + start_date_str = start_date.strftime("%Y-%m-%d") + end_date_str = end_date.strftime("%Y-%m-%d") + + # Build queries for distinct values + date_filter = f""" + toDate(created_at) >= toDate('{start_date_str}') + AND toDate(created_at) <= toDate('{end_date_str}') + """ + + status_query = f""" + SELECT DISTINCT status + FROM foo + WHERE {date_filter} + AND status IS NOT NULL + ORDER BY status + """ + + tags_query = f""" + SELECT DISTINCT arrayJoin(tags) AS tag + FROM foo + WHERE {date_filter} + AND tags IS NOT NULL + ORDER BY tag + """ + + priorities_query = f""" + SELECT DISTINCT priority + FROM foo + WHERE {date_filter} + AND priority IS NOT NULL + ORDER BY priority + """ + + # Execute queries + status_results = context.query(status_query, {}) + tags_results = context.query(tags_query, {}) + priorities_results = context.query(priorities_query, {}) + + # Extract values from results + possible_statuses = [row["status"] for row in status_results] + possible_tags = [row["tag"] for row in tags_results] + possible_priorities = [int(row["priority"]) for row in priorities_results] + + return GetFooFiltersValuesResponse( + status=possible_statuses, + tags=possible_tags, + priorities=possible_priorities + ) + + +# Create the API instance +foo_filters_values_api = Api[GetFooFiltersValuesParams, GetFooFiltersValuesResponse]( + name="foo-filters-values", + query_function=foo_filters_values_api_handler +) \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py new file mode 100644 index 00000000..4fe4a3c8 --- /dev/null +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py @@ -0,0 +1,78 @@ +from moose_lib import Api +from typing import Optional, List +from pydantic import BaseModel +import time +from datetime import datetime, timedelta + + +class FoosScoreOverTimeDataPoint(BaseModel): + date: str + averageScore: float + totalCount: int + + +class GetFoosScoreOverTimeParams(BaseModel): + days: Optional[int] = 90 + + +class GetFoosScoreOverTimeResponse(BaseModel): + data: List[FoosScoreOverTimeDataPoint] + queryTime: int + + +def foo_score_over_time_api_handler( + context, + params: GetFoosScoreOverTimeParams +) -> GetFoosScoreOverTimeResponse: + """ + Score over time consumption API for foo data + """ + # Calculate date range + start_date = datetime.now() - timedelta(days=params.days) + end_date = datetime.now() + + # Format dates for SQL + start_date_str = start_date.strftime("%Y-%m-%d") + end_date_str = end_date.strftime("%Y-%m-%d") + + # Query to get daily score aggregations + query = f""" + SELECT + formatDateTime(toDate(created_at), '%Y-%m-%d') as date, + AVG(score) as averageScore, + COUNT(*) as totalCount + FROM foo + WHERE toDate(created_at) >= toDate('{start_date_str}') + AND toDate(created_at) <= toDate('{end_date_str}') + AND score IS NOT NULL + GROUP BY toDate(created_at) + ORDER BY toDate(created_at) ASC + """ + + start_time = time.time() + + results = context.query(query, {}) + + query_time = int((time.time() - start_time) * 1000) + + # Convert results to response format + data = [ + FoosScoreOverTimeDataPoint( + date=str(row["date"]), # Date is already formatted as YYYY-MM-DD string + averageScore=round(float(row["averageScore"]), 2), # Round to 2 decimal places + totalCount=int(row["totalCount"]) + ) + for row in results + ] + + return GetFoosScoreOverTimeResponse( + data=data, + queryTime=query_time + ) + + +# Create the API instance +foo_score_over_time_api = Api[GetFoosScoreOverTimeParams, GetFoosScoreOverTimeResponse]( + name="foo-score-over-time", + query_function=foo_score_over_time_api_handler +) \ No newline at end of file diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/main.py b/ufa-lite/services/analytical-moose-foobar-py/app/main.py index adddab8a..d8759f63 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/main.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/main.py @@ -24,8 +24,17 @@ # Need help? Check out the quickstart guide: # → docs.fiveonefour.com/moose/getting-started/quickstart +# Import external models for their side effects: registers additional data models required for pipeline and API setup from .external_models import * +# Import and register all consumption APIs +from .apis.bar.consumption.bar_average_value_api import bar_average_value_api +from .apis.bar.consumption.bar_base_api import bar_consumption_api +from .apis.foo.consumption.foo_base_api import foo_consumption_api +from .apis.foo.consumption.foo_score_over_time_api import foo_score_over_time_api +from .apis.foo.consumption.foo_cube_aggregations_api import foo_cube_aggregations_api +from .apis.foo.consumption.foo_filters_values_api import foo_filters_values_api + from pydantic import BaseModel, Field from typing import Optional, Any, Annotated From c60d2360726338a482e865e36417af425bea68cb Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 14:22:12 -0700 Subject: [PATCH 05/11] apis with sql string objects --- .../bar/consumption/bar_average_value_api.py | 14 +++---- .../app/apis/bar/consumption/bar_base_api.py | 12 +++--- .../app/apis/foo/consumption/foo_base_api.py | 12 +++--- .../consumption/foo_cube_aggregations_api.py | 38 +++++++++---------- .../foo/consumption/foo_filters_values_api.py | 37 +++++++++--------- .../consumption/foo_score_over_time_api.py | 23 +++++------ 6 files changed, 69 insertions(+), 67 deletions(-) diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py index 58e2854e..13a7d301 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py @@ -1,4 +1,4 @@ -from moose_lib import Api +from moose_lib import Api, MooseClient from typing import Dict, Any from pydantic import BaseModel from app.external_models import bar_model @@ -17,7 +17,7 @@ class GetBarsAverageValueResponse(BaseModel): def bar_average_value_api_handler( - context, + client: MooseClient, params: EmptyParams ) -> GetBarsAverageValueResponse: """ @@ -25,15 +25,15 @@ def bar_average_value_api_handler( """ start_time = time.time() - query = """ + query = f""" SELECT - AVG(value) as averageValue, + AVG({bar_model.columns.value}) as averageValue, COUNT(*) as count - FROM bar - WHERE value IS NOT NULL + FROM {bar_model.name} + WHERE {bar_model.columns.value} IS NOT NULL """ - results = context.query(query, {}) + results = client.query(query, {}) query_time = int((time.time() - start_time) * 1000) diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py index 671796b1..9325c9b6 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py @@ -1,4 +1,4 @@ -from moose_lib import Api +from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel, Field from app.external_models import bar, bar_model @@ -89,7 +89,7 @@ class GetBarsResponse(BaseModel): queryTime: int -def bar_consumption_api_handler(context, params: GetBarsParams) -> GetBarsResponse: +def bar_consumption_api_handler(client: MooseClient, params: GetBarsParams) -> GetBarsResponse: """ Consumption API for bar data following Moose documentation pattern """ @@ -97,8 +97,8 @@ def bar_consumption_api_handler(context, params: GetBarsParams) -> GetBarsRespon upper_sort_order = params.sort_order.upper() # Get total count - count_query = f"SELECT count() as total FROM bar" - count_result = context.query(count_query, {}) + count_query = f"SELECT count() as total FROM {bar_model.name}" + count_result = client.query(count_query, {}) total_count = count_result[0]["total"] if count_result else 0 start_time = time.time() @@ -106,13 +106,13 @@ def bar_consumption_api_handler(context, params: GetBarsParams) -> GetBarsRespon # Build dynamic query including CDC fields query = f""" SELECT * - FROM bar + FROM {bar_model.name} ORDER BY {params.sort_by} {upper_sort_order} LIMIT {params.limit} OFFSET {params.offset} """ - results = context.query(query, {}) + results = client.query(query, {}) query_time = int((time.time() - start_time) * 1000) # Convert to milliseconds diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py index 6b01d928..dbea6064 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py @@ -1,4 +1,4 @@ -from moose_lib import Api +from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel from app.external_models import foo, foo_model @@ -93,7 +93,7 @@ class GetFoosResponse(BaseModel): queryTime: int -def foo_consumption_api_handler(context, params: GetFoosParams) -> GetFoosResponse: +def foo_consumption_api_handler(client: MooseClient, params: GetFoosParams) -> GetFoosResponse: """ Consumption API for foo data following Moose documentation pattern """ @@ -101,8 +101,8 @@ def foo_consumption_api_handler(context, params: GetFoosParams) -> GetFoosRespon upper_sort_order = params.sort_order.upper() # Get total count - count_query = f"SELECT count() as total FROM foo" - count_result = context.query(count_query, {}) + count_query = f"SELECT count() as total FROM {foo_model.name}" + count_result = client.query(count_query, {}) total_count = count_result[0]["total"] if count_result else 0 start_time = time.time() @@ -110,13 +110,13 @@ def foo_consumption_api_handler(context, params: GetFoosParams) -> GetFoosRespon # Build dynamic query including CDC fields query = f""" SELECT * - FROM foo + FROM {foo_model.name} ORDER BY {params.sort_by} {upper_sort_order} LIMIT {params.limit} OFFSET {params.offset} """ - results = context.query(query, {}) + results = client.query(query, {}) query_time = int((time.time() - start_time) * 1000) # Convert to milliseconds diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py index c860c831..0bbd51d5 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py @@ -1,4 +1,4 @@ -from moose_lib import Api +from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel from app.external_models import foo_model @@ -35,7 +35,7 @@ class GetFooCubeAggregationsResponse(BaseModel): def foo_cube_aggregations_api_handler( - context, + client: MooseClient, params: GetFooCubeAggregationsParams ) -> GetFooCubeAggregationsResponse: """ @@ -52,16 +52,16 @@ def foo_cube_aggregations_api_handler( # Build optional WHERE fragments where_clauses = [ - f"toDate(created_at) >= toDate('{start_date_str}')", - f"toDate(created_at) <= toDate('{end_date_str}')", - "score IS NOT NULL" + f"toDate({foo_model.columns.created_at}) >= toDate('{start_date_str}')", + f"toDate({foo_model.columns.created_at}) <= toDate('{end_date_str}')", + f"{foo_model.columns.score} IS NOT NULL" ] if params.status: - where_clauses.append(f"status = '{params.status}'") + where_clauses.append(f"{foo_model.columns.status} = '{params.status}'") if params.priority is not None: - where_clauses.append(f"priority = {params.priority}") + where_clauses.append(f"{foo_model.columns.priority} = {params.priority}") limited = max(1, min(params.limit, 200)) paged_offset = max(0, params.offset) @@ -69,9 +69,9 @@ def foo_cube_aggregations_api_handler( # Map sort column safely sort_column = { "month": "month", - "status": "status", + "status": f"{foo_model.columns.status}", "tag": "tag", - "priority": "priority", + "priority": f"{foo_model.columns.priority}", "n": "n", "avgScore": "avgScore", "avg_score": "avgScore", @@ -87,24 +87,24 @@ def foo_cube_aggregations_api_handler( query = f""" SELECT - formatDateTime(toStartOfMonth(created_at), '%Y-%m-01') AS month, - status, - arrayJoin(tags) AS tag, - priority, + formatDateTime(toStartOfMonth({foo_model.columns.created_at}), '%Y-%m-01') AS month, + {foo_model.columns.status}, + arrayJoin({foo_model.columns.tags}) AS tag, + {foo_model.columns.priority}, count() AS n, - avg(score) AS avgScore, - quantileTDigest(0.5)(toFloat64(score)) AS p50, - quantileTDigest(0.9)(toFloat64(score)) AS p90, + avg({foo_model.columns.score}) AS avgScore, + quantileTDigest(0.5)(toFloat64({foo_model.columns.score})) AS p50, + quantileTDigest(0.9)(toFloat64({foo_model.columns.score})) AS p90, COUNT() OVER() AS total - FROM foo + FROM {foo_model.name} WHERE {where_clause} - GROUP BY month, status, tag, priority + GROUP BY month, {foo_model.columns.status}, tag, {foo_model.columns.priority} {having_clause} ORDER BY {sort_column} {sort_dir} LIMIT {limited} OFFSET {paged_offset} """ - results = context.query(query, {}) + results = client.query(query, {}) query_time = int((time.time() - start_time) * 1000) total = results[0]["total"] if results else 0 diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py index 56f840d5..77001f5c 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py @@ -1,6 +1,7 @@ -from moose_lib import Api +from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel +from app.external_models import foo_model from datetime import datetime, timedelta @@ -15,7 +16,7 @@ class GetFooFiltersValuesResponse(BaseModel): def foo_filters_values_api_handler( - context, + client: MooseClient, params: GetFooFiltersValuesParams ) -> GetFooFiltersValuesResponse: """ @@ -30,38 +31,38 @@ def foo_filters_values_api_handler( # Build queries for distinct values date_filter = f""" - toDate(created_at) >= toDate('{start_date_str}') - AND toDate(created_at) <= toDate('{end_date_str}') + toDate({foo_model.columns.created_at}) >= toDate('{start_date_str}') + AND toDate({foo_model.columns.created_at}) <= toDate('{end_date_str}') """ status_query = f""" - SELECT DISTINCT status - FROM foo + SELECT DISTINCT {foo_model.columns.status} + FROM {foo_model.name} WHERE {date_filter} - AND status IS NOT NULL - ORDER BY status + AND {foo_model.columns.status} IS NOT NULL + ORDER BY {foo_model.columns.status} """ tags_query = f""" - SELECT DISTINCT arrayJoin(tags) AS tag - FROM foo + SELECT DISTINCT arrayJoin({foo_model.columns.tags}) AS tag + FROM {foo_model.name} WHERE {date_filter} - AND tags IS NOT NULL + AND {foo_model.columns.tags} IS NOT NULL ORDER BY tag """ priorities_query = f""" - SELECT DISTINCT priority - FROM foo + SELECT DISTINCT {foo_model.columns.priority} + FROM {foo_model.name} WHERE {date_filter} - AND priority IS NOT NULL - ORDER BY priority + AND {foo_model.columns.priority} IS NOT NULL + ORDER BY {foo_model.columns.priority} """ # Execute queries - status_results = context.query(status_query, {}) - tags_results = context.query(tags_query, {}) - priorities_results = context.query(priorities_query, {}) + status_results = client.query(status_query, {}) + tags_results = client.query(tags_query, {}) + priorities_results = client.query(priorities_query, {}) # Extract values from results possible_statuses = [row["status"] for row in status_results] diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py index 4fe4a3c8..4a127040 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py @@ -1,6 +1,7 @@ -from moose_lib import Api +from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel +from app.external_models import foo_model import time from datetime import datetime, timedelta @@ -21,7 +22,7 @@ class GetFoosScoreOverTimeResponse(BaseModel): def foo_score_over_time_api_handler( - context, + client: MooseClient, params: GetFoosScoreOverTimeParams ) -> GetFoosScoreOverTimeResponse: """ @@ -38,20 +39,20 @@ def foo_score_over_time_api_handler( # Query to get daily score aggregations query = f""" SELECT - formatDateTime(toDate(created_at), '%Y-%m-%d') as date, - AVG(score) as averageScore, + formatDateTime(toDate({foo_model.columns.created_at}), '%Y-%m-%d') as date, + AVG({foo_model.columns.score}) as averageScore, COUNT(*) as totalCount - FROM foo - WHERE toDate(created_at) >= toDate('{start_date_str}') - AND toDate(created_at) <= toDate('{end_date_str}') - AND score IS NOT NULL - GROUP BY toDate(created_at) - ORDER BY toDate(created_at) ASC + FROM {foo_model.name} + WHERE toDate({foo_model.columns.created_at}) >= toDate('{start_date_str}') + AND toDate({foo_model.columns.created_at}) <= toDate('{end_date_str}') + AND {foo_model.columns.score} IS NOT NULL + GROUP BY toDate({foo_model.columns.created_at}) + ORDER BY toDate({foo_model.columns.created_at}) ASC """ start_time = time.time() - results = context.query(query, {}) + results = client.query(query, {}) query_time = int((time.time() - start_time) * 1000) From 277b27a2cf94bd16e913067ab991c0c25556ec41 Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 14:25:40 -0700 Subject: [PATCH 06/11] model to table in olaptable object naming --- .../bar/consumption/bar_average_value_api.py | 8 ++--- .../app/apis/bar/consumption/bar_base_api.py | 8 ++--- .../app/apis/foo/consumption/foo_base_api.py | 6 ++-- .../consumption/foo_cube_aggregations_api.py | 34 +++++++++---------- .../foo/consumption/foo_filters_values_api.py | 28 +++++++-------- .../consumption/foo_score_over_time_api.py | 18 +++++----- .../app/external_models.py | 8 ++--- 7 files changed, 55 insertions(+), 55 deletions(-) diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py index 13a7d301..c8a00d2a 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_average_value_api.py @@ -1,7 +1,7 @@ from moose_lib import Api, MooseClient from typing import Dict, Any from pydantic import BaseModel -from app.external_models import bar_model +from app.external_models import bar_table import time @@ -27,10 +27,10 @@ def bar_average_value_api_handler( query = f""" SELECT - AVG({bar_model.columns.value}) as averageValue, + AVG({bar_table.columns.value}) as averageValue, COUNT(*) as count - FROM {bar_model.name} - WHERE {bar_model.columns.value} IS NOT NULL + FROM {bar_table.name} + WHERE {bar_table.columns.value} IS NOT NULL """ results = client.query(query, {}) diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py index 9325c9b6..f530530b 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py @@ -1,7 +1,7 @@ from moose_lib import Api, MooseClient from typing import Optional, List -from pydantic import BaseModel, Field -from app.external_models import bar, bar_model +from pydantic import BaseModel +from app.external_models import bar, bar_table import time from datetime import datetime, timezone @@ -97,7 +97,7 @@ def bar_consumption_api_handler(client: MooseClient, params: GetBarsParams) -> G upper_sort_order = params.sort_order.upper() # Get total count - count_query = f"SELECT count() as total FROM {bar_model.name}" + count_query = f"SELECT count() as total FROM {bar_table.name}" count_result = client.query(count_query, {}) total_count = count_result[0]["total"] if count_result else 0 @@ -106,7 +106,7 @@ def bar_consumption_api_handler(client: MooseClient, params: GetBarsParams) -> G # Build dynamic query including CDC fields query = f""" SELECT * - FROM {bar_model.name} + FROM {bar_table.name} ORDER BY {params.sort_by} {upper_sort_order} LIMIT {params.limit} OFFSET {params.offset} diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py index dbea6064..d897f127 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py @@ -1,7 +1,7 @@ from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel -from app.external_models import foo, foo_model +from app.external_models import foo, foo_table import time from datetime import datetime, timezone @@ -101,7 +101,7 @@ def foo_consumption_api_handler(client: MooseClient, params: GetFoosParams) -> G upper_sort_order = params.sort_order.upper() # Get total count - count_query = f"SELECT count() as total FROM {foo_model.name}" + count_query = f"SELECT count() as total FROM {foo_table.name}" count_result = client.query(count_query, {}) total_count = count_result[0]["total"] if count_result else 0 @@ -110,7 +110,7 @@ def foo_consumption_api_handler(client: MooseClient, params: GetFoosParams) -> G # Build dynamic query including CDC fields query = f""" SELECT * - FROM {foo_model.name} + FROM {foo_table.name} ORDER BY {params.sort_by} {upper_sort_order} LIMIT {params.limit} OFFSET {params.offset} diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py index 0bbd51d5..8156fb8f 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_cube_aggregations_api.py @@ -1,7 +1,7 @@ from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel -from app.external_models import foo_model +from app.external_models import foo_table import time from datetime import datetime, timedelta @@ -52,16 +52,16 @@ def foo_cube_aggregations_api_handler( # Build optional WHERE fragments where_clauses = [ - f"toDate({foo_model.columns.created_at}) >= toDate('{start_date_str}')", - f"toDate({foo_model.columns.created_at}) <= toDate('{end_date_str}')", - f"{foo_model.columns.score} IS NOT NULL" + f"toDate({foo_table.columns.created_at}) >= toDate('{start_date_str}')", + f"toDate({foo_table.columns.created_at}) <= toDate('{end_date_str}')", + f"{foo_table.columns.score} IS NOT NULL" ] if params.status: - where_clauses.append(f"{foo_model.columns.status} = '{params.status}'") + where_clauses.append(f"{foo_table.columns.status} = '{params.status}'") if params.priority is not None: - where_clauses.append(f"{foo_model.columns.priority} = {params.priority}") + where_clauses.append(f"{foo_table.columns.priority} = {params.priority}") limited = max(1, min(params.limit, 200)) paged_offset = max(0, params.offset) @@ -69,9 +69,9 @@ def foo_cube_aggregations_api_handler( # Map sort column safely sort_column = { "month": "month", - "status": f"{foo_model.columns.status}", + "status": f"{foo_table.columns.status}", "tag": "tag", - "priority": f"{foo_model.columns.priority}", + "priority": f"{foo_table.columns.priority}", "n": "n", "avgScore": "avgScore", "avg_score": "avgScore", @@ -87,18 +87,18 @@ def foo_cube_aggregations_api_handler( query = f""" SELECT - formatDateTime(toStartOfMonth({foo_model.columns.created_at}), '%Y-%m-01') AS month, - {foo_model.columns.status}, - arrayJoin({foo_model.columns.tags}) AS tag, - {foo_model.columns.priority}, + formatDateTime(toStartOfMonth({foo_table.columns.created_at}), '%Y-%m-01') AS month, + {foo_table.columns.status}, + arrayJoin({foo_table.columns.tags}) AS tag, + {foo_table.columns.priority}, count() AS n, - avg({foo_model.columns.score}) AS avgScore, - quantileTDigest(0.5)(toFloat64({foo_model.columns.score})) AS p50, - quantileTDigest(0.9)(toFloat64({foo_model.columns.score})) AS p90, + avg({foo_table.columns.score}) AS avgScore, + quantileTDigest(0.5)(toFloat64({foo_table.columns.score})) AS p50, + quantileTDigest(0.9)(toFloat64({foo_table.columns.score})) AS p90, COUNT() OVER() AS total - FROM {foo_model.name} + FROM {foo_table.name} WHERE {where_clause} - GROUP BY month, {foo_model.columns.status}, tag, {foo_model.columns.priority} + GROUP BY month, {foo_table.columns.status}, tag, {foo_table.columns.priority} {having_clause} ORDER BY {sort_column} {sort_dir} LIMIT {limited} OFFSET {paged_offset} diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py index 77001f5c..d36fc916 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_filters_values_api.py @@ -1,7 +1,7 @@ from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel -from app.external_models import foo_model +from app.external_models import foo_table from datetime import datetime, timedelta @@ -31,32 +31,32 @@ def foo_filters_values_api_handler( # Build queries for distinct values date_filter = f""" - toDate({foo_model.columns.created_at}) >= toDate('{start_date_str}') - AND toDate({foo_model.columns.created_at}) <= toDate('{end_date_str}') + toDate({foo_table.columns.created_at}) >= toDate('{start_date_str}') + AND toDate({foo_table.columns.created_at}) <= toDate('{end_date_str}') """ status_query = f""" - SELECT DISTINCT {foo_model.columns.status} - FROM {foo_model.name} + SELECT DISTINCT {foo_table.columns.status} + FROM {foo_table.name} WHERE {date_filter} - AND {foo_model.columns.status} IS NOT NULL - ORDER BY {foo_model.columns.status} + AND {foo_table.columns.status} IS NOT NULL + ORDER BY {foo_table.columns.status} """ tags_query = f""" - SELECT DISTINCT arrayJoin({foo_model.columns.tags}) AS tag - FROM {foo_model.name} + SELECT DISTINCT arrayJoin({foo_table.columns.tags}) AS tag + FROM {foo_table.name} WHERE {date_filter} - AND {foo_model.columns.tags} IS NOT NULL + AND {foo_table.columns.tags} IS NOT NULL ORDER BY tag """ priorities_query = f""" - SELECT DISTINCT {foo_model.columns.priority} - FROM {foo_model.name} + SELECT DISTINCT {foo_table.columns.priority} + FROM {foo_table.name} WHERE {date_filter} - AND {foo_model.columns.priority} IS NOT NULL - ORDER BY {foo_model.columns.priority} + AND {foo_table.columns.priority} IS NOT NULL + ORDER BY {foo_table.columns.priority} """ # Execute queries diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py index 4a127040..9a647a33 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_score_over_time_api.py @@ -1,7 +1,7 @@ from moose_lib import Api, MooseClient from typing import Optional, List from pydantic import BaseModel -from app.external_models import foo_model +from app.external_models import foo_table import time from datetime import datetime, timedelta @@ -39,15 +39,15 @@ def foo_score_over_time_api_handler( # Query to get daily score aggregations query = f""" SELECT - formatDateTime(toDate({foo_model.columns.created_at}), '%Y-%m-%d') as date, - AVG({foo_model.columns.score}) as averageScore, + formatDateTime(toDate({foo_table.columns.created_at}), '%Y-%m-%d') as date, + AVG({foo_table.columns.score}) as averageScore, COUNT(*) as totalCount - FROM {foo_model.name} - WHERE toDate({foo_model.columns.created_at}) >= toDate('{start_date_str}') - AND toDate({foo_model.columns.created_at}) <= toDate('{end_date_str}') - AND {foo_model.columns.score} IS NOT NULL - GROUP BY toDate({foo_model.columns.created_at}) - ORDER BY toDate({foo_model.columns.created_at}) ASC + FROM {foo_table.name} + WHERE toDate({foo_table.columns.created_at}) >= toDate('{start_date_str}') + AND toDate({foo_table.columns.created_at}) <= toDate('{end_date_str}') + AND {foo_table.columns.score} IS NOT NULL + GROUP BY toDate({foo_table.columns.created_at}) + ORDER BY toDate({foo_table.columns.created_at}) ASC """ start_time = time.time() diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/external_models.py b/ufa-lite/services/analytical-moose-foobar-py/app/external_models.py index c849ca31..0c92fe8a 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/external_models.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/external_models.py @@ -60,28 +60,28 @@ class users(BaseModel): UNDERSCORE_PREFIXED_peerdb_is_deleted: Annotated[int, "int8"] = Field(alias="_peerdb_is_deleted") UNDERSCORE_PREFIXED_peerdb_version: Annotated[int, "int64"] = Field(alias="_peerdb_version") -peerdb_raw_mirror_a_4_be_3_c_5_e_1_df_3_45_e_4_805_b_cb_363_b_330_b_4_e_model = OlapTable[_peerdb_raw_mirror_a4be3c5e__1df3__45e4__805b__cb363b330b4e]("_peerdb_raw_mirror_a4be3c5e__1df3__45e4__805b__cb363b330b4e", OlapConfig( +peerdb_raw_mirror_a_4_be_3_c_5_e_1_df_3_45_e_4_805_b_cb_363_b_330_b_4_e_table = OlapTable[_peerdb_raw_mirror_a4be3c5e__1df3__45e4__805b__cb363b330b4e]("_peerdb_raw_mirror_a4be3c5e__1df3__45e4__805b__cb363b330b4e", OlapConfig( order_by_fields=["_peerdb_batch_id", "_peerdb_destination_table_name"], life_cycle=LifeCycle.EXTERNALLY_MANAGED, engine=MergeTreeEngine(), settings={"index_granularity": "8192"}, )) -bar_model = OlapTable[bar]("bar", OlapConfig( +bar_table = OlapTable[bar]("bar", OlapConfig( order_by_fields=["id"], life_cycle=LifeCycle.EXTERNALLY_MANAGED, engine=ReplacingMergeTreeEngine(ver="_peerdb_version"), settings={"index_granularity": "8192"}, )) -foo_model = OlapTable[foo]("foo", OlapConfig( +foo_table = OlapTable[foo]("foo", OlapConfig( order_by_fields=["id"], life_cycle=LifeCycle.EXTERNALLY_MANAGED, engine=ReplacingMergeTreeEngine(ver="_peerdb_version"), settings={"index_granularity": "8192"}, )) -users_model = OlapTable[users]("users", OlapConfig( +users_table = OlapTable[users]("users", OlapConfig( order_by_fields=["id"], life_cycle=LifeCycle.EXTERNALLY_MANAGED, engine=ReplacingMergeTreeEngine(ver="_peerdb_version"), From c79d53ed0c696f5c2902ca8e83d1e1a1955ff708 Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 14:47:10 -0700 Subject: [PATCH 07/11] base api simplification --- .../app/apis/bar/consumption/bar_base_api.py | 90 +--------------- .../app/apis/foo/consumption/foo_base_api.py | 101 +----------------- .../analytical-moose-foobar-py/app/main.py | 2 +- 3 files changed, 10 insertions(+), 183 deletions(-) diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py index f530530b..b52b6055 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/bar/consumption/bar_base_api.py @@ -5,77 +5,12 @@ import time from datetime import datetime, timezone - -def safe_datetime_convert(value) -> str: - """ - Safely convert a value to JavaScript-compatible ISO datetime string. - Handles various input formats from ClickHouse. - """ - if value is None or value == "": - return "1970-01-01T00:00:00.000Z" # Return epoch time for null/empty values - - if isinstance(value, datetime): - # Ensure timezone info and return ISO format with Z suffix - if value.tzinfo is None: - # Assume UTC if no timezone info - value = value.replace(tzinfo=timezone.utc) - return value.isoformat().replace('+00:00', 'Z') - - if isinstance(value, str): - # Try to parse and reformat the string - try: - # Try ISO format first - dt = datetime.fromisoformat(value.replace('Z', '+00:00')) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat().replace('+00:00', 'Z') - except ValueError: - # Try other common formats - try: - dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') - dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat().replace('+00:00', 'Z') - except ValueError: - try: - dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f') - dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat().replace('+00:00', 'Z') - except ValueError: - # Last resort - return current time in ISO format - return datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') - - # If it's some other type, try to convert to string first - try: - dt = datetime.fromisoformat(str(value).replace('Z', '+00:00')) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat().replace('+00:00', 'Z') - except (ValueError, TypeError): - # Final fallback - return epoch time - return "1970-01-01T00:00:00.000Z" - - class GetBarsParams(BaseModel): limit: Optional[int] = 10 offset: Optional[int] = 0 sort_by: Optional[str] = "created_at" sort_order: Optional[str] = "DESC" - -class BarWithCDC(BaseModel): - id: str - foo_id: str - value: int - label: Optional[str] = None - notes: Optional[str] = None - is_enabled: bool - created_at: str - updated_at: str - _peerdb_synced_at: str - _peerdb_is_deleted: int - _peerdb_version: int - - class PaginationInfo(BaseModel): limit: int offset: int @@ -84,7 +19,7 @@ class PaginationInfo(BaseModel): class GetBarsResponse(BaseModel): - data: List[BarWithCDC] + data: List[bar] pagination: PaginationInfo queryTime: int @@ -111,34 +46,17 @@ def bar_consumption_api_handler(client: MooseClient, params: GetBarsParams) -> G LIMIT {params.limit} OFFSET {params.offset} """ - + # Run the query results = client.query(query, {}) + # Calculate query time query_time = int((time.time() - start_time) * 1000) # Convert to milliseconds # Create pagination metadata has_more = params.offset + len(results) < total_count - # Convert results to response format - data = [ - BarWithCDC( - id=str(row["id"]), - foo_id=str(row["foo_id"]), - value=int(row["value"]), - label=row.get("label"), - notes=row.get("notes"), - is_enabled=bool(row["is_enabled"]), - created_at=safe_datetime_convert(row["created_at"]), - updated_at=safe_datetime_convert(row["updated_at"]), - _peerdb_synced_at=safe_datetime_convert(row["_peerdb_synced_at"]), - _peerdb_is_deleted=int(row["_peerdb_is_deleted"]), - _peerdb_version=int(row["_peerdb_version"]) - ) - for row in results - ] - return GetBarsResponse( - data=data, + data=results, pagination=PaginationInfo( limit=params.limit, offset=params.offset, diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py index d897f127..64d91a2a 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/apis/foo/consumption/foo_base_api.py @@ -3,57 +3,6 @@ from pydantic import BaseModel from app.external_models import foo, foo_table import time -from datetime import datetime, timezone - - -def safe_datetime_convert(value) -> str: - """ - Safely convert a value to JavaScript-compatible ISO datetime string. - Handles various input formats from ClickHouse. - """ - if value is None or value == "": - return "1970-01-01T00:00:00.000Z" # Return epoch time for null/empty values - - if isinstance(value, datetime): - # Ensure timezone info and return ISO format with Z suffix - if value.tzinfo is None: - # Assume UTC if no timezone info - value = value.replace(tzinfo=timezone.utc) - return value.isoformat().replace('+00:00', 'Z') - - if isinstance(value, str): - # Try to parse and reformat the string - try: - # Try ISO format first - dt = datetime.fromisoformat(value.replace('Z', '+00:00')) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat().replace('+00:00', 'Z') - except ValueError: - # Try other common formats - try: - dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S') - dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat().replace('+00:00', 'Z') - except ValueError: - try: - dt = datetime.strptime(value, '%Y-%m-%d %H:%M:%S.%f') - dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat().replace('+00:00', 'Z') - except ValueError: - # Last resort - return current time in ISO format - return datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z') - - # If it's some other type, try to convert to string first - try: - dt = datetime.fromisoformat(str(value).replace('Z', '+00:00')) - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - return dt.isoformat().replace('+00:00', 'Z') - except (ValueError, TypeError): - # Final fallback - return epoch time - return "1970-01-01T00:00:00.000Z" - class GetFoosParams(BaseModel): limit: Optional[int] = 10 @@ -61,25 +10,6 @@ class GetFoosParams(BaseModel): sort_by: Optional[str] = "created_at" sort_order: Optional[str] = "DESC" - -class FooWithCDCForConsumption(BaseModel): - id: str - name: str - description: Optional[str] = None - status: str - priority: int - is_active: bool - metadata: Optional[str] = None - tags: List[str] - score: Optional[float] = None - large_text: Optional[str] = None - created_at: str - updated_at: str - _peerdb_synced_at: str - _peerdb_is_deleted: int - _peerdb_version: int - - class PaginationInfo(BaseModel): limit: int offset: int @@ -88,7 +18,7 @@ class PaginationInfo(BaseModel): class GetFoosResponse(BaseModel): - data: List[FooWithCDCForConsumption] + data: List[foo] pagination: PaginationInfo queryTime: int @@ -115,38 +45,17 @@ def foo_consumption_api_handler(client: MooseClient, params: GetFoosParams) -> G LIMIT {params.limit} OFFSET {params.offset} """ - + # Run the query results = client.query(query, {}) + # Calculate query time query_time = int((time.time() - start_time) * 1000) # Convert to milliseconds - # Create pagination metadata (matching transactional API format) + # Create pagination metadata has_more = params.offset + len(results) < total_count - # Convert results to response format - data = [ - FooWithCDCForConsumption( - id=str(row["id"]), - name=row["name"], - description=row.get("description"), - status=str(row["status"]), # Convert status to string for consumption - priority=row["priority"], - is_active=row["is_active"], - metadata=row.get("metadata"), - tags=row["tags"] or [], - score=float(row["score"]) if row.get("score") is not None else None, - large_text=row.get("large_text"), - created_at=safe_datetime_convert(row["created_at"]), - updated_at=safe_datetime_convert(row["updated_at"]), - _peerdb_synced_at=safe_datetime_convert(row["_peerdb_synced_at"]), - _peerdb_is_deleted=row["_peerdb_is_deleted"], - _peerdb_version=row["_peerdb_version"] - ) - for row in results - ] - return GetFoosResponse( - data=data, + data=results, pagination=PaginationInfo( limit=params.limit, offset=params.offset, diff --git a/ufa-lite/services/analytical-moose-foobar-py/app/main.py b/ufa-lite/services/analytical-moose-foobar-py/app/main.py index d8759f63..54eabe85 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/app/main.py +++ b/ufa-lite/services/analytical-moose-foobar-py/app/main.py @@ -57,7 +57,7 @@ class dish(BaseModel): lowest_price: clickhouse_decimal(18, 3) highest_price: clickhouse_decimal(18, 3) -dish_model = OlapTable[dish]("dish", OlapConfig( +dish_table = OlapTable[dish]("dish", OlapConfig( order_by_fields=["id"], engine=MergeTreeEngine(), settings={"index_granularity": "8192"}, From fbe257b15fa6f55bb7b01c62ed7bf3c1d05fa53f Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 15:16:37 -0700 Subject: [PATCH 08/11] git ignore venv --- .gitignore | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8d2fe974..9c817a77 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ dist-ssr .env* .cache ./logs/ +venv/ # Ignore scratch files - used for testing and development in Data Warehouse scratch* @@ -32,7 +33,4 @@ nohup.out # Service-specific temp files services/*/temp_migration/ services/*/dev-*.log -services/*/.moose/ - -# Python virtual environments -venv/ +services/*/.moose/ \ No newline at end of file From 5e0e3862e78afa767f068f86efb6127265ce7d2c Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 15:17:43 -0700 Subject: [PATCH 09/11] admin api key --- ufa-lite/services/analytical-moose-foobar-py/moose.config.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml b/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml index 8a5ae42e..14d99fa9 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml +++ b/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml @@ -66,3 +66,6 @@ ddl_plan = false [typescript_config] package_manager = "npm" + +[authentication] +admin_api_key = "199d13d8201f7418054de884f3a955c928122ab3" \ No newline at end of file From e05e2c8c1d26165adb7ef00eab039f5679573d16 Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Sun, 14 Sep 2025 15:33:00 -0700 Subject: [PATCH 10/11] admin key --- .../services/analytical-moose-foobar-py/moose.config.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml b/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml index 14d99fa9..c2e93b90 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml +++ b/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml @@ -56,6 +56,7 @@ api_key = "" [supported_old_versions] [authentication] +admin_api_key = "199d13d8201f7418054de884f3a955c928122ab3" [features] streaming_engine = false @@ -66,6 +67,3 @@ ddl_plan = false [typescript_config] package_manager = "npm" - -[authentication] -admin_api_key = "199d13d8201f7418054de884f3a955c928122ab3" \ No newline at end of file From 35c05660ed94f3d2d04e0d70b6bddb246c78d574 Mon Sep 17 00:00:00 2001 From: Chris Crane Date: Mon, 15 Sep 2025 12:30:50 -0700 Subject: [PATCH 11/11] update ports --- .../services/analytical-moose-foobar-py/moose.config.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml b/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml index c2e93b90..5039f753 100644 --- a/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml +++ b/ufa-lite/services/analytical-moose-foobar-py/moose.config.toml @@ -17,9 +17,9 @@ native_port = 9000 [http_server_config] host = "localhost" -port = 4000 -management_port = 5001 -proxy_port = 4001 +port = 4410 +management_port = 5411 +proxy_port = 4411 max_request_body_size = 10485760 [redis_config]