diff --git a/.dockerignore b/.dockerignore index 8b3ba26..fe794c9 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,3 +25,4 @@ Makefile .* README.md *.xml +*.chdb diff --git a/.gitignore b/.gitignore index 387a29b..2262c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -148,4 +148,7 @@ docker/volumes/ volumes # Data Source -*.xml +*.xml + +# ClickHouse database +*.chdb \ No newline at end of file diff --git a/Dockerfile.ch b/Dockerfile.ch new file mode 100644 index 0000000..4141bc4 --- /dev/null +++ b/Dockerfile.ch @@ -0,0 +1,31 @@ +FROM ghcr.io/astral-sh/uv:python3.13-bookworm + +WORKDIR /app + +ENV UV_COMPILE_BYTECODE=1 +ENV UV_LINK_MODE=copy +ENV UV_TOOL_BIN_DIR=/usr/local/bin + +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync --locked --no-install-project --no-dev + +COPY . /app +RUN mv /app/xmltemp123 /app/scripts/raw.xml +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --locked --no-dev + +ENV PATH="/app/.venv/bin:$PATH" + +RUN echo '#!/bin/bash\n\ + set -e\n\ + echo "Running clickhouse importer..."\n\ + uv run --directory /app/scripts/ clickhouse_importer.py && \ + echo "Copying applehealth.chdb to volume..." && \ + cp -r /app/scripts/applehealth.chdb /volume/applehealth.chdb && \ + echo "Complete!"' > /app/entrypoint.sh + +RUN chmod +x /app/entrypoint.sh + +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/Makefile b/Makefile index f6001ae..a5e6c41 100644 --- a/Makefile +++ b/Makefile @@ -41,5 +41,17 @@ es: ## Run Elasticsearch and import Apple Health XML data into ES for Apple Hea ./scripts/run_elasticsearch.sh $(UV) python scripts/xml2es.py +ch: ## Import Apple Health XML data into a docker volume for ClickHouse + $(UV) scripts/clickhouse_importer.py + +chwin: ## Import Apple Health XML data into a docker volume for ClickHouse (for Windows users) + move *.xml xmltemp123 + docker volume create applehealth-data + docker build . --file Dockerfile.ch -t uvcopier + docker run --rm -v applehealth-data:/volume uvcopier + docker run --rm -v applehealth-data:/source -v $pwd/:/dest alpine cp -r /source/applehealth.chdb /dest/ + move xmltemp123 raw.xml + docker volume rm applehealth-data + downgrade: ## Revert the last migration $(ALEMBIC_CMD) downgrade -1 diff --git a/README.md b/README.md index 03cd9fb..ab1245f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ - [🚀 Getting Started](#-getting-started) - [📝 Usage](#-usage) - [🔧 Configuration](#-configuration) -- [🐳 Docker Setup](#-docker-setup) +- [🐳 Docker Setup](#-docker-mcp) - [🛠️ MCP Tools](#️-mcp-tools) - [🗺️ Roadmap](#️-roadmap) - [👥 Contributors](#-contributors) @@ -29,14 +29,14 @@ ## 🔍 About The Project -**Apple Health MCP Server** implements a Model Context Protocol (MCP) server designed for seamless interaction between LLM-based agents and Apple Health data. It provides a standardized interface for querying, analyzing, and managing Apple Health records—imported from XML exports and indexed in Elasticsearch—through a comprehensive suite of tools. These tools are accessible from MCP-compatible clients (such as Claude Desktop), enabling users to explore, search, and analyze personal health data using natural-language prompts and advanced filtering, all without requiring direct knowledge of the underlying data formats or Elasticsearch queries. +**Apple Health MCP Server** implements a Model Context Protocol (MCP) server designed for seamless interaction between LLM-based agents and Apple Health data. It provides a standardized interface for querying, analyzing, and managing Apple Health records—imported from XML exports and indexed in Elasticsearch or Clickhouse—through a comprehensive suite of tools. These tools are accessible from MCP-compatible clients (such as Claude Desktop), enabling users to explore, search, and analyze personal health data using natural-language prompts and advanced filtering, all without requiring direct knowledge of the underlying data formats or Elasticsearch/ClickHouse queries. ### ✨ Key Features - **🚀 FastMCP Framework**: Built on FastMCP for high-performance MCP server capabilities - **🍏 Apple Health Data Management**: Import, parse, and analyze Apple Health XML exports - **🔎 Powerful Search & Filtering**: Query and filter health records using natural language and advanced parameters -- **📦 Elasticsearch Integration**: Index and search health data efficiently at scale +- **📦 Elasticsearch and ClickHouse Integration**: Index and search health data efficiently at scale - **🛠️ Modular MCP Tools**: Tools for structure analysis, record search, type-based extraction, and more - **📈 Data Summaries & Trends**: Generate statistics and trend analyses from your health data - **🐳 Container Ready**: Docker support for easy deployment and scaling @@ -49,6 +49,7 @@ The Apple Health MCP Server is built with a modular, extensible architecture des - **MCP Tools**: Dedicated tools for Apple Health XML structure analysis, record search, type-based extraction, and statistics/trend generation. Each tool is accessible via the MCP protocol for natural language and programmatic access. - **XML Import & Parsing**: Efficient streaming and parsing of large Apple Health XML exports, extracting records, workouts, and metadata for further analysis. - **Elasticsearch Backend**: All health records are indexed in Elasticsearch, enabling fast, scalable search, filtering, and aggregation across large datasets. +- **ClickHouse Backend**: Health records can also be indexed to a ClickHouse database, making the deployment easier for the enduser by using an in-memory database instead of a server-based approach. - **Service Layer**: Business logic for XML and Elasticsearch operations is encapsulated in dedicated service modules, ensuring separation of concerns and easy extensibility. - **FastMCP Framework**: Provides the MCP server interface, routing, and tool registration, making the system compatible with LLM-based agents and MCP clients (e.g., Claude Desktop). - **Configuration & Deployment**: Environment-based configuration and Docker support for easy setup and deployment in various environments. @@ -96,6 +97,10 @@ Follow these steps to set up Apple Health MCP Server in your environment. ```sh uv run python scripts/xml2es.py --delete-all ``` +3. If you choose to use ClickHouse instead of Elasticsearch: + - Run `make ch` to create a database with your exported XML data + - **Note: If you are using Windows, Docker is the only way to integrate ClickHouse into this MCP Server.** + - On Windows: Run `mingw32-make chwin` (or any other version of `make` available on Windows) ### Configuration Files @@ -126,6 +131,8 @@ You can run the MCP Server in your LLM Client in two ways: "type=bind,source=/app,target=/root_project/app", // optional "--mount", "type=bind,source=/config/.env,target=/root_project/config/.env", + "--mount", // optional - only include this if you use clickhouse + "type=bind,source=/applehealth.chdb,target=/root_project/applehealth.chdb", // optional "-e", "ES_HOST=host.docker.internal", "mcp-server:latest" @@ -192,6 +199,9 @@ After completing the above steps, restart your MCP Client to apply the changes. | ES_USER | Elasticsearch username | `elastic` | ❌ | | ES_PASSWORD | Elasticsearch password | `elastic` | ❌ | | ES_INDEX | Elasticsearch index name | `apple_health_data` | ❌ | +| CH_DB_NAME | ClickHouse database name | `applehealth` | ❌ | +| CH_TABLE_NAME | ClickHouse table name | `data` | ❌ | +| CHUNK_SIZE | Number of records indexed into CH at once | `10000` | ❌ | | XML_SAMPLE_SIZE | Number of XML records to sample | `1000` | ❌ |

