getdango 0.0.1__py3-none-any.whl
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.
- dango/__init__.py +12 -0
- dango/cli/__init__.py +5 -0
- dango/cli/db_helpers.py +123 -0
- dango/cli/env_helpers.py +336 -0
- dango/cli/init.py +815 -0
- dango/cli/main.py +3252 -0
- dango/cli/model_wizard.py +519 -0
- dango/cli/oauth.py +436 -0
- dango/cli/schema_manager.py +361 -0
- dango/cli/source_wizard.py +665 -0
- dango/cli/utils.py +780 -0
- dango/cli/validate.py +549 -0
- dango/cli/wizard.py +309 -0
- dango/config/__init__.py +49 -0
- dango/config/exceptions.py +25 -0
- dango/config/loader.py +288 -0
- dango/config/models.py +406 -0
- dango/ingestion/__init__.py +18 -0
- dango/ingestion/csv_loader.py +749 -0
- dango/ingestion/dlt_runner.py +1257 -0
- dango/ingestion/dlt_sources/__init__.py +11 -0
- dango/ingestion/dlt_sources/airtable/README.md +87 -0
- dango/ingestion/dlt_sources/airtable/__init__.py +68 -0
- dango/ingestion/dlt_sources/asana_dlt/README.md +69 -0
- dango/ingestion/dlt_sources/asana_dlt/__init__.py +264 -0
- dango/ingestion/dlt_sources/asana_dlt/helpers.py +17 -0
- dango/ingestion/dlt_sources/asana_dlt/settings.py +144 -0
- dango/ingestion/dlt_sources/chess/README.md +64 -0
- dango/ingestion/dlt_sources/chess/__init__.py +167 -0
- dango/ingestion/dlt_sources/chess/helpers.py +21 -0
- dango/ingestion/dlt_sources/chess/settings.py +4 -0
- dango/ingestion/dlt_sources/facebook_ads/README.md +72 -0
- dango/ingestion/dlt_sources/facebook_ads/__init__.py +214 -0
- dango/ingestion/dlt_sources/facebook_ads/exceptions.py +5 -0
- dango/ingestion/dlt_sources/facebook_ads/helpers.py +256 -0
- dango/ingestion/dlt_sources/facebook_ads/settings.py +192 -0
- dango/ingestion/dlt_sources/facebook_ads/utils.py +49 -0
- dango/ingestion/dlt_sources/freshdesk/README.md +71 -0
- dango/ingestion/dlt_sources/freshdesk/__init__.py +76 -0
- dango/ingestion/dlt_sources/freshdesk/freshdesk_client.py +102 -0
- dango/ingestion/dlt_sources/freshdesk/settings.py +9 -0
- dango/ingestion/dlt_sources/github/README.md +60 -0
- dango/ingestion/dlt_sources/github/__init__.py +149 -0
- dango/ingestion/dlt_sources/github/helpers.py +193 -0
- dango/ingestion/dlt_sources/github/queries.py +115 -0
- dango/ingestion/dlt_sources/github/settings.py +10 -0
- dango/ingestion/dlt_sources/google_ads/README.md +88 -0
- dango/ingestion/dlt_sources/google_ads/__init__.py +163 -0
- dango/ingestion/dlt_sources/google_ads/helpers/__init__.py +0 -0
- dango/ingestion/dlt_sources/google_ads/helpers/data_processing.py +20 -0
- dango/ingestion/dlt_sources/google_ads/setup_script_gcp_oauth.py +55 -0
- dango/ingestion/dlt_sources/google_analytics/README.md +59 -0
- dango/ingestion/dlt_sources/google_analytics/__init__.py +153 -0
- dango/ingestion/dlt_sources/google_analytics/helpers/__init__.py +72 -0
- dango/ingestion/dlt_sources/google_analytics/helpers/data_processing.py +189 -0
- dango/ingestion/dlt_sources/google_analytics/settings.py +3 -0
- dango/ingestion/dlt_sources/google_analytics/setup_script_gcp_oauth.py +55 -0
- dango/ingestion/dlt_sources/google_sheets/README.md +97 -0
- dango/ingestion/dlt_sources/google_sheets/__init__.py +152 -0
- dango/ingestion/dlt_sources/google_sheets/helpers/__init__.py +1 -0
- dango/ingestion/dlt_sources/google_sheets/helpers/api_calls.py +147 -0
- dango/ingestion/dlt_sources/google_sheets/helpers/data_processing.py +349 -0
- dango/ingestion/dlt_sources/google_sheets/setup_script_gcp_oauth.py +61 -0
- dango/ingestion/dlt_sources/hubspot/README.md +76 -0
- dango/ingestion/dlt_sources/hubspot/__init__.py +537 -0
- dango/ingestion/dlt_sources/hubspot/helpers.py +251 -0
- dango/ingestion/dlt_sources/hubspot/settings.py +130 -0
- dango/ingestion/dlt_sources/hubspot/utils.py +29 -0
- dango/ingestion/dlt_sources/inbox/README.md +101 -0
- dango/ingestion/dlt_sources/inbox/__init__.py +179 -0
- dango/ingestion/dlt_sources/inbox/helpers.py +186 -0
- dango/ingestion/dlt_sources/inbox/settings.py +5 -0
- dango/ingestion/dlt_sources/jira/README.md +80 -0
- dango/ingestion/dlt_sources/jira/__init__.py +138 -0
- dango/ingestion/dlt_sources/jira/settings.py +30 -0
- dango/ingestion/dlt_sources/kafka/README.md +82 -0
- dango/ingestion/dlt_sources/kafka/__init__.py +134 -0
- dango/ingestion/dlt_sources/kafka/helpers.py +262 -0
- dango/ingestion/dlt_sources/kafka/sources/kafka/__init__.py +0 -0
- dango/ingestion/dlt_sources/kinesis/README.md +82 -0
- dango/ingestion/dlt_sources/kinesis/__init__.py +130 -0
- dango/ingestion/dlt_sources/kinesis/helpers.py +63 -0
- dango/ingestion/dlt_sources/matomo/README.md +81 -0
- dango/ingestion/dlt_sources/matomo/__init__.py +223 -0
- dango/ingestion/dlt_sources/matomo/helpers/__init__.py +1 -0
- dango/ingestion/dlt_sources/matomo/helpers/data_processing.py +104 -0
- dango/ingestion/dlt_sources/matomo/helpers/matomo_client.py +170 -0
- dango/ingestion/dlt_sources/matomo/settings.py +3 -0
- dango/ingestion/dlt_sources/mongodb/README.md +81 -0
- dango/ingestion/dlt_sources/mongodb/__init__.py +164 -0
- dango/ingestion/dlt_sources/mongodb/helpers.py +665 -0
- dango/ingestion/dlt_sources/mux/README.md +56 -0
- dango/ingestion/dlt_sources/mux/__init__.py +88 -0
- dango/ingestion/dlt_sources/mux/settings.py +4 -0
- dango/ingestion/dlt_sources/notion/README.md +52 -0
- dango/ingestion/dlt_sources/notion/__init__.py +84 -0
- dango/ingestion/dlt_sources/notion/helpers/__init__.py +0 -0
- dango/ingestion/dlt_sources/notion/helpers/client.py +164 -0
- dango/ingestion/dlt_sources/notion/helpers/database.py +78 -0
- dango/ingestion/dlt_sources/notion/settings.py +3 -0
- dango/ingestion/dlt_sources/personio/README.md +87 -0
- dango/ingestion/dlt_sources/personio/__init__.py +330 -0
- dango/ingestion/dlt_sources/personio/helpers.py +85 -0
- dango/ingestion/dlt_sources/personio/settings.py +2 -0
- dango/ingestion/dlt_sources/pipedrive/README.md +78 -0
- dango/ingestion/dlt_sources/pipedrive/__init__.py +200 -0
- dango/ingestion/dlt_sources/pipedrive/helpers/__init__.py +20 -0
- dango/ingestion/dlt_sources/pipedrive/helpers/custom_fields_munger.py +102 -0
- dango/ingestion/dlt_sources/pipedrive/helpers/pages.py +115 -0
- dango/ingestion/dlt_sources/pipedrive/settings.py +29 -0
- dango/ingestion/dlt_sources/pipedrive/typing.py +4 -0
- dango/ingestion/dlt_sources/salesforce/README.md +131 -0
- dango/ingestion/dlt_sources/salesforce/__init__.py +148 -0
- dango/ingestion/dlt_sources/salesforce/helpers/__init__.py +0 -0
- dango/ingestion/dlt_sources/salesforce/helpers/client.py +214 -0
- dango/ingestion/dlt_sources/salesforce/helpers/records.py +121 -0
- dango/ingestion/dlt_sources/salesforce/settings.py +4 -0
- dango/ingestion/dlt_sources/shopify_dlt/README.md +61 -0
- dango/ingestion/dlt_sources/shopify_dlt/__init__.py +228 -0
- dango/ingestion/dlt_sources/shopify_dlt/exceptions.py +2 -0
- dango/ingestion/dlt_sources/shopify_dlt/helpers.py +146 -0
- dango/ingestion/dlt_sources/shopify_dlt/settings.py +5 -0
- dango/ingestion/dlt_sources/slack/README.md +95 -0
- dango/ingestion/dlt_sources/slack/__init__.py +288 -0
- dango/ingestion/dlt_sources/slack/helpers.py +205 -0
- dango/ingestion/dlt_sources/slack/settings.py +22 -0
- dango/ingestion/dlt_sources/strapi/README.md +58 -0
- dango/ingestion/dlt_sources/strapi/__init__.py +33 -0
- dango/ingestion/dlt_sources/strapi/helpers.py +42 -0
- dango/ingestion/dlt_sources/strapi/settings.py +1 -0
- dango/ingestion/dlt_sources/stripe_analytics/README.md +60 -0
- dango/ingestion/dlt_sources/stripe_analytics/__init__.py +118 -0
- dango/ingestion/dlt_sources/stripe_analytics/helpers.py +68 -0
- dango/ingestion/dlt_sources/stripe_analytics/metrics.py +95 -0
- dango/ingestion/dlt_sources/stripe_analytics/schemas.py +311 -0
- dango/ingestion/dlt_sources/stripe_analytics/settings.py +15 -0
- dango/ingestion/dlt_sources/workable/README.md +83 -0
- dango/ingestion/dlt_sources/workable/__init__.py +119 -0
- dango/ingestion/dlt_sources/workable/settings.py +30 -0
- dango/ingestion/dlt_sources/workable/workable_client.py +96 -0
- dango/ingestion/dlt_sources/zendesk/README.md +67 -0
- dango/ingestion/dlt_sources/zendesk/__init__.py +462 -0
- dango/ingestion/dlt_sources/zendesk/helpers/__init__.py +25 -0
- dango/ingestion/dlt_sources/zendesk/helpers/api_helpers.py +106 -0
- dango/ingestion/dlt_sources/zendesk/helpers/credentials.py +52 -0
- dango/ingestion/dlt_sources/zendesk/helpers/talk_api.py +116 -0
- dango/ingestion/dlt_sources/zendesk/settings.py +70 -0
- dango/ingestion/sources/__init__.py +9 -0
- dango/ingestion/sources/registry.py +1390 -0
- dango/platform/__init__.py +12 -0
- dango/platform/__main__.py +10 -0
- dango/platform/docker.py +380 -0
- dango/platform/network.py +509 -0
- dango/platform/watcher.py +531 -0
- dango/platform/watcher_runner.py +315 -0
- dango/templates/Dockerfile.metabase +40 -0
- dango/templates/__init__.py +5 -0
- dango/templates/dbt/schema.yml.j2 +21 -0
- dango/templates/dbt/sources.yml.j2 +48 -0
- dango/templates/dbt/staging_model.sql.j2 +31 -0
- dango/templates/docker-compose.yml.j2 +46 -0
- dango/templates/nginx.conf.j2 +110 -0
- dango/transformation/__init__.py +113 -0
- dango/transformation/generator.py +528 -0
- dango/utils/__init__.py +24 -0
- dango/utils/activity_log.py +54 -0
- dango/utils/data_validation.py +312 -0
- dango/utils/database.py +30 -0
- dango/utils/db_health.py +244 -0
- dango/utils/dbt_lock.py +236 -0
- dango/utils/dbt_status.py +106 -0
- dango/utils/sync_history.py +58 -0
- dango/visualization/__init__.py +9 -0
- dango/visualization/dashboard_manager.py +1102 -0
- dango/visualization/metabase.py +1046 -0
- dango/web/__init__.py +9 -0
- dango/web/app.py +2819 -0
- dango/web/static/css/main.css +200 -0
- dango/web/static/health.html +425 -0
- dango/web/static/index.html +528 -0
- dango/web/static/js/app.js +2154 -0
- dango/web/static/js/logs.js +473 -0
- dango/web/static/logs.html +251 -0
- getdango-0.0.1.dist-info/METADATA +300 -0
- getdango-0.0.1.dist-info/RECORD +189 -0
- getdango-0.0.1.dist-info/WHEEL +5 -0
- getdango-0.0.1.dist-info/entry_points.txt +2 -0
- getdango-0.0.1.dist-info/licenses/LICENSE +201 -0
- getdango-0.0.1.dist-info/top_level.txt +1 -0
dango/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dango - Local-first data platform
|
|
3
|
+
|
|
4
|
+
Laptop to cloud data stack in one weekend.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.0.1"
|
|
8
|
+
__author__ = "Dango Team"
|
|
9
|
+
__license__ = "Apache-2.0"
|
|
10
|
+
|
|
11
|
+
# Package metadata
|
|
12
|
+
__all__ = ["__version__", "__author__", "__license__"]
|
dango/cli/__init__.py
ADDED
dango/cli/db_helpers.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Database Helper Functions
|
|
3
|
+
|
|
4
|
+
Utilities for matching tables to source configurations, used by db status and db clean commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Dict, Set, Tuple
|
|
8
|
+
from dango.config import DangoConfig
|
|
9
|
+
from dango.ingestion.sources.registry import get_source_metadata
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_schema_table_mapping(config: DangoConfig) -> Tuple[Dict[str, Set[str]], Dict[str, str]]:
|
|
13
|
+
"""
|
|
14
|
+
Build mapping of schemas to expected tables based on source configurations.
|
|
15
|
+
|
|
16
|
+
For multi-resource sources (Stripe, Shopify, etc.):
|
|
17
|
+
- Schema: raw_{source_name} (e.g., raw_stripe_test)
|
|
18
|
+
- Tables: endpoint names (e.g., charge, customer, subscription)
|
|
19
|
+
|
|
20
|
+
For single-resource sources (CSV, etc.):
|
|
21
|
+
- Schema: raw
|
|
22
|
+
- Tables: source names (e.g., orders)
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
config: Dango configuration with source definitions
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tuple of:
|
|
29
|
+
- schema_to_tables: Dict[schema_name, Set[table_names]]
|
|
30
|
+
- source_to_schema: Dict[source_name, schema_name]
|
|
31
|
+
"""
|
|
32
|
+
schema_to_tables = {} # schema → set of table names
|
|
33
|
+
source_to_schema = {} # source_name → schema_name (for staging lookup)
|
|
34
|
+
|
|
35
|
+
for source in config.sources.sources:
|
|
36
|
+
source_name = source.name.lower()
|
|
37
|
+
|
|
38
|
+
# Get source metadata to check if multi-resource
|
|
39
|
+
metadata = get_source_metadata(source.type.value)
|
|
40
|
+
is_multi_resource = metadata.get("multi_resource", False) if metadata else False
|
|
41
|
+
|
|
42
|
+
if is_multi_resource:
|
|
43
|
+
# Multi-resource source: one schema per source, tables are endpoint names
|
|
44
|
+
schema_name = f"raw_{source_name}"
|
|
45
|
+
source_to_schema[source_name] = schema_name
|
|
46
|
+
|
|
47
|
+
# Get source-specific config
|
|
48
|
+
source_config = getattr(source, source.type.value, None)
|
|
49
|
+
if source_config:
|
|
50
|
+
source_dict = source_config.model_dump() if hasattr(source_config, 'model_dump') else {}
|
|
51
|
+
endpoints = source_dict.get('endpoints') or source_dict.get('resources') or source_dict.get('tables')
|
|
52
|
+
|
|
53
|
+
if endpoints:
|
|
54
|
+
if schema_name not in schema_to_tables:
|
|
55
|
+
schema_to_tables[schema_name] = set()
|
|
56
|
+
for endpoint in endpoints:
|
|
57
|
+
schema_to_tables[schema_name].add(endpoint.lower())
|
|
58
|
+
else:
|
|
59
|
+
# Single-resource source: 'raw' schema, table name is source name
|
|
60
|
+
if 'raw' not in schema_to_tables:
|
|
61
|
+
schema_to_tables['raw'] = set()
|
|
62
|
+
schema_to_tables['raw'].add(source_name)
|
|
63
|
+
source_to_schema[source_name] = 'raw'
|
|
64
|
+
|
|
65
|
+
return schema_to_tables, source_to_schema
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def is_table_configured(
|
|
69
|
+
schema: str,
|
|
70
|
+
table: str,
|
|
71
|
+
schema_to_tables: Dict[str, Set[str]],
|
|
72
|
+
source_to_schema: Dict[str, str]
|
|
73
|
+
) -> bool:
|
|
74
|
+
"""
|
|
75
|
+
Check if a table is configured in sources.yml
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
schema: Table schema name
|
|
79
|
+
table: Table name
|
|
80
|
+
schema_to_tables: Mapping from schema to expected tables
|
|
81
|
+
source_to_schema: Mapping from source name to schema
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
True if table is configured, False if orphaned
|
|
85
|
+
"""
|
|
86
|
+
# Skip dlt internal tables (always considered configured)
|
|
87
|
+
if table.startswith('_dlt_'):
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
# Raw tables: check schema-specific expected tables
|
|
91
|
+
if schema == 'raw' or schema.startswith('raw_'):
|
|
92
|
+
expected_in_schema = schema_to_tables.get(schema, set())
|
|
93
|
+
return table in expected_in_schema
|
|
94
|
+
|
|
95
|
+
# Staging tables: stg_{source_name}__{endpoint} or stg_{source_name}
|
|
96
|
+
elif schema == 'staging':
|
|
97
|
+
if table.startswith('stg_'):
|
|
98
|
+
# Try to match against source schemas
|
|
99
|
+
for source_name, raw_schema in source_to_schema.items():
|
|
100
|
+
# Check if staging table belongs to this source
|
|
101
|
+
if table.startswith(f"stg_{source_name}__") or table == f"stg_{source_name}":
|
|
102
|
+
# Extract endpoint/table name
|
|
103
|
+
if "__" in table:
|
|
104
|
+
endpoint = table.split("__", 1)[1]
|
|
105
|
+
else:
|
|
106
|
+
endpoint = source_name
|
|
107
|
+
|
|
108
|
+
# Check if this endpoint exists in the raw schema
|
|
109
|
+
expected_in_raw = schema_to_tables.get(raw_schema, set())
|
|
110
|
+
if endpoint in expected_in_raw or source_name in expected_in_raw:
|
|
111
|
+
return True
|
|
112
|
+
return False
|
|
113
|
+
else:
|
|
114
|
+
# Other staging tables - assume configured
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
# Intermediate and marts tables - always assume configured
|
|
118
|
+
# (these are custom models, not auto-generated)
|
|
119
|
+
elif schema in ('intermediate', 'marts'):
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
# Unknown schema - assume not configured
|
|
123
|
+
return False
|
dango/cli/env_helpers.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
.env File Helpers
|
|
3
|
+
|
|
4
|
+
Utilities for creating, validating, and managing .env files for credentials.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Dict, Set, Optional, Tuple
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def create_env_template(
|
|
18
|
+
env_file: Path,
|
|
19
|
+
env_vars: List[Dict[str, str]],
|
|
20
|
+
backup: bool = True
|
|
21
|
+
) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Create or update .env file with helpful templates for required variables.
|
|
24
|
+
|
|
25
|
+
Uses atomic write pattern to prevent data loss on failure.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
env_file: Path to .env file
|
|
29
|
+
env_vars: List of env var configs with name, help, format, example
|
|
30
|
+
backup: Whether to backup existing .env before modifying
|
|
31
|
+
"""
|
|
32
|
+
env_file.parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
|
|
34
|
+
# Read existing content (or start with header for new file)
|
|
35
|
+
if env_file.exists():
|
|
36
|
+
original_content = env_file.read_text()
|
|
37
|
+
existing_lines = original_content.splitlines()
|
|
38
|
+
else:
|
|
39
|
+
original_content = ""
|
|
40
|
+
existing_lines = [
|
|
41
|
+
"# Dango Data Platform - Environment Variables",
|
|
42
|
+
"# Add your credentials below",
|
|
43
|
+
""
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Backup existing file if requested
|
|
47
|
+
if backup and env_file.exists():
|
|
48
|
+
backup_file = env_file.with_suffix('.env.backup')
|
|
49
|
+
backup_file.write_text(original_content)
|
|
50
|
+
|
|
51
|
+
# Parse existing vars
|
|
52
|
+
existing_vars = set()
|
|
53
|
+
for line in existing_lines:
|
|
54
|
+
if '=' in line and not line.strip().startswith('#'):
|
|
55
|
+
var_name = line.split('=')[0].strip()
|
|
56
|
+
existing_vars.add(var_name)
|
|
57
|
+
|
|
58
|
+
# Build new vars section (only if not already present)
|
|
59
|
+
new_content = []
|
|
60
|
+
for var_config in env_vars:
|
|
61
|
+
var_name = var_config.get('name')
|
|
62
|
+
|
|
63
|
+
if var_name not in existing_vars:
|
|
64
|
+
# Add section header with source information
|
|
65
|
+
source_name = var_config.get('source_name')
|
|
66
|
+
source_type = var_config.get('source_type')
|
|
67
|
+
|
|
68
|
+
if source_name and source_type:
|
|
69
|
+
new_content.append(f"\n# {var_config.get('display_name', var_name)}")
|
|
70
|
+
new_content.append(f"# For source: '{source_name}' ({source_type})")
|
|
71
|
+
else:
|
|
72
|
+
new_content.append(f"\n# {var_config.get('display_name', var_name)}")
|
|
73
|
+
|
|
74
|
+
# Add help text
|
|
75
|
+
if 'help' in var_config:
|
|
76
|
+
new_content.append(f"# {var_config['help']}")
|
|
77
|
+
|
|
78
|
+
# Add format hint
|
|
79
|
+
if 'format' in var_config:
|
|
80
|
+
new_content.append(f"# Format: {var_config['format']}")
|
|
81
|
+
|
|
82
|
+
# Add example
|
|
83
|
+
if 'example' in var_config:
|
|
84
|
+
new_content.append(f"# Example: {var_config['example']}")
|
|
85
|
+
|
|
86
|
+
# Add empty var line
|
|
87
|
+
new_content.append(f"{var_name}=")
|
|
88
|
+
new_content.append("") # Blank line
|
|
89
|
+
|
|
90
|
+
# Atomic write: temp file + rename
|
|
91
|
+
if new_content or not env_file.exists():
|
|
92
|
+
temp_file = env_file.with_suffix('.env.tmp')
|
|
93
|
+
try:
|
|
94
|
+
# Write combined content to temp file
|
|
95
|
+
with open(temp_file, 'w') as f:
|
|
96
|
+
if existing_lines:
|
|
97
|
+
f.write('\n'.join(existing_lines))
|
|
98
|
+
if new_content:
|
|
99
|
+
f.write('\n'.join(new_content))
|
|
100
|
+
|
|
101
|
+
# Atomic rename
|
|
102
|
+
temp_file.replace(env_file)
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# Clean up temp file and restore original if it existed
|
|
106
|
+
try:
|
|
107
|
+
if temp_file.exists():
|
|
108
|
+
temp_file.unlink()
|
|
109
|
+
except:
|
|
110
|
+
pass
|
|
111
|
+
raise Exception(f"Failed to update .env file: {e}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def validate_env_file(
|
|
115
|
+
env_file: Path,
|
|
116
|
+
required_vars: List[str]
|
|
117
|
+
) -> Tuple[bool, List[str]]:
|
|
118
|
+
"""
|
|
119
|
+
Validate that .env file contains all required variables with non-empty values.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
env_file: Path to .env file
|
|
123
|
+
required_vars: List of required variable names
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Tuple of (all_valid, missing_vars)
|
|
127
|
+
"""
|
|
128
|
+
if not env_file.exists():
|
|
129
|
+
return False, required_vars
|
|
130
|
+
|
|
131
|
+
# Parse .env file
|
|
132
|
+
env_values = {}
|
|
133
|
+
for line in env_file.read_text().splitlines():
|
|
134
|
+
line = line.strip()
|
|
135
|
+
if line and not line.startswith('#') and '=' in line:
|
|
136
|
+
key, value = line.split('=', 1)
|
|
137
|
+
key = key.strip()
|
|
138
|
+
value = value.strip().strip('"').strip("'")
|
|
139
|
+
env_values[key] = value
|
|
140
|
+
|
|
141
|
+
# Check required vars
|
|
142
|
+
missing = []
|
|
143
|
+
for var in required_vars:
|
|
144
|
+
if var not in env_values or not env_values[var]:
|
|
145
|
+
missing.append(var)
|
|
146
|
+
|
|
147
|
+
return (len(missing) == 0, missing)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def open_file_in_default_app(filepath: Path) -> bool:
|
|
151
|
+
"""
|
|
152
|
+
Open file in OS default application (non-blocking).
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
filepath: Path to file to open
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
True if opened successfully, False otherwise
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
system = platform.system()
|
|
162
|
+
|
|
163
|
+
if system == "Darwin": # macOS
|
|
164
|
+
subprocess.Popen(["open", str(filepath)])
|
|
165
|
+
elif system == "Windows":
|
|
166
|
+
subprocess.Popen(["start", str(filepath)], shell=True)
|
|
167
|
+
elif system == "Linux":
|
|
168
|
+
subprocess.Popen(["xdg-open", str(filepath)])
|
|
169
|
+
else:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
return True
|
|
173
|
+
except Exception:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def guide_env_setup(
|
|
178
|
+
env_file: Path,
|
|
179
|
+
required_vars: List[Dict[str, str]],
|
|
180
|
+
source_name: str,
|
|
181
|
+
setup_guide: List[str] = None
|
|
182
|
+
) -> bool:
|
|
183
|
+
"""
|
|
184
|
+
Guide user through .env setup with optional file opening and validation.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
env_file: Path to .env file
|
|
188
|
+
required_vars: List of required var configs (with name, help, etc.)
|
|
189
|
+
source_name: Name of source being configured
|
|
190
|
+
setup_guide: Optional list of setup instructions to display
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
True if validated successfully, False otherwise
|
|
194
|
+
"""
|
|
195
|
+
from inquirer import prompt, Confirm
|
|
196
|
+
from inquirer.themes import GreenPassion
|
|
197
|
+
|
|
198
|
+
console.print(f"\n[bold cyan]📝 Credentials Setup[/bold cyan]")
|
|
199
|
+
console.print(f"File: [dim]{env_file.absolute()}[/dim]\n")
|
|
200
|
+
|
|
201
|
+
# Show detailed setup guide if provided
|
|
202
|
+
if setup_guide:
|
|
203
|
+
console.print("[bold]How to get your credentials:[/bold]")
|
|
204
|
+
for step in setup_guide:
|
|
205
|
+
console.print(f" {step}")
|
|
206
|
+
console.print()
|
|
207
|
+
|
|
208
|
+
# Show required credentials
|
|
209
|
+
console.print("[bold]Required credentials:[/bold]")
|
|
210
|
+
for var_config in required_vars:
|
|
211
|
+
var_name = var_config.get('name')
|
|
212
|
+
help_text = var_config.get('help', '')
|
|
213
|
+
console.print(f" • {var_name}")
|
|
214
|
+
if help_text:
|
|
215
|
+
console.print(f" [dim]{help_text}[/dim]")
|
|
216
|
+
|
|
217
|
+
# Offer to open file
|
|
218
|
+
console.print()
|
|
219
|
+
questions = [
|
|
220
|
+
Confirm(
|
|
221
|
+
'open_file',
|
|
222
|
+
message="Would you like me to open .env in your editor?",
|
|
223
|
+
default=True
|
|
224
|
+
)
|
|
225
|
+
]
|
|
226
|
+
answers = prompt(questions, theme=GreenPassion())
|
|
227
|
+
|
|
228
|
+
if answers and answers['open_file']:
|
|
229
|
+
if open_file_in_default_app(env_file):
|
|
230
|
+
console.print("[green]✅ Opened .env in your default editor[/green]")
|
|
231
|
+
console.print("\n[bold cyan]Next steps:[/bold cyan]")
|
|
232
|
+
console.print(" 1. Find the section with empty values (e.g., STRIPE_API_KEY=)")
|
|
233
|
+
console.print(" 2. Paste your credential after the = sign")
|
|
234
|
+
console.print(" 3. [bold]SAVE THE FILE[/bold] (Cmd+S or Ctrl+S)")
|
|
235
|
+
console.print(" 4. Return here and press Enter")
|
|
236
|
+
else:
|
|
237
|
+
console.print("[yellow]⚠️ Couldn't open automatically[/yellow]")
|
|
238
|
+
console.print(f"Please manually open: {env_file}")
|
|
239
|
+
console.print("\n[bold cyan]Then:[/bold cyan]")
|
|
240
|
+
console.print(" 1. Find the section with empty values")
|
|
241
|
+
console.print(" 2. Paste your credentials after the = sign")
|
|
242
|
+
console.print(" 3. [bold]SAVE THE FILE[/bold]")
|
|
243
|
+
|
|
244
|
+
# Wait for user to finish
|
|
245
|
+
console.print("\n[bold yellow]⚠️ Press Enter AFTER you've saved the .env file[/bold yellow]")
|
|
246
|
+
input()
|
|
247
|
+
|
|
248
|
+
# Validate
|
|
249
|
+
var_names = [v.get('name') for v in required_vars]
|
|
250
|
+
is_valid, missing = validate_env_file(env_file, var_names)
|
|
251
|
+
|
|
252
|
+
return handle_validation_result(
|
|
253
|
+
is_valid, missing, env_file, source_name
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def handle_validation_result(
|
|
258
|
+
is_valid: bool,
|
|
259
|
+
missing_vars: List[str],
|
|
260
|
+
env_file: Path,
|
|
261
|
+
source_name: str
|
|
262
|
+
) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Handle validation results with retry logic and graceful failure.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
is_valid: Whether all required vars are present
|
|
268
|
+
missing_vars: List of missing variable names
|
|
269
|
+
env_file: Path to .env file
|
|
270
|
+
source_name: Name of source being configured
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True if validation passed or user chose to skip
|
|
274
|
+
"""
|
|
275
|
+
from inquirer import prompt, List as InquirerList
|
|
276
|
+
from inquirer.themes import GreenPassion
|
|
277
|
+
|
|
278
|
+
if is_valid:
|
|
279
|
+
console.print("\n[green]✅ All credentials validated successfully![/green]")
|
|
280
|
+
return True
|
|
281
|
+
|
|
282
|
+
# Validation failed
|
|
283
|
+
console.print("\n[yellow]⚠️ Credential Validation Failed[/yellow]")
|
|
284
|
+
console.print("\nMissing or empty variables:")
|
|
285
|
+
for var in missing_vars:
|
|
286
|
+
console.print(f" • {var}")
|
|
287
|
+
|
|
288
|
+
console.print(f"\n[bold]Note:[/bold] Source '{source_name}' has been saved to sources.yml")
|
|
289
|
+
console.print("However, it won't work until you add valid credentials.\n")
|
|
290
|
+
|
|
291
|
+
# Offer retry
|
|
292
|
+
questions = [
|
|
293
|
+
InquirerList(
|
|
294
|
+
'action',
|
|
295
|
+
message="What would you like to do?",
|
|
296
|
+
choices=[
|
|
297
|
+
'Edit .env again and retry validation',
|
|
298
|
+
'Skip validation (I\'ll add credentials later)',
|
|
299
|
+
'Cancel source setup'
|
|
300
|
+
],
|
|
301
|
+
)
|
|
302
|
+
]
|
|
303
|
+
answers = prompt(questions, theme=GreenPassion())
|
|
304
|
+
|
|
305
|
+
if not answers:
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
action = answers['action']
|
|
309
|
+
|
|
310
|
+
if action == 'Edit .env again and retry validation':
|
|
311
|
+
# Reopen file
|
|
312
|
+
if open_file_in_default_app(env_file):
|
|
313
|
+
console.print("[green]✅ Reopened .env[/green]")
|
|
314
|
+
|
|
315
|
+
console.print("\n[bold]Press Enter when ready to retry validation...[/bold]")
|
|
316
|
+
input()
|
|
317
|
+
|
|
318
|
+
# Retry validation
|
|
319
|
+
is_valid_retry, missing_retry = validate_env_file(env_file, missing_vars)
|
|
320
|
+
return handle_validation_result(
|
|
321
|
+
is_valid_retry, missing_retry, env_file, source_name
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
elif action == 'Skip validation (I\'ll add credentials later)':
|
|
325
|
+
console.print("\n[yellow]⚠️ Skipping validation[/yellow]")
|
|
326
|
+
console.print("\n[cyan]Next steps:[/cyan]")
|
|
327
|
+
console.print(f" 1. Edit .env and add missing credentials")
|
|
328
|
+
console.print(f" 2. Validate with: [bold]dango config validate[/bold]")
|
|
329
|
+
console.print(f" 3. Or try syncing: [bold]dango sync --source {source_name}[/bold]\n")
|
|
330
|
+
return True # Allow wizard to complete
|
|
331
|
+
|
|
332
|
+
else: # Cancel
|
|
333
|
+
console.print("\n[red]Source setup cancelled[/red]")
|
|
334
|
+
console.print(f"The source config was saved but is incomplete.")
|
|
335
|
+
console.print(f"To remove it, edit .dango/sources.yml\n")
|
|
336
|
+
return False
|