drc-vantage 1.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- drc_vantage-1.0.7/.env.example +17 -0
- drc_vantage-1.0.7/.gitignore +79 -0
- drc_vantage-1.0.7/.python-version +1 -0
- drc_vantage-1.0.7/PKG-INFO +70 -0
- drc_vantage-1.0.7/README.md +38 -0
- drc_vantage-1.0.7/pyproject.toml +44 -0
- drc_vantage-1.0.7/src/vantage/__init__.py +16 -0
- drc_vantage-1.0.7/src/vantage/client.py +126 -0
- drc_vantage-1.0.7/src/vantage/core/logo.py +157 -0
- drc_vantage-1.0.7/src/vantage/core/organization.py +51 -0
- drc_vantage-1.0.7/src/vantage/core/s3.py +31 -0
- drc_vantage-1.0.7/src/vantage/data_sources/data_source.py +71 -0
- drc_vantage-1.0.7/src/vantage/data_sources/data_source_category.py +32 -0
- drc_vantage-1.0.7/src/vantage/data_sources/sync/data_source_helpers.py +25 -0
- drc_vantage-1.0.7/src/vantage/data_sources/sync/sync_data_source.py +98 -0
- drc_vantage-1.0.7/src/vantage/data_sources/sync/sync_data_source_category.py +88 -0
- drc_vantage-1.0.7/src/vantage/data_storage_system/data_storage_system.py +77 -0
- drc_vantage-1.0.7/src/vantage/data_storage_system/sync/data_storage_system_helpers.py +51 -0
- drc_vantage-1.0.7/src/vantage/data_storage_system/sync/sync_data_storage_system.py +108 -0
- drc_vantage-1.0.7/src/vantage/datasets/alerts/check_alert.py +21 -0
- drc_vantage-1.0.7/src/vantage/datasets/alerts/check_dataset_alerts.py +22 -0
- drc_vantage-1.0.7/src/vantage/datasets/alerts/email.py +170 -0
- drc_vantage-1.0.7/src/vantage/datasets/alerts/evaluate_alert.py +283 -0
- drc_vantage-1.0.7/src/vantage/datasets/alerts/queries.py +128 -0
- drc_vantage-1.0.7/src/vantage/datasets/alerts/run_alert_check.py +95 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/analyze_dataset.py +16 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/column_analysis.py +436 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/health_score.py +138 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/list_dataset_ids.py +14 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/metrics/anomalies.py +167 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/metrics/completion.py +103 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/metrics/row_count.py +139 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/run_analysis.py +228 -0
- drc_vantage-1.0.7/src/vantage/datasets/analysis/smart_alerts.py +138 -0
- drc_vantage-1.0.7/src/vantage/datasets/dataset.py +150 -0
- drc_vantage-1.0.7/src/vantage/datasets/linking/assign_dataset_data_sources.py +105 -0
- drc_vantage-1.0.7/src/vantage/datasets/sync/dataset_helpers.py +275 -0
- drc_vantage-1.0.7/src/vantage/datasets/sync/materialized_views.py +116 -0
- drc_vantage-1.0.7/src/vantage/datasets/sync/sync_dataset.py +227 -0
- drc_vantage-1.0.7/src/vantage/datasets/teams/assign_dataset_to_team.py +279 -0
- drc_vantage-1.0.7/src/vantage/datasets/teams/team_helpers.py +69 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/__init__.py +3 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_avg.py +74 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_between.py +59 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_count_distinct.py +68 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_count_not_null.py +68 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_count_null.py +60 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_max.py +64 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_max_length.py +58 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_mean.py +74 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_median.py +78 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_min.py +64 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_min_length.py +58 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_not_between.py +59 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_not_null.py +46 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_percent_not_null.py +83 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_percent_null.py +81 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_percentile.py +86 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_regex_match.py +58 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_regex_not_match.py +58 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_stddev.py +74 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_sum.py +66 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_unique.py +49 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_values_in_set.py +62 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_values_not_in_set.py +61 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_custom_sql.py +58 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_duplicate_rows.py +52 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_freshness.py +86 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_row_count.py +52 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_row_count_between.py +43 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_schema_change.py +25 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/run_check.py +124 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/checks/sql_helpers.py +80 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/get_dataset_info.py +65 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/get_dataset_validations.py +105 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/recommend_validations.py +143 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/dedupe.py +154 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/fetch.py +333 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/llm.py +78 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/normalize.py +75 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/prompt.py +140 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/save.py +165 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/schemas.py +67 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/rule_config.py +188 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/rule_config_reference.json +143 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/run_all_dataset_validation_suites.py +28 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/run_dataset_validations.py +13 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/run_validation.py +18 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/run_validation_rule.py +120 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/run_validation_suite.py +190 -0
- drc_vantage-1.0.7/src/vantage/datasets/validations/schemas.py +136 -0
- drc_vantage-1.0.7/src/vantage/kpis/compute/compute.py +160 -0
- drc_vantage-1.0.7/src/vantage/kpis/compute/compute_manual.py +399 -0
- drc_vantage-1.0.7/src/vantage/kpis/compute/compute_sql.py +37 -0
- drc_vantage-1.0.7/src/vantage/kpis/compute/fetch.py +35 -0
- drc_vantage-1.0.7/src/vantage/kpis/compute/save.py +65 -0
- drc_vantage-1.0.7/src/vantage/kpis/compute/schemas.py +0 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/fetch.py +105 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/llm.py +112 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/prompt.py +86 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/save.py +80 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/schemas.py +26 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/company_news/llm.py +0 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/company_news/save.py +0 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/company_news/schemas.py +0 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/configuration.py +35 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/kpi_insights/fetch.py +60 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/kpi_insights/llm.py +59 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/kpi_insights/prompt.py +117 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/kpi_insights/save.py +79 -0
- drc_vantage-1.0.7/src/vantage/smart_cockpit/regenerate/generate_missing_kpi_insights.py +23 -0
- drc_vantage-1.0.7/uv.lock +1122 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# PostgreSQL (data warehouse)
|
|
2
|
+
POSTGRES_HOST=localhost
|
|
3
|
+
POSTGRES_PORT=5432
|
|
4
|
+
POSTGRES_USER=postgres
|
|
5
|
+
POSTGRES_PASSWORD=pOstgres123!
|
|
6
|
+
POSTGRES_DATABASE=demo
|
|
7
|
+
POSTGRES_DEFAULT_SCHEMA=public
|
|
8
|
+
|
|
9
|
+
# Vantage configuration
|
|
10
|
+
VANTAGE_SCHEMA=vantage
|
|
11
|
+
GENERATE_AI_VALIDATIONS=false
|
|
12
|
+
|
|
13
|
+
# MinIO / S3
|
|
14
|
+
S3_ENDPOINT_URL=http://localhost:9000
|
|
15
|
+
S3_ACCESS_KEY=minioadmin
|
|
16
|
+
S3_SECRET_KEY=minioadmin
|
|
17
|
+
S3_BUCKET_NAME=demo
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# Dependencies
|
|
4
|
+
**/node_modules/
|
|
5
|
+
**/.pnp
|
|
6
|
+
.pnp.*
|
|
7
|
+
.yarn/*
|
|
8
|
+
!.yarn/patches
|
|
9
|
+
!.yarn/plugins
|
|
10
|
+
!.yarn/releases
|
|
11
|
+
!.yarn/versions
|
|
12
|
+
|
|
13
|
+
# Testing
|
|
14
|
+
**/coverage/
|
|
15
|
+
|
|
16
|
+
# Next.js
|
|
17
|
+
**/.next/
|
|
18
|
+
**/out/
|
|
19
|
+
|
|
20
|
+
# Production
|
|
21
|
+
**/build/
|
|
22
|
+
|
|
23
|
+
# Python
|
|
24
|
+
**/__pycache__/
|
|
25
|
+
**/*.py[cod]
|
|
26
|
+
**/.venv/
|
|
27
|
+
**/dist/
|
|
28
|
+
**/wheels/
|
|
29
|
+
**/*.egg-info/
|
|
30
|
+
**/.ruff_cache/
|
|
31
|
+
**/.pytest_cache/
|
|
32
|
+
|
|
33
|
+
# TypeScript
|
|
34
|
+
**/*.tsbuildinfo
|
|
35
|
+
**/next-env.d.ts
|
|
36
|
+
|
|
37
|
+
# Prisma
|
|
38
|
+
**/prisma/generated/
|
|
39
|
+
|
|
40
|
+
# Environment variables
|
|
41
|
+
.env
|
|
42
|
+
.env.*
|
|
43
|
+
!.env.example
|
|
44
|
+
!.env.*.example
|
|
45
|
+
|
|
46
|
+
# Debug logs
|
|
47
|
+
npm-debug.log*
|
|
48
|
+
yarn-debug.log*
|
|
49
|
+
yarn-error.log*
|
|
50
|
+
.pnpm-debug.log*
|
|
51
|
+
|
|
52
|
+
# Deployment
|
|
53
|
+
**/.vercel/
|
|
54
|
+
|
|
55
|
+
# OS
|
|
56
|
+
**/.DS_Store
|
|
57
|
+
**/Thumbs.db
|
|
58
|
+
|
|
59
|
+
# IDE — ignore personal settings, keep shared workspace config
|
|
60
|
+
**/.vscode/*
|
|
61
|
+
!**/.vscode/settings.json
|
|
62
|
+
!**/.vscode/extensions.json
|
|
63
|
+
!**/.vscode/tasks.json
|
|
64
|
+
!**/.vscode/launch.json
|
|
65
|
+
**/.idea/
|
|
66
|
+
|
|
67
|
+
# Cursor
|
|
68
|
+
**/.cursor/
|
|
69
|
+
|
|
70
|
+
# Local themes & branding
|
|
71
|
+
**/src/config/themes/
|
|
72
|
+
**/public/branding/
|
|
73
|
+
|
|
74
|
+
# Notebooks
|
|
75
|
+
*.ipynb_checkpoints/
|
|
76
|
+
notebook.ipynb
|
|
77
|
+
|
|
78
|
+
# Secrets
|
|
79
|
+
*.pem
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: drc-vantage
|
|
3
|
+
Version: 1.0.7
|
|
4
|
+
Summary: Python SDK for the DRC Vantage data platform
|
|
5
|
+
Project-URL: Homepage, https://github.com/nathan294/vantage
|
|
6
|
+
Project-URL: Repository, https://github.com/nathan294/vantage
|
|
7
|
+
Project-URL: Issues, https://github.com/nathan294/vantage/issues
|
|
8
|
+
Author: DRC
|
|
9
|
+
License-Expression: LicenseRef-Proprietary
|
|
10
|
+
Keywords: analytics,data,governance,postgresql,sdk,vantage
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: Other/Proprietary License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Database
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.13
|
|
20
|
+
Requires-Dist: boto3>=1.43.46
|
|
21
|
+
Requires-Dist: langchain-core>=0.3.0
|
|
22
|
+
Requires-Dist: langchain-openai>=0.3.0
|
|
23
|
+
Requires-Dist: loguru>=0.7.3
|
|
24
|
+
Requires-Dist: numpy>=2.5.1
|
|
25
|
+
Requires-Dist: pillow>=11.0.0
|
|
26
|
+
Requires-Dist: psycopg[binary]>=3.3.4
|
|
27
|
+
Requires-Dist: pydantic>=2.13.4
|
|
28
|
+
Requires-Dist: resend>=2.32.2
|
|
29
|
+
Requires-Dist: scikit-learn>=1.9.0
|
|
30
|
+
Requires-Dist: sqlalchemy>=2.0.51
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# DRC Vantage SDK
|
|
34
|
+
|
|
35
|
+
Python SDK for the [Vantage](https://github.com/nathan294/vantage) data platform: dataset sync, validation, analysis, KPI computation, and smart cockpit pipelines.
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.13+
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install drc-vantage
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from vantage import Client, Dataset
|
|
51
|
+
|
|
52
|
+
client = Client(
|
|
53
|
+
postgres_host="localhost",
|
|
54
|
+
postgres_user="postgres",
|
|
55
|
+
postgres_password="secret",
|
|
56
|
+
postgres_database="vantage",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
dataset = Dataset(
|
|
60
|
+
client=client,
|
|
61
|
+
location={"schema": "marts", "table": "customers"},
|
|
62
|
+
dataset_name="Customers",
|
|
63
|
+
dataset_type="TABLE",
|
|
64
|
+
)
|
|
65
|
+
dataset.sync()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
Proprietary. All rights reserved.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# DRC Vantage SDK
|
|
2
|
+
|
|
3
|
+
Python SDK for the [Vantage](https://github.com/nathan294/vantage) data platform: dataset sync, validation, analysis, KPI computation, and smart cockpit pipelines.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Python 3.13+
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install drc-vantage
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from vantage import Client, Dataset
|
|
19
|
+
|
|
20
|
+
client = Client(
|
|
21
|
+
postgres_host="localhost",
|
|
22
|
+
postgres_user="postgres",
|
|
23
|
+
postgres_password="secret",
|
|
24
|
+
postgres_database="vantage",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
dataset = Dataset(
|
|
28
|
+
client=client,
|
|
29
|
+
location={"schema": "marts", "table": "customers"},
|
|
30
|
+
dataset_name="Customers",
|
|
31
|
+
dataset_type="TABLE",
|
|
32
|
+
)
|
|
33
|
+
dataset.sync()
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
Proprietary. All rights reserved.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "drc-vantage"
|
|
7
|
+
version = "1.0.7"
|
|
8
|
+
description = "Python SDK for the DRC Vantage data platform"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.13"
|
|
11
|
+
license = "LicenseRef-Proprietary"
|
|
12
|
+
authors = [{ name = "DRC" }]
|
|
13
|
+
keywords = ["vantage", "data", "sdk", "postgresql", "analytics", "governance"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: Other/Proprietary License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Topic :: Database",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"boto3>=1.43.46",
|
|
26
|
+
"langchain-core>=0.3.0",
|
|
27
|
+
"langchain-openai>=0.3.0",
|
|
28
|
+
"loguru>=0.7.3",
|
|
29
|
+
"numpy>=2.5.1",
|
|
30
|
+
"pillow>=11.0.0",
|
|
31
|
+
"psycopg[binary]>=3.3.4",
|
|
32
|
+
"pydantic>=2.13.4",
|
|
33
|
+
"resend>=2.32.2",
|
|
34
|
+
"scikit-learn>=1.9.0",
|
|
35
|
+
"sqlalchemy>=2.0.51",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/nathan294/vantage"
|
|
40
|
+
Repository = "https://github.com/nathan294/vantage"
|
|
41
|
+
Issues = "https://github.com/nathan294/vantage/issues"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/vantage"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""DRC Vantage SDK — platform data pipelines."""
|
|
2
|
+
|
|
3
|
+
from vantage.client import Base, Client
|
|
4
|
+
from vantage.data_sources.data_source import DataSource
|
|
5
|
+
from vantage.data_sources.data_source_category import DataSourceCategory
|
|
6
|
+
from vantage.data_storage_system.data_storage_system import DataStorageSystem
|
|
7
|
+
from vantage.datasets.dataset import Dataset
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Base",
|
|
11
|
+
"Client",
|
|
12
|
+
"DataSource",
|
|
13
|
+
"DataSourceCategory",
|
|
14
|
+
"DataStorageSystem",
|
|
15
|
+
"Dataset",
|
|
16
|
+
]
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Vantage SDK client: database access and runtime configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import Any, Generator
|
|
7
|
+
from urllib.parse import quote_plus
|
|
8
|
+
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from sqlalchemy import create_engine, text
|
|
11
|
+
from sqlalchemy.engine import Engine
|
|
12
|
+
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Base(DeclarativeBase):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_secret(value: Any) -> str:
|
|
20
|
+
if hasattr(value, "get_secret_value"):
|
|
21
|
+
return value.get_secret_value()
|
|
22
|
+
return str(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Client:
|
|
26
|
+
"""Entry point for SDK runtime configuration and database access."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
*,
|
|
31
|
+
vantage_schema: str = "vantage",
|
|
32
|
+
generate_ai_validations: bool = False,
|
|
33
|
+
postgres_host: str | None = None,
|
|
34
|
+
postgres_port: int = 5432,
|
|
35
|
+
postgres_user: str | None = None,
|
|
36
|
+
postgres_password: Any | None = None,
|
|
37
|
+
postgres_database: str = "vantage",
|
|
38
|
+
openai_api_key: str | None = None,
|
|
39
|
+
resend_api_key: str | None = None,
|
|
40
|
+
s3_client: Any | None = None,
|
|
41
|
+
s3_bucket_name: str | None = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.vantage_schema = vantage_schema
|
|
44
|
+
self.generate_ai_validations = generate_ai_validations
|
|
45
|
+
self.postgres_host = postgres_host
|
|
46
|
+
self.postgres_port = postgres_port
|
|
47
|
+
self.postgres_user = postgres_user
|
|
48
|
+
self.postgres_password = postgres_password
|
|
49
|
+
self.postgres_database = postgres_database
|
|
50
|
+
self.openai_api_key = openai_api_key
|
|
51
|
+
self.resend_api_key = resend_api_key
|
|
52
|
+
self.s3_client = s3_client
|
|
53
|
+
self.s3_bucket_name = s3_bucket_name
|
|
54
|
+
self._engine: Engine | None = None
|
|
55
|
+
self._session_maker: sessionmaker[Session] | None = None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_postgres_configured(self) -> bool:
|
|
59
|
+
return bool(self.postgres_host and self.postgres_user and self.postgres_password)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_s3_configured(self) -> bool:
|
|
63
|
+
return self.s3_client is not None and self.s3_bucket_name is not None
|
|
64
|
+
|
|
65
|
+
def _build_postgres_url(self, database: str | None = None) -> str:
|
|
66
|
+
if not self.is_postgres_configured:
|
|
67
|
+
raise ValueError("PostgreSQL is not configured. Set POSTGRES_* variables in your .env file.")
|
|
68
|
+
|
|
69
|
+
db = database or self.postgres_database
|
|
70
|
+
password = quote_plus(_get_secret(self.postgres_password))
|
|
71
|
+
user = quote_plus(self.postgres_user) # type: ignore[arg-type]
|
|
72
|
+
return f"postgresql+psycopg://{user}:{password}@{self.postgres_host}:{self.postgres_port}/{db}"
|
|
73
|
+
|
|
74
|
+
def get_engine(self) -> Engine:
|
|
75
|
+
if self._engine is None:
|
|
76
|
+
self._engine = create_engine(self._build_postgres_url(), pool_pre_ping=True)
|
|
77
|
+
return self._engine
|
|
78
|
+
|
|
79
|
+
def get_session_maker(self) -> sessionmaker[Session]:
|
|
80
|
+
if self._session_maker is None:
|
|
81
|
+
self._session_maker = sessionmaker(
|
|
82
|
+
bind=self.get_engine(),
|
|
83
|
+
class_=Session,
|
|
84
|
+
expire_on_commit=False,
|
|
85
|
+
)
|
|
86
|
+
return self._session_maker
|
|
87
|
+
|
|
88
|
+
@contextmanager
|
|
89
|
+
def get_session(self) -> Generator[Session, None, None]:
|
|
90
|
+
session = self.get_session_maker()()
|
|
91
|
+
try:
|
|
92
|
+
yield session
|
|
93
|
+
session.commit()
|
|
94
|
+
except Exception:
|
|
95
|
+
session.rollback()
|
|
96
|
+
raise
|
|
97
|
+
finally:
|
|
98
|
+
session.close()
|
|
99
|
+
|
|
100
|
+
def ensure_database_exists(self) -> None:
|
|
101
|
+
if not self.is_postgres_configured:
|
|
102
|
+
logger.warning("Database configuration is not valid, skipping database creation check")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
engine = create_engine(
|
|
106
|
+
self._build_postgres_url("postgres"),
|
|
107
|
+
pool_pre_ping=True,
|
|
108
|
+
isolation_level="AUTOCOMMIT",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
with engine.connect() as conn:
|
|
113
|
+
exists = conn.execute(
|
|
114
|
+
text("SELECT 1 FROM pg_database WHERE datname = :dbname"),
|
|
115
|
+
{"dbname": self.postgres_database},
|
|
116
|
+
).scalar()
|
|
117
|
+
|
|
118
|
+
if exists:
|
|
119
|
+
logger.info(f"Database '{self.postgres_database}' already exists")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
logger.info(f"Database '{self.postgres_database}' does not exist, creating it...")
|
|
123
|
+
conn.execute(text(f'CREATE DATABASE "{self.postgres_database}"'))
|
|
124
|
+
logger.info(f"Database '{self.postgres_database}' created successfully")
|
|
125
|
+
finally:
|
|
126
|
+
engine.dispose()
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Prepare and upload data source logos to S3 (mirrors frontend logo pipeline)."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import mimetypes
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
from urllib.request import Request, urlopen
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from PIL import Image
|
|
12
|
+
from vantage.client import Client
|
|
13
|
+
from vantage.core.s3 import upload_object
|
|
14
|
+
|
|
15
|
+
SVG_MIME = "image/svg+xml"
|
|
16
|
+
MAX_INPUT_BYTES = 8 * 1024 * 1024
|
|
17
|
+
LOGO_MAX_DIMENSION = 200
|
|
18
|
+
WEBP_QUALITY = 85
|
|
19
|
+
LOGO_FETCH_USER_AGENT = "demo-flows/0.1 (Foresyn; data source logo sync)"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_extension_for_logo_content_type(content_type: str) -> str | None:
|
|
23
|
+
normalized = content_type.split(";")[0].strip().lower()
|
|
24
|
+
mapping = {
|
|
25
|
+
"image/webp": "webp",
|
|
26
|
+
"image/svg+xml": "svg",
|
|
27
|
+
"image/png": "png",
|
|
28
|
+
"image/jpeg": "jpg",
|
|
29
|
+
"image/jpg": "jpg",
|
|
30
|
+
}
|
|
31
|
+
return mapping.get(normalized)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def prepare_data_source_logo(
|
|
35
|
+
image_bytes: bytes,
|
|
36
|
+
*,
|
|
37
|
+
filename: str | None = None,
|
|
38
|
+
content_type: str | None = None,
|
|
39
|
+
) -> tuple[bytes, str]:
|
|
40
|
+
"""
|
|
41
|
+
Prepare a logo for upload.
|
|
42
|
+
|
|
43
|
+
Raster images: resize/compress to WebP, max 200×200px.
|
|
44
|
+
SVG: kept as vector text (no raster compression), basic structure check.
|
|
45
|
+
"""
|
|
46
|
+
if len(image_bytes) > MAX_INPUT_BYTES:
|
|
47
|
+
raise ValueError("FILE_TOO_LARGE")
|
|
48
|
+
|
|
49
|
+
guessed_type = content_type or (
|
|
50
|
+
mimetypes.guess_type(filename or "")[0] if filename else None
|
|
51
|
+
)
|
|
52
|
+
looks_svg = guessed_type == SVG_MIME or (
|
|
53
|
+
filename and filename.lower().endswith(".svg")
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if looks_svg:
|
|
57
|
+
return _prepare_svg_logo(image_bytes)
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
return _prepare_raster_logo(image_bytes)
|
|
61
|
+
except ValueError:
|
|
62
|
+
raise
|
|
63
|
+
except Exception as exc:
|
|
64
|
+
raise ValueError("COMPRESS_FAILED") from exc
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def upload_data_source_logo(client: Client, image_buffer: bytes, content_type: str) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Upload a data source logo to S3. Uses a random object name (never the client filename).
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
S3 object key to store in ``DataSource.logoUrl`` (e.g. ``logos/uuid.webp``)
|
|
73
|
+
"""
|
|
74
|
+
ext = get_extension_for_logo_content_type(content_type)
|
|
75
|
+
if not ext:
|
|
76
|
+
raise ValueError(f"Unsupported logo content type: {content_type}")
|
|
77
|
+
|
|
78
|
+
normalized_type = content_type.split(";")[0].strip().lower()
|
|
79
|
+
object_key = f"logos/{uuid4()}.{ext}"
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
upload_object(
|
|
83
|
+
client=client,
|
|
84
|
+
object_key=object_key,
|
|
85
|
+
body=image_buffer,
|
|
86
|
+
content_type=normalized_type or f"image/{'jpeg' if ext == 'jpg' else ext}",
|
|
87
|
+
)
|
|
88
|
+
logger.info("Uploaded data source logo to %s", object_key)
|
|
89
|
+
return object_key
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
logger.exception("Error uploading data source logo to S3")
|
|
92
|
+
raise RuntimeError(
|
|
93
|
+
f"Failed to upload data source logo: {exc if isinstance(exc, Exception) else 'Unknown error'}"
|
|
94
|
+
) from exc
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def upload_data_source_logo_from_path(client: Client, path: str | Path) -> str:
|
|
98
|
+
"""Read a local file, prepare it, upload to S3, and return the object key."""
|
|
99
|
+
file_path = Path(path)
|
|
100
|
+
if not file_path.is_file():
|
|
101
|
+
raise FileNotFoundError(f"Logo file not found: {file_path}")
|
|
102
|
+
|
|
103
|
+
raw = file_path.read_bytes()
|
|
104
|
+
prepared, content_type = prepare_data_source_logo(raw, filename=file_path.name)
|
|
105
|
+
return upload_data_source_logo(client, prepared, content_type)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def upload_data_source_logo_from_url(client: Client, url: str) -> str:
|
|
109
|
+
"""Download a remote image, prepare it, upload to S3, and return the object key."""
|
|
110
|
+
if not client.is_s3_configured:
|
|
111
|
+
return url
|
|
112
|
+
|
|
113
|
+
parsed = urlparse(url)
|
|
114
|
+
if parsed.scheme not in ("http", "https"):
|
|
115
|
+
raise ValueError(f"Invalid logo URL: {url}")
|
|
116
|
+
|
|
117
|
+
request = Request(url, headers={"User-Agent": LOGO_FETCH_USER_AGENT})
|
|
118
|
+
with urlopen(request, timeout=30) as response:
|
|
119
|
+
raw = response.read()
|
|
120
|
+
content_type = response.headers.get("Content-Type")
|
|
121
|
+
filename = Path(parsed.path).name or None
|
|
122
|
+
|
|
123
|
+
prepared, content_type = prepare_data_source_logo(
|
|
124
|
+
raw, filename=filename, content_type=content_type
|
|
125
|
+
)
|
|
126
|
+
return upload_data_source_logo(client, prepared, content_type)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _prepare_svg_logo(image_bytes: bytes) -> tuple[bytes, str]:
|
|
130
|
+
try:
|
|
131
|
+
text = image_bytes.decode("utf-8")
|
|
132
|
+
except UnicodeDecodeError as exc:
|
|
133
|
+
raise ValueError("INVALID_SVG") from exc
|
|
134
|
+
|
|
135
|
+
if not text.strip().lstrip().lower().startswith("<svg"):
|
|
136
|
+
raise ValueError("INVALID_SVG")
|
|
137
|
+
|
|
138
|
+
return text.encode("utf-8"), SVG_MIME
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _prepare_raster_logo(image_bytes: bytes) -> tuple[bytes, str]:
|
|
142
|
+
with Image.open(io.BytesIO(image_bytes)) as img:
|
|
143
|
+
img.thumbnail(
|
|
144
|
+
(LOGO_MAX_DIMENSION, LOGO_MAX_DIMENSION), Image.Resampling.LANCZOS
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if img.mode in ("RGBA", "LA") or (
|
|
148
|
+
img.mode == "P" and "transparency" in img.info
|
|
149
|
+
):
|
|
150
|
+
img = img.convert("RGBA")
|
|
151
|
+
else:
|
|
152
|
+
img = img.convert("RGB")
|
|
153
|
+
|
|
154
|
+
out = io.BytesIO()
|
|
155
|
+
img.save(out, format="WEBP", quality=WEBP_QUALITY, method=6)
|
|
156
|
+
|
|
157
|
+
return out.getvalue(), "image/webp"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Shared helpers for resolving platform organizations."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from sqlalchemy import text
|
|
6
|
+
|
|
7
|
+
from vantage.client import Client
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_organization_by_slug(client: Client, organization_slug: str) -> Optional[str]:
|
|
11
|
+
with client.get_session() as session:
|
|
12
|
+
organization = session.execute(
|
|
13
|
+
text(f"SELECT id FROM {client.vantage_schema}.organization WHERE slug = :organization_slug"),
|
|
14
|
+
{"organization_slug": organization_slug},
|
|
15
|
+
).fetchone()
|
|
16
|
+
return organization[0] if organization else None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_organization_count(client: Client) -> int:
|
|
20
|
+
with client.get_session() as session:
|
|
21
|
+
row = session.execute(text(f"SELECT COUNT(*) FROM {client.vantage_schema}.organization")).fetchone()
|
|
22
|
+
return int(row[0]) if row else 0
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_single_organization_id_if_only_one_exists(client: Client) -> Optional[str]:
|
|
26
|
+
with client.get_session() as session:
|
|
27
|
+
rows = session.execute(text(f"SELECT id FROM {client.vantage_schema}.organization LIMIT 2")).fetchall()
|
|
28
|
+
if len(rows) == 1:
|
|
29
|
+
return rows[0][0]
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resolve_organization_id(client: Client,
|
|
34
|
+
organization_id: Optional[str] = None,
|
|
35
|
+
organization_slug: Optional[str] = None,
|
|
36
|
+
) -> str:
|
|
37
|
+
if organization_slug:
|
|
38
|
+
org_id = get_organization_by_slug(client, organization_slug=organization_slug)
|
|
39
|
+
if not org_id:
|
|
40
|
+
raise ValueError(f"Organization with slug '{organization_slug}' not found")
|
|
41
|
+
return org_id
|
|
42
|
+
if organization_id:
|
|
43
|
+
return organization_id
|
|
44
|
+
org_id = get_single_organization_id_if_only_one_exists(client)
|
|
45
|
+
if org_id:
|
|
46
|
+
return org_id
|
|
47
|
+
if get_organization_count(client) == 0:
|
|
48
|
+
raise ValueError("No organization found; provide organization_id or organization_slug")
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"Multiple organizations found; provide organization_id or organization_slug"
|
|
51
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""S3-compatible object storage client (MinIO, AWS S3, etc.)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from vantage.client import Client
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_s3_client(client: Client) -> Any:
|
|
9
|
+
if not client.is_s3_configured:
|
|
10
|
+
raise ValueError("S3 is not configured. Pass s3_client and s3_bucket_name to Client.")
|
|
11
|
+
return client.s3_client
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def upload_object(
|
|
15
|
+
client: Client,
|
|
16
|
+
object_key: str,
|
|
17
|
+
body: bytes,
|
|
18
|
+
content_type: str,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Upload bytes to the configured bucket. Returns the object key."""
|
|
21
|
+
if not client.is_s3_configured:
|
|
22
|
+
raise ValueError("S3 is not configured. Pass s3_client and s3_bucket_name to Client.")
|
|
23
|
+
|
|
24
|
+
client.s3_client.put_object(
|
|
25
|
+
Bucket=client.s3_bucket_name,
|
|
26
|
+
Key=object_key,
|
|
27
|
+
Body=body,
|
|
28
|
+
ContentLength=len(body),
|
|
29
|
+
ContentType=content_type,
|
|
30
|
+
)
|
|
31
|
+
return object_key
|