(back to top)

@@ -199,7 +209,7 @@ After completing the above steps, restart your MCP Client to apply the changes. ## 🛠️ MCP Tools -The Apple Health MCP Server provides a suite of tools for exploring, searching, and analyzing your Apple Health data, both at the raw XML level and in Elasticsearch: +The Apple Health MCP Server provides a suite of tools for exploring, searching, and analyzing your Apple Health data, both at the raw XML level and in Elasticsearch/ClickHouse: ### XML Tools (`xml_reader`) @@ -218,6 +228,15 @@ The Apple Health MCP Server provides a suite of tools for exploring, searching, | `get_statistics_by_type_es` | Get comprehensive statistics (count, min, max, avg, sum) for a specific health record type. | | `get_trend_data_es` | Analyze trends for a health record type over time (daily, weekly, monthly, yearly aggregations). | +### ClickHouse Tools (`ch_reader`) + +| Tool | Description | +|-----------------------------|-----------------------------------------------------------------------------------------------------| +| `get_health_summary_ch` | Get a summary of all Apple Health data in ClickHouse (total count, type breakdown, etc.). | +| `search_health_records_ch` | Flexible search for health records in ClickHouse with advanced filtering and query options. | +| `get_statistics_by_type_ch` | Get comprehensive statistics (count, min, max, avg, sum) for a specific health record type. | +| `get_trend_data_ch` | Analyze trends for a health record type over time (daily, weekly, monthly, yearly aggregations). | + All tools are accessible via MCP-compatible clients and can be used with natural language or programmatic queries to explore and analyze your Apple Health data.

(back to top)

@@ -250,4 +269,4 @@ Distributed under the MIT License. See [MIT License](LICENSE) for more informati

Built with ❤️ by Momentum • Transforming healthcare data management with AI

