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.
Files changed (112) hide show
  1. drc_vantage-1.0.7/.env.example +17 -0
  2. drc_vantage-1.0.7/.gitignore +79 -0
  3. drc_vantage-1.0.7/.python-version +1 -0
  4. drc_vantage-1.0.7/PKG-INFO +70 -0
  5. drc_vantage-1.0.7/README.md +38 -0
  6. drc_vantage-1.0.7/pyproject.toml +44 -0
  7. drc_vantage-1.0.7/src/vantage/__init__.py +16 -0
  8. drc_vantage-1.0.7/src/vantage/client.py +126 -0
  9. drc_vantage-1.0.7/src/vantage/core/logo.py +157 -0
  10. drc_vantage-1.0.7/src/vantage/core/organization.py +51 -0
  11. drc_vantage-1.0.7/src/vantage/core/s3.py +31 -0
  12. drc_vantage-1.0.7/src/vantage/data_sources/data_source.py +71 -0
  13. drc_vantage-1.0.7/src/vantage/data_sources/data_source_category.py +32 -0
  14. drc_vantage-1.0.7/src/vantage/data_sources/sync/data_source_helpers.py +25 -0
  15. drc_vantage-1.0.7/src/vantage/data_sources/sync/sync_data_source.py +98 -0
  16. drc_vantage-1.0.7/src/vantage/data_sources/sync/sync_data_source_category.py +88 -0
  17. drc_vantage-1.0.7/src/vantage/data_storage_system/data_storage_system.py +77 -0
  18. drc_vantage-1.0.7/src/vantage/data_storage_system/sync/data_storage_system_helpers.py +51 -0
  19. drc_vantage-1.0.7/src/vantage/data_storage_system/sync/sync_data_storage_system.py +108 -0
  20. drc_vantage-1.0.7/src/vantage/datasets/alerts/check_alert.py +21 -0
  21. drc_vantage-1.0.7/src/vantage/datasets/alerts/check_dataset_alerts.py +22 -0
  22. drc_vantage-1.0.7/src/vantage/datasets/alerts/email.py +170 -0
  23. drc_vantage-1.0.7/src/vantage/datasets/alerts/evaluate_alert.py +283 -0
  24. drc_vantage-1.0.7/src/vantage/datasets/alerts/queries.py +128 -0
  25. drc_vantage-1.0.7/src/vantage/datasets/alerts/run_alert_check.py +95 -0
  26. drc_vantage-1.0.7/src/vantage/datasets/analysis/analyze_dataset.py +16 -0
  27. drc_vantage-1.0.7/src/vantage/datasets/analysis/column_analysis.py +436 -0
  28. drc_vantage-1.0.7/src/vantage/datasets/analysis/health_score.py +138 -0
  29. drc_vantage-1.0.7/src/vantage/datasets/analysis/list_dataset_ids.py +14 -0
  30. drc_vantage-1.0.7/src/vantage/datasets/analysis/metrics/anomalies.py +167 -0
  31. drc_vantage-1.0.7/src/vantage/datasets/analysis/metrics/completion.py +103 -0
  32. drc_vantage-1.0.7/src/vantage/datasets/analysis/metrics/row_count.py +139 -0
  33. drc_vantage-1.0.7/src/vantage/datasets/analysis/run_analysis.py +228 -0
  34. drc_vantage-1.0.7/src/vantage/datasets/analysis/smart_alerts.py +138 -0
  35. drc_vantage-1.0.7/src/vantage/datasets/dataset.py +150 -0
  36. drc_vantage-1.0.7/src/vantage/datasets/linking/assign_dataset_data_sources.py +105 -0
  37. drc_vantage-1.0.7/src/vantage/datasets/sync/dataset_helpers.py +275 -0
  38. drc_vantage-1.0.7/src/vantage/datasets/sync/materialized_views.py +116 -0
  39. drc_vantage-1.0.7/src/vantage/datasets/sync/sync_dataset.py +227 -0
  40. drc_vantage-1.0.7/src/vantage/datasets/teams/assign_dataset_to_team.py +279 -0
  41. drc_vantage-1.0.7/src/vantage/datasets/teams/team_helpers.py +69 -0
  42. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/__init__.py +3 -0
  43. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_avg.py +74 -0
  44. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_between.py +59 -0
  45. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_count_distinct.py +68 -0
  46. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_count_not_null.py +68 -0
  47. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_count_null.py +60 -0
  48. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_max.py +64 -0
  49. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_max_length.py +58 -0
  50. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_mean.py +74 -0
  51. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_median.py +78 -0
  52. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_min.py +64 -0
  53. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_min_length.py +58 -0
  54. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_not_between.py +59 -0
  55. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_not_null.py +46 -0
  56. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_percent_not_null.py +83 -0
  57. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_percent_null.py +81 -0
  58. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_percentile.py +86 -0
  59. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_regex_match.py +58 -0
  60. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_regex_not_match.py +58 -0
  61. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_stddev.py +74 -0
  62. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_sum.py +66 -0
  63. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_unique.py +49 -0
  64. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_values_in_set.py +62 -0
  65. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_column_values_not_in_set.py +61 -0
  66. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_custom_sql.py +58 -0
  67. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_duplicate_rows.py +52 -0
  68. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_freshness.py +86 -0
  69. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_row_count.py +52 -0
  70. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_row_count_between.py +43 -0
  71. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/check_table_schema_change.py +25 -0
  72. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/run_check.py +124 -0
  73. drc_vantage-1.0.7/src/vantage/datasets/validations/checks/sql_helpers.py +80 -0
  74. drc_vantage-1.0.7/src/vantage/datasets/validations/get_dataset_info.py +65 -0
  75. drc_vantage-1.0.7/src/vantage/datasets/validations/get_dataset_validations.py +105 -0
  76. drc_vantage-1.0.7/src/vantage/datasets/validations/recommend_validations.py +143 -0
  77. drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/dedupe.py +154 -0
  78. drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/fetch.py +333 -0
  79. drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/llm.py +78 -0
  80. drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/normalize.py +75 -0
  81. drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/prompt.py +140 -0
  82. drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/save.py +165 -0
  83. drc_vantage-1.0.7/src/vantage/datasets/validations/recommendations/schemas.py +67 -0
  84. drc_vantage-1.0.7/src/vantage/datasets/validations/rule_config.py +188 -0
  85. drc_vantage-1.0.7/src/vantage/datasets/validations/rule_config_reference.json +143 -0
  86. drc_vantage-1.0.7/src/vantage/datasets/validations/run_all_dataset_validation_suites.py +28 -0
  87. drc_vantage-1.0.7/src/vantage/datasets/validations/run_dataset_validations.py +13 -0
  88. drc_vantage-1.0.7/src/vantage/datasets/validations/run_validation.py +18 -0
  89. drc_vantage-1.0.7/src/vantage/datasets/validations/run_validation_rule.py +120 -0
  90. drc_vantage-1.0.7/src/vantage/datasets/validations/run_validation_suite.py +190 -0
  91. drc_vantage-1.0.7/src/vantage/datasets/validations/schemas.py +136 -0
  92. drc_vantage-1.0.7/src/vantage/kpis/compute/compute.py +160 -0
  93. drc_vantage-1.0.7/src/vantage/kpis/compute/compute_manual.py +399 -0
  94. drc_vantage-1.0.7/src/vantage/kpis/compute/compute_sql.py +37 -0
  95. drc_vantage-1.0.7/src/vantage/kpis/compute/fetch.py +35 -0
  96. drc_vantage-1.0.7/src/vantage/kpis/compute/save.py +65 -0
  97. drc_vantage-1.0.7/src/vantage/kpis/compute/schemas.py +0 -0
  98. drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/fetch.py +105 -0
  99. drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/llm.py +112 -0
  100. drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/prompt.py +86 -0
  101. drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/save.py +80 -0
  102. drc_vantage-1.0.7/src/vantage/smart_cockpit/ai_summary/schemas.py +26 -0
  103. drc_vantage-1.0.7/src/vantage/smart_cockpit/company_news/llm.py +0 -0
  104. drc_vantage-1.0.7/src/vantage/smart_cockpit/company_news/save.py +0 -0
  105. drc_vantage-1.0.7/src/vantage/smart_cockpit/company_news/schemas.py +0 -0
  106. drc_vantage-1.0.7/src/vantage/smart_cockpit/configuration.py +35 -0
  107. drc_vantage-1.0.7/src/vantage/smart_cockpit/kpi_insights/fetch.py +60 -0
  108. drc_vantage-1.0.7/src/vantage/smart_cockpit/kpi_insights/llm.py +59 -0
  109. drc_vantage-1.0.7/src/vantage/smart_cockpit/kpi_insights/prompt.py +117 -0
  110. drc_vantage-1.0.7/src/vantage/smart_cockpit/kpi_insights/save.py +79 -0
  111. drc_vantage-1.0.7/src/vantage/smart_cockpit/regenerate/generate_missing_kpi_insights.py +23 -0
  112. 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