-
\ No newline at end of file + diff --git a/app/config.py b/app/config.py index 238867e..37e6f93 100644 --- a/app/config.py +++ b/app/config.py @@ -28,6 +28,11 @@ class Settings(BaseSettings): ES_PASSWORD: SecretStr = SecretStr("elastic") ES_INDEX: str = "apple_health_data" + CH_DIRNAME: str = "applehealth.chdb" + CH_DB_NAME: str = "applehealth" + CH_TABLE_NAME: str = "data" + CHUNK_SIZE: int = 10_000 + RAW_XML_PATH: str = "raw.xml" XML_SAMPLE_SIZE: int = 1000 diff --git a/app/mcp/v1/mcp.py b/app/mcp/v1/mcp.py index 74f715e..a082138 100644 --- a/app/mcp/v1/mcp.py +++ b/app/mcp/v1/mcp.py @@ -1,8 +1,9 @@ from fastmcp import FastMCP -from app.mcp.v1.tools import es_reader, xml_reader +from app.mcp.v1.tools import es_reader, xml_reader, ch_reader mcp_router = FastMCP(name="Main MCP") mcp_router.mount(es_reader.es_reader_router) mcp_router.mount(xml_reader.xml_reader_router) +mcp_router.mount(ch_reader.ch_reader_router) diff --git a/app/mcp/v1/tools/ch_reader.py b/app/mcp/v1/tools/ch_reader.py new file mode 100644 index 0000000..80eb35b --- /dev/null +++ b/app/mcp/v1/tools/ch_reader.py @@ -0,0 +1,125 @@ +from typing import Any +from fastmcp import FastMCP + +from app.schemas.record import RecordType, IntervalType, HealthRecordSearchParams +from app.services.health.clickhouse import ( + get_health_summary_from_ch, + search_health_records_from_ch, + get_statistics_by_type_from_ch, + get_trend_data_from_ch, +) + +ch_reader_router = FastMCP(name="CH Reader MCP") + +@ch_reader_router.tool +def get_health_summary_ch() -> dict[str, Any]: + """ + Get a summary of Apple Health data from ClickHouse. + The function returns total record count, record type breakdown, and (optionally) a date range aggregation. + + Notes for LLM: + - IMPORTANT - Do not guess, autofill, or assume any missing data. + - When asked for medical advice, try to use my data from ClickHouse first. + """ + try: + return get_health_summary_from_ch() + except Exception as e: + return {'error': str(e)} + +@ch_reader_router.tool +def search_health_records_ch(params: HealthRecordSearchParams) -> dict[str, Any]: + """ + Search health records in ClickHouse with flexible query building. + + Parameters: + - params: HealthRecordSearchParams object containing all search/filter parameters. + + Notes for LLMs: + - This function should return a list of health record documents (dicts) matching the search criteria. + - Each document in the list should represent a single health record as stored in ClickHouse. + - If an error occurs, the function should return a list with a single dict containing an 'error' key and the error message. + - Use this to retrieve structured health data for further analysis, filtering, or display. + - Example source_name: "Rob’s iPhone", "Polar Flow", "Sync Solver". + - Example date_from/date_to: "2020-01-01T00:00:00+00:00" + - Example value_min/value_max: "10", "100.5" + - IMPORTANT - Do not guess, autofill, or assume any missing data. + - When asked for medical advice, try to use my data from ClickHouse first. + """ + try: + return search_health_records_from_ch(params) + except Exception as e: + return {'error': str(e)} + +@ch_reader_router.tool +def get_statistics_by_type_ch(record_type: RecordType | str) -> dict[str, Any]: + """ + Get comprehensive statistics for a specific health record type from ClickHouse. + + Parameters: + - record_type: The type of health record to analyze. Use RecordType for most frequent types. Use str if that type is beyond RecordType scope. + + Returns: + - record_type: The analyzed record type + - total_count: Total number of records of this type in the index + - value_statistics: Statistical summary of the 'value' field including: + * count: Number of records with values + * min: Minimum value recorded + * max: Maximum value recorded + * avg: Average value across all records + * sum: Sum of all values + - sources: Breakdown of records by source device/app (e.g., "Rob's iPhone", "Polar Flow") + + Notes for LLMs: + - This function provides comprehensive statistical analysis for any health record type. + - The value_statistics object contains all basic statistics (count, min, max, avg, sum) for the 'value' field. + - The sources breakdown shows which devices/apps contributed data for this record type. + - Example types: "HKQuantityTypeIdentifierStepCount", "HKQuantityTypeIdentifierBodyMassIndex", "HKQuantityTypeIdentifierHeartRate", etc. + - Use this function to understand the distribution, range, and trends of specific health metrics. + - The function is useful for health analysis, identifying outliers, and understanding data quality. + - date_range key for query is commented, since it contained hardcoded from date, but you can use it anyway if you replace startDate with your data. + - IMPORTANT - Do not guess, autofill, or assume any missing data. + - When asked for medical advice, try to use my data from ClickHouse first. + """ + try: + return get_statistics_by_type_from_ch(record_type) + except Exception as e: + return {"error": f"Failed to get statistics: {str(e)}"} + + +@ch_reader_router.tool +def get_trend_data_ch( + record_type: RecordType | str, + interval: IntervalType = "month", + date_from: str | None = None, + date_to: str | None = None, +) -> dict[str, Any]: + """ + Get trend data for a specific health record type over time using ClickHouse date histogram aggregation. + + Parameters: + - record_type: The type of health record to analyze (e.g., "HKQuantityTypeIdentifierStepCount") + - interval: Time interval for aggregation. + - date_from, date_to: Optional ISO8601 date strings for filtering date range + + Returns: + - record_type: The analyzed record type + - interval: The time interval used + - trend_data: List of time buckets with statistics for each period: + * date: The time period (ISO string) + * avg_value: Average value for the period + * min_value: Minimum value for the period + * max_value: Maximum value for the period + * count: Number of records in the period + + Notes for LLMs: + - Use this to analyze trends, patterns, and seasonal variations in health data + - The function automatically handles date filtering if date_from/date_to are provided + - IMPORTANT - interval must be one of: "day", "week", "month", or "year". Do not use other values. + - Do not guess, autofill, or assume any missing data. + - When asked for medical advice, try to use my data from ClickHouse first. + """ + try: + return get_trend_data_from_ch(record_type, interval, date_from, date_to) + except Exception as e: + return {"error": f"Failed to get trend data: {str(e)}"} + diff --git a/app/mcp/v1/tools/xml_reader.py b/app/mcp/v1/tools/xml_reader.py index 19a59ab..27e8100 100644 --- a/app/mcp/v1/tools/xml_reader.py +++ b/app/mcp/v1/tools/xml_reader.py @@ -3,7 +3,7 @@ from fastmcp import FastMCP from app.schemas.record import RecordType -from app.services.health.xml import analyze_xml_structure, search_xml, get_records_by_type +from app.services.health.direct_xml import analyze_xml_structure, search_xml, get_records_by_type xml_reader_router = FastMCP(name="XML Reader MCP") diff --git a/app/services/ch.py b/app/services/ch.py new file mode 100644 index 0000000..b8665f4 --- /dev/null +++ b/app/services/ch.py @@ -0,0 +1,35 @@ +import json +from dataclasses import dataclass +from json import JSONDecodeError +from pathlib import Path +from typing import Any + +import chdb + +from app.config import settings + + +@dataclass +class CHClient: + def __init__(self): + self.session = chdb.session.Session(settings.CH_DIRNAME) + self.db_name: str = settings.CH_DB_NAME + self.table_name: str = settings.CH_TABLE_NAME + self.path: Path = Path(settings.RAW_XML_PATH) + + def __post_init__(self): + if not self.path.exists(): + raise FileNotFoundError(f"XML file not found: {self.path}") + self.session.query(f"CREATE DATABASE IF NOT EXISTS {self.db_name}") + + def inquire(self, query: str) -> dict[str, Any]: + """ + Makes an SQL query to the database + :return: result of the query + """ + # first call to json.loads() only returns a string, and the second one a dict + response: str = json.dumps(str(self.session.query(query, fmt='JSON'))) + try: + return json.loads(json.loads(response)) + except JSONDecodeError as e: + return {'error': str(e)} \ No newline at end of file diff --git a/app/services/health/clickhouse.py b/app/services/health/clickhouse.py new file mode 100644 index 0000000..677198c --- /dev/null +++ b/app/services/health/clickhouse.py @@ -0,0 +1,63 @@ +from time import time +from typing import Any + +from app.services.ch import CHClient +from app.schemas.record import RecordType, IntervalType, HealthRecordSearchParams + + +ch = CHClient() + +def build_value_range(valuemin: str | None, valuemax: str | None) -> str | None: + if valuemax and valuemin: + return f"value >= '{valuemin}' and value <= '{valuemax}'" + if valuemin: + return f"value >= '{valuemin}'" + if valuemax: + return f"value <= '{valuemax}'" + return None + + +def fill_query(params: HealthRecordSearchParams) -> str: + conditions = [] + + query = f"SELECT * FROM {ch.db_name}.{ch.table_name} WHERE 1=1" + if params.record_type: + conditions.append(f" type = '{params.record_type}'") + if params.source_name: + conditions.append(f" source_name = '{params.source_name}'") + if params.date_from or params.date_to: + conditions.append(build_value_range(params.date_from, params.date_to)) + if params.value_min or params.value_max: + conditions.append(build_value_range(params.value_min, params.value_max)) + + if conditions: + query += " AND " + " AND ".join(conditions) + query += f'LIMIT {params.limit}' + return query + + +def get_health_summary_from_ch() -> dict[str, Any]: + return ch.inquire(f"SELECT type, COUNT(*) FROM {ch.db_name}.{ch.table_name} GROUP BY type") + + +def search_health_records_from_ch(params: HealthRecordSearchParams) -> dict[str, Any]: + query: str = fill_query(params) + return ch.inquire(query) + + +def get_statistics_by_type_from_ch(record_type: RecordType | str) -> dict[str, Any]: + return ch.inquire(f"SELECT type, COUNT(*), AVG(numerical), SUM(numerical), MIN(numerical), MAX(numerical) FROM {ch.db_name}.{ch.table_name} WHERE type = '{record_type}' GROUP BY type") + + +def get_trend_data_from_ch( + record_type: RecordType | str, + interval: IntervalType = "month", + date_from: str | None = None, + date_to: str | None = None, +) -> dict[str, Any]: + return ch.inquire(f""" + SELECT toStartOfInterval(startDate, INTERVAL 1 {interval}) AS interval, + AVG(numerical), MIN(numerical), MAX(numerical), COUNT(*) FROM {ch.db_name}.{ch.table_name} + WHERE type = '{record_type}' {f"AND startDate >= '{date_from}'" if date_from else ''} {f"AND startDate <= '{date_to}'" if date_to else ''} + GROUP BY interval ORDER BY interval ASC + """) diff --git a/app/services/health/xml.py b/app/services/health/direct_xml.py similarity index 100% rename from app/services/health/xml.py rename to app/services/health/direct_xml.py diff --git a/config/.env.example b/config/.env.example index 493be53..2cd5e11 100644 --- a/config/.env.example +++ b/config/.env.example @@ -1,4 +1,8 @@ ES_USER="elastic" ES_PASSWORD="elastic" ES_HOST="localhost" +CH_DIRNAME="applehealth.chdb" +CH_DB_NAME="applehealth" +CH_TABLE_NAME="data" +CHUNK_SIZE="10000" RAW_XML_PATH="raw.xml" diff --git a/pyproject.toml b/pyproject.toml index 9673b79..a2b2230 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,13 @@ version = "0.1.0" description = "MCP server for Apple Health data" requires-python = ">=3.13" dependencies = [ + "chdb>=3.5.0", "cryptography>=45", "elasticsearch>=9.0", "fastmcp>=2.10", "greenlet>=3.2", "httpx>=0.28", + "pandas>=2.3.2", "passlib>=1.7", "pydantic>=2.11", "pydantic-settings>=2.10", diff --git a/scripts/clickhouse_importer.py b/scripts/clickhouse_importer.py new file mode 100644 index 0000000..495b57c --- /dev/null +++ b/scripts/clickhouse_importer.py @@ -0,0 +1,65 @@ +from sys import stderr + +from app.services.ch import CHClient +from scripts.xml_exporter import XMLExporter + +class CHIndexer(XMLExporter, CHClient): + def __init__(self): + XMLExporter.__init__(self) + CHClient.__init__(self) + self.session.query(f"CREATE DATABASE IF NOT EXISTS {self.db_name}") + + def create_table(self) -> None: + """ + Create a new table for exported xml health data + """ + self.session.query(f""" + CREATE TABLE IF NOT EXISTS {self.db_name}.{self.table_name} + ( + type String, + sourceVersion String, + sourceName String, + device String, + startDate DateTime, + endDate DateTime, + creationDate DateTime, + unit String, + value String, + numerical Float32 + ) + ENGINE = MergeTree + ORDER BY startDate + """) + + + def index_data(self) -> bool: + for docs in self.parse_xml(): + try: + self.session.query(f""" + INSERT INTO {self.db_name}.{self.table_name} + SELECT * + FROM Python(docs) + """) + except RuntimeError as e: + print(f"Failed to insert {len(docs)} records") + print(e, file=stderr) + return False + return True + + def run(self) -> bool: + """ + Creates a new table in the database and populates it with data from the XML file provided + """ + self.create_table() + print(f"Created table {self.db_name}.{self.table_name}") + result: bool = self.index_data() + if result: + print("Inserted data into chdb correctly") + return True + else: + print("Error during data indexing") + return False + +if __name__ == "__main__": + ch = CHIndexer() + ch.run() \ No newline at end of file diff --git a/scripts/xml_exporter.py b/scripts/xml_exporter.py new file mode 100644 index 0000000..33fdbae --- /dev/null +++ b/scripts/xml_exporter.py @@ -0,0 +1,82 @@ +from datetime import datetime +from pathlib import Path +from typing import Any, Generator +from xml.etree import ElementTree as ET + +from pandas import DataFrame + +from app.config import settings + +class XMLExporter: + def __init__(self): + self.path: Path = Path(settings.RAW_XML_PATH) + self.chunk_size: int = settings.CHUNK_SIZE + + DATE_FIELDS: tuple[str] = ("startDate", "endDate", "creationDate") + DEFAULT_VALUES: dict[str, str] = { + "unit": "unknown", + "sourceVersion": "unknown", + "device": "unknown", + "value": "unknown", + } + COLUMN_NAMES: tuple[str, ...] = ( + "type", + "sourceVersion", + "sourceName", + "device", + "startDate", + "endDate", + "creationDate", + "unit", + "value", + "numerical", + ) + + def update_record(self, document: dict[str, Any]) -> dict[str, Any]: + """ + Updates records to fill out columns without specified data: + There are 9 columns that need to be filled out, and there are 4 columns + that are optional and aren't filled out in every record + """ + for field in self.DATE_FIELDS: + document[field] = datetime.strptime( + document[field], '%Y-%m-%d %H:%M:%S %z' + ) + + if len(document) != 9: + document.update( + {k: v for k, v in self.DEFAULT_VALUES.items() if k not in document} + ) + + # making sure there are value field with text values + # and numerical which always contain numbers for the sake + # of aggregation in clickhouse + try: + val = float(document['value']) + document['numerical'] = val + except (TypeError, ValueError): + document['numerical'] = 0.0 + + return document + + def parse_xml(self) -> Generator[DataFrame, Any, None]: + """ + Parses the XML file and yields pandas dataframes of specified chunk_size. + Extracts attributes from each Record element. + """ + records: list[dict[str, Any]] = [] + + for event, elem in ET.iterparse(self.path, events=("start",)): + if elem.tag == "Record" and event == "start": + if len(records) >= self.chunk_size: + yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) + records = [] + record: dict[str, Any] = elem.attrib.copy() + + # fill out empty cells if they exist and convert dates to datetime + self.update_record(record) + records.append(record) + elem.clear() + + # yield remaining records + yield DataFrame(records).reindex(columns=self.COLUMN_NAMES) \ No newline at end of file diff --git a/uv.lock b/uv.lock index db7b3b3..8b77b04 100644 --- a/uv.lock +++ b/uv.lock @@ -29,11 +29,13 @@ name = "apple-health-mcp-server" version = "0.1.0" source = { editable = "." } dependencies = [ + { name = "chdb" }, { name = "cryptography" }, { name = "elasticsearch" }, { name = "fastmcp" }, { name = "greenlet" }, { name = "httpx" }, + { name = "pandas" }, { name = "passlib" }, { name = "pydantic" }, { name = "pydantic-settings" }, @@ -55,11 +57,13 @@ lint = [ [package.metadata] requires-dist = [ + { name = "chdb", specifier = ">=3.5.0" }, { name = "cryptography", specifier = ">=45" }, { name = "elasticsearch", specifier = ">=9.0" }, { name = "fastmcp", specifier = ">=2.10" }, { name = "greenlet", specifier = ">=3.2" }, { name = "httpx", specifier = ">=0.28" }, + { name = "pandas", specifier = ">=2.3.2" }, { name = "passlib", specifier = ">=1.7" }, { name = "pydantic", specifier = ">=2.11" }, { name = "pydantic-settings", specifier = ">=2.10" }, @@ -162,6 +166,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "chdb" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pandas" }, + { name = "pyarrow" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/ca/7f271935686ade90f123eda64e121074f0ddf179c0282f3edadbd1c4bf73/chdb-3.5.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:a997db7d0dd9f97313e6819848e5e3b84b6561714bc7f2322a29bdf94d8bda26", size = 95109036, upload-time = "2025-07-28T08:55:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/fa/83/e476537e3af8170846eda145e5e04657293a72d5d516bf0616ce4a7671ae/chdb-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2575788c4a0d9d610c4336e5786bea9003f60236792168d46f7970985454b2f3", size = 86613387, upload-time = "2025-07-28T04:56:14.248Z" }, + { url = "https://files.pythonhosted.org/packages/d6/83/db96538d4f4e22403246e580de212877b16fe4a2d34625380d9f4b3659bf/chdb-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bf4016dddd858790b9150774d9a7423103c288938b67da1ba9d3451e98f8623", size = 122351968, upload-time = "2025-07-28T05:02:51.322Z" }, + { url = "https://files.pythonhosted.org/packages/05/3e/43e5c1de8d76025ec788e2fd6e71f8a1ec6c966f977a3d1c05b01b82b4d2/chdb-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f0bdf5472f6369935a5814ab5b12eea7524c55f2fe40eb8c0fe5165018f7672", size = 180469409, upload-time = "2025-07-28T05:12:30.546Z" }, +] + [[package]] name = "click" version = "8.2.1" @@ -255,7 +274,7 @@ version = "3.22.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, - { name = "docstring-parser", marker = "python_full_version < '4.0'" }, + { name = "docstring-parser", marker = "python_full_version < '4'" }, { name = "rich" }, { name = "rich-rst" }, ] @@ -569,6 +588,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] +[[package]] +name = "numpy" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/c0/c6bb172c916b00700ed3bf71cb56175fd1f7dbecebf8353545d0b5519f6c/numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3", size = 20949074, upload-time = "2025-07-24T20:43:07.813Z" }, + { url = "https://files.pythonhosted.org/packages/20/4e/c116466d22acaf4573e58421c956c6076dc526e24a6be0903219775d862e/numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b", size = 14177311, upload-time = "2025-07-24T20:43:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/78/45/d4698c182895af189c463fc91d70805d455a227261d950e4e0f1310c2550/numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6", size = 5106022, upload-time = "2025-07-24T20:43:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/9f/76/3e6880fef4420179309dba72a8c11f6166c431cf6dee54c577af8906f914/numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089", size = 6640135, upload-time = "2025-07-24T20:43:49.28Z" }, + { url = "https://files.pythonhosted.org/packages/34/fa/87ff7f25b3c4ce9085a62554460b7db686fef1e0207e8977795c7b7d7ba1/numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2", size = 14278147, upload-time = "2025-07-24T20:44:10.328Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/571b2c7a3833ae419fe69ff7b479a78d313581785203cc70a8db90121b9a/numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f", size = 16635989, upload-time = "2025-07-24T20:44:34.88Z" }, + { url = "https://files.pythonhosted.org/packages/24/5a/84ae8dca9c9a4c592fe11340b36a86ffa9fd3e40513198daf8a97839345c/numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee", size = 16053052, upload-time = "2025-07-24T20:44:58.872Z" }, + { url = "https://files.pythonhosted.org/packages/57/7c/e5725d99a9133b9813fcf148d3f858df98511686e853169dbaf63aec6097/numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6", size = 18577955, upload-time = "2025-07-24T20:45:26.714Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7c546fcf42145f29b71e4d6f429e96d8d68e5a7ba1830b2e68d7418f0bbd/numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b", size = 6311843, upload-time = "2025-07-24T20:49:24.444Z" }, + { url = "https://files.pythonhosted.org/packages/aa/6f/a428fd1cb7ed39b4280d057720fed5121b0d7754fd2a9768640160f5517b/numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56", size = 12782876, upload-time = "2025-07-24T20:49:43.227Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/4ea455c9040a12595fb6c43f2c217257c7b52dd0ba332c6a6c1d28b289fe/numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2", size = 10192786, upload-time = "2025-07-24T20:49:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/80/23/8278f40282d10c3f258ec3ff1b103d4994bcad78b0cba9208317f6bb73da/numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab", size = 21047395, upload-time = "2025-07-24T20:45:58.821Z" }, + { url = "https://files.pythonhosted.org/packages/1f/2d/624f2ce4a5df52628b4ccd16a4f9437b37c35f4f8a50d00e962aae6efd7a/numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2", size = 14300374, upload-time = "2025-07-24T20:46:20.207Z" }, + { url = "https://files.pythonhosted.org/packages/f6/62/ff1e512cdbb829b80a6bd08318a58698867bca0ca2499d101b4af063ee97/numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a", size = 5228864, upload-time = "2025-07-24T20:46:30.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8e/74bc18078fff03192d4032cfa99d5a5ca937807136d6f5790ce07ca53515/numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286", size = 6737533, upload-time = "2025-07-24T20:46:46.111Z" }, + { url = "https://files.pythonhosted.org/packages/19/ea/0731efe2c9073ccca5698ef6a8c3667c4cf4eea53fcdcd0b50140aba03bc/numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8", size = 14352007, upload-time = "2025-07-24T20:47:07.1Z" }, + { url = "https://files.pythonhosted.org/packages/cf/90/36be0865f16dfed20f4bc7f75235b963d5939707d4b591f086777412ff7b/numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a", size = 16701914, upload-time = "2025-07-24T20:47:32.459Z" }, + { url = "https://files.pythonhosted.org/packages/94/30/06cd055e24cb6c38e5989a9e747042b4e723535758e6153f11afea88c01b/numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91", size = 16132708, upload-time = "2025-07-24T20:47:58.129Z" }, + { url = "https://files.pythonhosted.org/packages/9a/14/ecede608ea73e58267fd7cb78f42341b3b37ba576e778a1a06baffbe585c/numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5", size = 18651678, upload-time = "2025-07-24T20:48:25.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/f3/2fe6066b8d07c3685509bc24d56386534c008b462a488b7f503ba82b8923/numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5", size = 6441832, upload-time = "2025-07-24T20:48:37.181Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/0937d66d05204d8f28630c9c60bc3eda68824abde4cf756c4d6aad03b0c6/numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450", size = 12927049, upload-time = "2025-07-24T20:48:56.24Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ed/13542dd59c104d5e654dfa2ac282c199ba64846a74c2c4bcdbc3a0f75df1/numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a", size = 10262935, upload-time = "2025-07-24T20:49:13.136Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7c/7659048aaf498f7611b783e000c7268fcc4dcf0ce21cd10aad7b2e8f9591/numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a", size = 20950906, upload-time = "2025-07-24T20:50:30.346Z" }, + { url = "https://files.pythonhosted.org/packages/80/db/984bea9d4ddf7112a04cfdfb22b1050af5757864cfffe8e09e44b7f11a10/numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b", size = 14185607, upload-time = "2025-07-24T20:50:51.923Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/b3d6f414f4eca568f469ac112a3b510938d892bc5a6c190cb883af080b77/numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125", size = 5114110, upload-time = "2025-07-24T20:51:01.041Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d2/6f5e6826abd6bca52392ed88fe44a4b52aacb60567ac3bc86c67834c3a56/numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19", size = 6642050, upload-time = "2025-07-24T20:51:11.64Z" }, + { url = "https://files.pythonhosted.org/packages/c4/43/f12b2ade99199e39c73ad182f103f9d9791f48d885c600c8e05927865baf/numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f", size = 14296292, upload-time = "2025-07-24T20:51:33.488Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f9/77c07d94bf110a916b17210fac38680ed8734c236bfed9982fd8524a7b47/numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5", size = 16638913, upload-time = "2025-07-24T20:51:58.517Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d1/9d9f2c8ea399cc05cfff8a7437453bd4e7d894373a93cdc46361bbb49a7d/numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58", size = 16071180, upload-time = "2025-07-24T20:52:22.827Z" }, + { url = "https://files.pythonhosted.org/packages/4c/41/82e2c68aff2a0c9bf315e47d61951099fed65d8cb2c8d9dc388cb87e947e/numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0", size = 18576809, upload-time = "2025-07-24T20:52:51.015Z" }, + { url = "https://files.pythonhosted.org/packages/14/14/4b4fd3efb0837ed252d0f583c5c35a75121038a8c4e065f2c259be06d2d8/numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2", size = 6366410, upload-time = "2025-07-24T20:56:44.949Z" }, + { url = "https://files.pythonhosted.org/packages/11/9e/b4c24a6b8467b61aced5c8dc7dcfce23621baa2e17f661edb2444a418040/numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b", size = 12918821, upload-time = "2025-07-24T20:57:06.479Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0f/0dc44007c70b1007c1cef86b06986a3812dd7106d8f946c09cfa75782556/numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910", size = 10477303, upload-time = "2025-07-24T20:57:22.879Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3e/075752b79140b78ddfc9c0a1634d234cfdbc6f9bbbfa6b7504e445ad7d19/numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e", size = 21047524, upload-time = "2025-07-24T20:53:22.086Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6d/60e8247564a72426570d0e0ea1151b95ce5bd2f1597bb878a18d32aec855/numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45", size = 14300519, upload-time = "2025-07-24T20:53:44.053Z" }, + { url = "https://files.pythonhosted.org/packages/4d/73/d8326c442cd428d47a067070c3ac6cc3b651a6e53613a1668342a12d4479/numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b", size = 5228972, upload-time = "2025-07-24T20:53:53.81Z" }, + { url = "https://files.pythonhosted.org/packages/34/2e/e71b2d6dad075271e7079db776196829019b90ce3ece5c69639e4f6fdc44/numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2", size = 6737439, upload-time = "2025-07-24T20:54:04.742Z" }, + { url = "https://files.pythonhosted.org/packages/15/b0/d004bcd56c2c5e0500ffc65385eb6d569ffd3363cb5e593ae742749b2daa/numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0", size = 14352479, upload-time = "2025-07-24T20:54:25.819Z" }, + { url = "https://files.pythonhosted.org/packages/11/e3/285142fcff8721e0c99b51686426165059874c150ea9ab898e12a492e291/numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0", size = 16702805, upload-time = "2025-07-24T20:54:50.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/c3/33b56b0e47e604af2c7cd065edca892d180f5899599b76830652875249a3/numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2", size = 16133830, upload-time = "2025-07-24T20:55:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ae/7b1476a1f4d6a48bc669b8deb09939c56dd2a439db1ab03017844374fb67/numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf", size = 18652665, upload-time = "2025-07-24T20:55:46.665Z" }, + { url = "https://files.pythonhosted.org/packages/14/ba/5b5c9978c4bb161034148ade2de9db44ec316fab89ce8c400db0e0c81f86/numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1", size = 6514777, upload-time = "2025-07-24T20:55:57.66Z" }, + { url = "https://files.pythonhosted.org/packages/eb/46/3dbaf0ae7c17cdc46b9f662c56da2054887b8d9e737c1476f335c83d33db/numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b", size = 13111856, upload-time = "2025-07-24T20:56:17.318Z" }, + { url = "https://files.pythonhosted.org/packages/c1/9e/1652778bce745a67b5fe05adde60ed362d38eb17d919a540e813d30f6874/numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631", size = 10544226, upload-time = "2025-07-24T20:56:34.509Z" }, +] + [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -590,6 +661,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] +[[package]] +name = "pandas" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684, upload-time = "2025-08-21T10:28:29.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/64/a2f7bf678af502e16b472527735d168b22b7824e45a4d7e96a4fbb634b59/pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e", size = 11531061, upload-time = "2025-08-21T10:27:34.647Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/c3d21b2b7769ef2f4c2b9299fcadd601efa6729f1357a8dbce8dd949ed70/pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9", size = 10668666, upload-time = "2025-08-21T10:27:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/f775ba76ecfb3424d7f5862620841cf0edb592e9abd2d2a5387d305fe7a8/pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a", size = 11332835, upload-time = "2025-08-21T10:27:40.188Z" }, + { url = "https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b", size = 12057211, upload-time = "2025-08-21T10:27:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9d/2df913f14b2deb9c748975fdb2491da1a78773debb25abbc7cbc67c6b549/pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6", size = 12749277, upload-time = "2025-08-21T10:27:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/da1a2417026bd14d98c236dba88e39837182459d29dcfcea510b2ac9e8a1/pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a", size = 13415256, upload-time = "2025-08-21T10:27:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/22/3c/f2af1ce8840ef648584a6156489636b5692c162771918aa95707c165ad2b/pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b", size = 10982579, upload-time = "2025-08-21T10:28:08.435Z" }, + { url = "https://files.pythonhosted.org/packages/f3/98/8df69c4097a6719e357dc249bf437b8efbde808038268e584421696cbddf/pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57", size = 12028163, upload-time = "2025-08-21T10:27:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/f95cbcbea319f349e10ff90db488b905c6883f03cbabd34f6b03cbc3c044/pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2", size = 11391860, upload-time = "2025-08-21T10:27:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1b/6a984e98c4abee22058aa75bfb8eb90dce58cf8d7296f8bc56c14bc330b0/pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9", size = 11309830, upload-time = "2025-08-21T10:27:56.957Z" }, + { url = "https://files.pythonhosted.org/packages/15/d5/f0486090eb18dd8710bf60afeaf638ba6817047c0c8ae5c6a25598665609/pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2", size = 11883216, upload-time = "2025-08-21T10:27:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/10/86/692050c119696da19e20245bbd650d8dfca6ceb577da027c3a73c62a047e/pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012", size = 12699743, upload-time = "2025-08-21T10:28:02.447Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141, upload-time = "2025-08-21T10:28:05.377Z" }, +] + [[package]] name = "passlib" version = "1.7.4" @@ -652,6 +750,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" }, ] +[[package]] +name = "pyarrow" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/ca/c7eaa8e62db8fb37ce942b1ea0c6d7abfe3786ca193957afa25e71b81b66/pyarrow-21.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e99310a4ebd4479bcd1964dff9e14af33746300cb014aa4a3781738ac63baf4a", size = 31154306, upload-time = "2025-07-18T00:56:04.42Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e8/e87d9e3b2489302b3a1aea709aaca4b781c5252fcb812a17ab6275a9a484/pyarrow-21.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d2fe8e7f3ce329a71b7ddd7498b3cfac0eeb200c2789bd840234f0dc271a8efe", size = 32680622, upload-time = "2025-07-18T00:56:07.505Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/79095d73a742aa0aba370c7942b1b655f598069489ab387fe47261a849e1/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f522e5709379d72fb3da7785aa489ff0bb87448a9dc5a75f45763a795a089ebd", size = 41104094, upload-time = "2025-07-18T00:56:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/89/4b/7782438b551dbb0468892a276b8c789b8bbdb25ea5c5eb27faadd753e037/pyarrow-21.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:69cbbdf0631396e9925e048cfa5bce4e8c3d3b41562bbd70c685a8eb53a91e61", size = 42825576, upload-time = "2025-07-18T00:56:15.569Z" }, + { url = "https://files.pythonhosted.org/packages/b3/62/0f29de6e0a1e33518dec92c65be0351d32d7ca351e51ec5f4f837a9aab91/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:731c7022587006b755d0bdb27626a1a3bb004bb56b11fb30d98b6c1b4718579d", size = 43368342, upload-time = "2025-07-18T00:56:19.531Z" }, + { url = "https://files.pythonhosted.org/packages/90/c7/0fa1f3f29cf75f339768cc698c8ad4ddd2481c1742e9741459911c9ac477/pyarrow-21.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc56bc708f2d8ac71bd1dcb927e458c93cec10b98eb4120206a4091db7b67b99", size = 45131218, upload-time = "2025-07-18T00:56:23.347Z" }, + { url = "https://files.pythonhosted.org/packages/01/63/581f2076465e67b23bc5a37d4a2abff8362d389d29d8105832e82c9c811c/pyarrow-21.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:186aa00bca62139f75b7de8420f745f2af12941595bbbfa7ed3870ff63e25636", size = 26087551, upload-time = "2025-07-18T00:56:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ab/357d0d9648bb8241ee7348e564f2479d206ebe6e1c47ac5027c2e31ecd39/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:a7a102574faa3f421141a64c10216e078df467ab9576684d5cd696952546e2da", size = 31290064, upload-time = "2025-07-18T00:56:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8a/5685d62a990e4cac2043fc76b4661bf38d06efed55cf45a334b455bd2759/pyarrow-21.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:1e005378c4a2c6db3ada3ad4c217b381f6c886f0a80d6a316fe586b90f77efd7", size = 32727837, upload-time = "2025-07-18T00:56:33.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/de/c0828ee09525c2bafefd3e736a248ebe764d07d0fd762d4f0929dbc516c9/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:65f8e85f79031449ec8706b74504a316805217b35b6099155dd7e227eef0d4b6", size = 41014158, upload-time = "2025-07-18T00:56:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/26/a2865c420c50b7a3748320b614f3484bfcde8347b2639b2b903b21ce6a72/pyarrow-21.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3a81486adc665c7eb1a2bde0224cfca6ceaba344a82a971ef059678417880eb8", size = 42667885, upload-time = "2025-07-18T00:56:41.483Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f9/4ee798dc902533159250fb4321267730bc0a107d8c6889e07c3add4fe3a5/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fc0d2f88b81dcf3ccf9a6ae17f89183762c8a94a5bdcfa09e05cfe413acf0503", size = 43276625, upload-time = "2025-07-18T00:56:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/5a/da/e02544d6997037a4b0d22d8e5f66bc9315c3671371a8b18c79ade1cefe14/pyarrow-21.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6299449adf89df38537837487a4f8d3bd91ec94354fdd2a7d30bc11c48ef6e79", size = 44951890, upload-time = "2025-07-18T00:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4e/519c1bc1876625fe6b71e9a28287c43ec2f20f73c658b9ae1d485c0c206e/pyarrow-21.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:222c39e2c70113543982c6b34f3077962b44fca38c0bd9e68bb6781534425c10", size = 26371006, upload-time = "2025-07-18T00:56:56.379Z" }, +] + [[package]] name = "pycparser" version = "2.22" @@ -810,6 +930,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + [[package]] name = "pywin32" version = "310" @@ -1128,6 +1257,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, ] +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + [[package]] name = "urllib3" version = "2.5.0"