frontier-runner 0.2.3__tar.gz → 0.2.4__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.
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/CHANGELOG.md +6 -0
- {frontier_runner-0.2.3/frontier_runner.egg-info → frontier_runner-0.2.4}/PKG-INFO +4 -4
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/README.md +3 -3
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/__init__.py +1 -1
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cli.py +105 -8
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/context.py +11 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/credentials.py +202 -5
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/commands.py +22 -5
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/profile_commands.py +6 -11
- frontier_runner-0.2.4/frontier/onboard/project_commands.py +310 -0
- frontier_runner-0.2.4/frontier/onboard/saas.py +594 -0
- frontier_runner-0.2.4/frontier/onboard/user_auth.py +182 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4/frontier_runner.egg-info}/PKG-INFO +4 -4
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier_runner.egg-info/SOURCES.txt +2 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/pyproject.toml +1 -1
- frontier_runner-0.2.3/frontier/onboard/saas.py +0 -276
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/LICENSE +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/MANIFEST.in +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/__main__.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/adapters/__init__.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/adapters/base.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/adapters/bigquery.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/adapters/databricks.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/adapters/postgres.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/adapters/redshift.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/adapters/snowflake.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/api.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/artifacts.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/__init__.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/config.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/consume.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/normalize.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/prove.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/route.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/store.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/streams.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/cdc/upload.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/certification.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/comment.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/compare.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/config.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/dbt_artifacts.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/environment.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/errors.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/execute.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/filter_v1.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/frontier.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/github.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/hashing.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/identity.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/impact.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/local_config.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/mutation.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/__init__.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/constants.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/demo.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/detect.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/discover.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/doctor.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/github.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/gitignore.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/hashkey.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/permissions.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/prompt.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/routes.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/onboard/versions.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/profiles.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/progress.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/proof.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/repair.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/semantic.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/snapshot.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/snowflake.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/snowflake_sql.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/sql_fingerprint.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/validation.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier/warehouse.py +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier_runner.egg-info/dependency_links.txt +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier_runner.egg-info/entry_points.txt +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier_runner.egg-info/requires.txt +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/frontier_runner.egg-info/top_level.txt +0 -0
- {frontier_runner-0.2.3 → frontier_runner-0.2.4}/setup.cfg +0 -0
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.4] — 2026-09-27
|
|
11
|
+
|
|
12
|
+
- CLI browser login (`frontier login`) uses a PKCE device-code flow and stores a short-lived user token separately from project API keys.
|
|
13
|
+
- `frontier project create` / `frontier profile create --create-project` create a SaaS project with user authorization, issue a project key, and bind a named Frontier profile without printing the key.
|
|
14
|
+
|
|
10
15
|
## [0.2.3] — 2026-09-27
|
|
11
16
|
|
|
12
17
|
- Named Frontier profiles: local execution contexts that bind a SaaS project credential, API origin, optional dbt target, and optional profiles.yml path without replacing `dbt_project.yml` `profile:`.
|
|
@@ -107,6 +112,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
107
112
|
- SaaS commands resolve stored keychain credentials through one shared
|
|
108
113
|
resolver instead of requiring `FRONTIER_API_KEY` in the environment.
|
|
109
114
|
|
|
115
|
+
[0.2.4]: https://github.com/jadsamara/frontier-runner/compare/v0.2.3...v0.2.4
|
|
110
116
|
[0.2.3]: https://github.com/jadsamara/frontier-runner/compare/v0.2.2...v0.2.3
|
|
111
117
|
[0.2.2]: https://github.com/jadsamara/frontier-runner/compare/v0.2.1...v0.2.2
|
|
112
118
|
[0.2.1]: https://github.com/jadsamara/frontier-runner/compare/v0.2.0...v0.2.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: frontier-runner
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Customer-side Frontier CLI for dbt + Snowflake or BigQuery + GitHub impact assessments.
|
|
5
5
|
Author: Frontier
|
|
6
6
|
License: MIT License
|
|
@@ -92,7 +92,7 @@ Until the package is on PyPI, install the GitHub Release wheel for a version
|
|
|
92
92
|
tag. You do not need a commit SHA:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
-
pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.
|
|
95
|
+
pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.4/frontier_runner-0.2.4-py3-none-any.whl"
|
|
96
96
|
pipx inject frontier-runner "snowflake-connector-python>=3.12,<4"
|
|
97
97
|
```
|
|
98
98
|
|
|
@@ -248,9 +248,9 @@ workflow uses `FRONTIER_BLOCKING=false` unless you pass `--blocking`.
|
|
|
248
248
|
Pin an immutable released version:
|
|
249
249
|
|
|
250
250
|
```bash
|
|
251
|
-
pip install "frontier-runner[snowflake]==0.2.
|
|
251
|
+
pip install "frontier-runner[snowflake]==0.2.4"
|
|
252
252
|
# or
|
|
253
|
-
pip install "frontier-runner[bigquery]==0.2.
|
|
253
|
+
pip install "frontier-runner[bigquery]==0.2.4"
|
|
254
254
|
```
|
|
255
255
|
|
|
256
256
|
Until PyPI trusted publishing is reviewed and live, install the GitHub Release
|
|
@@ -34,7 +34,7 @@ Until the package is on PyPI, install the GitHub Release wheel for a version
|
|
|
34
34
|
tag. You do not need a commit SHA:
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.
|
|
37
|
+
pipx install \ "frontier-runner[snowflake] @ https://github.com/jadsamara/frontier-runner/releases/download/v0.2.4/frontier_runner-0.2.4-py3-none-any.whl"
|
|
38
38
|
pipx inject frontier-runner "snowflake-connector-python>=3.12,<4"
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -190,9 +190,9 @@ workflow uses `FRONTIER_BLOCKING=false` unless you pass `--blocking`.
|
|
|
190
190
|
Pin an immutable released version:
|
|
191
191
|
|
|
192
192
|
```bash
|
|
193
|
-
pip install "frontier-runner[snowflake]==0.2.
|
|
193
|
+
pip install "frontier-runner[snowflake]==0.2.4"
|
|
194
194
|
# or
|
|
195
|
-
pip install "frontier-runner[bigquery]==0.2.
|
|
195
|
+
pip install "frontier-runner[bigquery]==0.2.4"
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
Until PyPI trusted publishing is reviewed and live, install the GitHub Release
|
|
@@ -148,6 +148,14 @@ from frontier.onboard.profile_commands import (
|
|
|
148
148
|
cmd_profile_status,
|
|
149
149
|
cmd_profile_use,
|
|
150
150
|
)
|
|
151
|
+
from frontier.onboard.project_commands import (
|
|
152
|
+
cmd_organization_list,
|
|
153
|
+
cmd_project_create,
|
|
154
|
+
cmd_project_key_create,
|
|
155
|
+
cmd_project_list,
|
|
156
|
+
cmd_project_status,
|
|
157
|
+
)
|
|
158
|
+
from frontier.onboard.user_auth import cmd_auth_user_logout, cmd_auth_user_status
|
|
151
159
|
from frontier.onboard.constants import DEFAULT_API_URL
|
|
152
160
|
from frontier.onboard.routes import (
|
|
153
161
|
derive_source_route,
|
|
@@ -2681,15 +2689,23 @@ def _add_project_dir(parser: argparse.ArgumentParser) -> None:
|
|
|
2681
2689
|
dest="project_dir_opt",
|
|
2682
2690
|
help="dbt project directory (same as the positional path)",
|
|
2683
2691
|
)
|
|
2684
|
-
_add_profile_option(parser)
|
|
2692
|
+
_add_profile_option(parser, inherit_parent=True)
|
|
2685
2693
|
|
|
2686
2694
|
|
|
2687
|
-
def _add_profile_option(
|
|
2695
|
+
def _add_profile_option(
|
|
2696
|
+
parser: argparse.ArgumentParser,
|
|
2697
|
+
*,
|
|
2698
|
+
inherit_parent: bool = False,
|
|
2699
|
+
) -> None:
|
|
2700
|
+
# Nested subparsers overwrite a parent dest when they declare the same
|
|
2701
|
+
# option with default=None. SUPPRESS lets `frontier --profile b …` win
|
|
2702
|
+
# over a later subparser default, while still accepting `--profile` after
|
|
2703
|
+
# the subcommand.
|
|
2688
2704
|
parser.add_argument(
|
|
2689
2705
|
"--profile",
|
|
2690
2706
|
dest="frontier_profile",
|
|
2691
2707
|
metavar="NAME",
|
|
2692
|
-
default=None,
|
|
2708
|
+
default=argparse.SUPPRESS if inherit_parent else None,
|
|
2693
2709
|
help=FRONTIER_PROFILE_HELP,
|
|
2694
2710
|
)
|
|
2695
2711
|
|
|
@@ -2979,14 +2995,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2979
2995
|
signup.add_argument("--api-url", help="SaaS origin")
|
|
2980
2996
|
signup.set_defaults(func=cmd_signup)
|
|
2981
2997
|
|
|
2982
|
-
login = sub.add_parser("login", help="
|
|
2998
|
+
login = sub.add_parser("login", help="Sign in as a Frontier user, or store a project API key")
|
|
2983
2999
|
login.add_argument("--api-key", action="store_true", help="Paste a project API key (hidden input). Never pass the key as a positional argument.")
|
|
2984
3000
|
login.add_argument("--api-url", help="SaaS origin")
|
|
2985
|
-
_add_profile_option(login)
|
|
3001
|
+
_add_profile_option(login, inherit_parent=True)
|
|
2986
3002
|
login.set_defaults(func=cmd_login)
|
|
2987
3003
|
|
|
2988
3004
|
logout = sub.add_parser("logout", help="Remove stored Frontier API credentials")
|
|
2989
|
-
_add_profile_option(logout)
|
|
3005
|
+
_add_profile_option(logout, inherit_parent=True)
|
|
2990
3006
|
logout.add_argument(
|
|
2991
3007
|
"--remove-metadata",
|
|
2992
3008
|
action="store_true",
|
|
@@ -2997,8 +3013,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2997
3013
|
auth = sub.add_parser("auth", help="Show authentication status")
|
|
2998
3014
|
auth_sub = auth.add_subparsers(dest="auth_command", required=True)
|
|
2999
3015
|
auth_status = auth_sub.add_parser("status", help="Show whether the CLI is authenticated")
|
|
3000
|
-
_add_profile_option(auth_status)
|
|
3016
|
+
_add_profile_option(auth_status, inherit_parent=True)
|
|
3017
|
+
auth_status.add_argument("--api-url", help="SaaS origin")
|
|
3001
3018
|
auth_status.set_defaults(func=cmd_auth_status)
|
|
3019
|
+
auth_user_status = auth_sub.add_parser("user-status", help="Show Frontier user authorization")
|
|
3020
|
+
auth_user_status.add_argument("--api-url", help="SaaS origin")
|
|
3021
|
+
auth_user_status.set_defaults(func=cmd_auth_user_status)
|
|
3022
|
+
auth_user_logout = auth_sub.add_parser("user-logout", help="Revoke stored Frontier user authorization")
|
|
3023
|
+
auth_user_logout.add_argument("--api-url", help="SaaS origin")
|
|
3024
|
+
auth_user_logout.set_defaults(func=cmd_auth_user_logout)
|
|
3002
3025
|
|
|
3003
3026
|
profile = sub.add_parser(
|
|
3004
3027
|
"profile",
|
|
@@ -3008,6 +3031,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3008
3031
|
"dbt `profile:` in dbt_project.yml."
|
|
3009
3032
|
),
|
|
3010
3033
|
)
|
|
3034
|
+
_add_profile_option(profile, inherit_parent=True)
|
|
3011
3035
|
profile_sub = profile.add_subparsers(dest="profile_command", required=True)
|
|
3012
3036
|
profile_create = profile_sub.add_parser(
|
|
3013
3037
|
"create",
|
|
@@ -3022,6 +3046,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3022
3046
|
action="store_true",
|
|
3023
3047
|
help="Prompt securely for the project API key (never pass the key as an argument)",
|
|
3024
3048
|
)
|
|
3049
|
+
profile_create.add_argument(
|
|
3050
|
+
"--create-project",
|
|
3051
|
+
metavar="NAME",
|
|
3052
|
+
help="Create a SaaS project with this name, issue a key, and bind this Frontier profile",
|
|
3053
|
+
)
|
|
3054
|
+
profile_create.add_argument("--organization", help="Organization ID or unambiguous slug")
|
|
3055
|
+
profile_create.add_argument("--use", action="store_true", help="Select this Frontier profile for the current repository")
|
|
3056
|
+
profile_create.add_argument("--non-interactive", action="store_true", help="Do not prompt; require --organization when multiple orgs exist")
|
|
3025
3057
|
profile_create.add_argument("--force", action="store_true", help="Replace an existing Frontier profile")
|
|
3026
3058
|
profile_create.add_argument("--yes", action="store_true", help="Accept the default API origin")
|
|
3027
3059
|
_add_project_dir(profile_create)
|
|
@@ -3048,6 +3080,61 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3048
3080
|
_add_project_dir(profile_remove)
|
|
3049
3081
|
profile_remove.set_defaults(func=cmd_profile_remove)
|
|
3050
3082
|
|
|
3083
|
+
organization = sub.add_parser("organization", help="List organizations for the signed-in Frontier user")
|
|
3084
|
+
organization_sub = organization.add_subparsers(dest="organization_command", required=True)
|
|
3085
|
+
organization_list = organization_sub.add_parser("list", help="List organizations you can access")
|
|
3086
|
+
organization_list.add_argument("--api-url", help="SaaS origin")
|
|
3087
|
+
organization_list.set_defaults(func=cmd_organization_list)
|
|
3088
|
+
|
|
3089
|
+
project = sub.add_parser("project", help="Create and inspect Frontier SaaS projects")
|
|
3090
|
+
project_sub = project.add_subparsers(dest="project_command", required=True)
|
|
3091
|
+
project_list = project_sub.add_parser("list", help="List accessible Frontier projects")
|
|
3092
|
+
project_list.add_argument("--api-url", help="SaaS origin")
|
|
3093
|
+
project_list.set_defaults(func=cmd_project_list)
|
|
3094
|
+
project_status = project_sub.add_parser("status", help="Show one accessible Frontier project")
|
|
3095
|
+
project_status.add_argument("project", help="Project name or ID")
|
|
3096
|
+
project_status.add_argument("--api-url", help="SaaS origin")
|
|
3097
|
+
project_status.set_defaults(func=cmd_project_status)
|
|
3098
|
+
project_create = project_sub.add_parser("create", help="Create a Frontier SaaS project")
|
|
3099
|
+
project_create.add_argument("name", help="SaaS project name")
|
|
3100
|
+
project_create.add_argument("--organization", help="Organization ID or unambiguous slug")
|
|
3101
|
+
project_create.add_argument("--profile", dest="profile", help="Create this named Frontier profile after issuing a key")
|
|
3102
|
+
project_create.add_argument("--target", help="Default dbt target for the Frontier profile")
|
|
3103
|
+
project_create.add_argument("--profiles", help="Optional profiles.yml path for the Frontier profile")
|
|
3104
|
+
project_create.add_argument("--api-url", help="SaaS origin")
|
|
3105
|
+
project_create.add_argument("--warehouse", help="Warehouse type (default snowflake)")
|
|
3106
|
+
project_create.add_argument("--no-profile", action="store_true", help="Do not create a named Frontier profile")
|
|
3107
|
+
project_create.add_argument("--use", action="store_true", help="Select the new Frontier profile for this repository")
|
|
3108
|
+
project_create.add_argument("--non-interactive", action="store_true", help="Do not prompt")
|
|
3109
|
+
project_create.add_argument("--force", action="store_true", help="Replace an existing Frontier profile")
|
|
3110
|
+
project_create.add_argument(
|
|
3111
|
+
"project_dir",
|
|
3112
|
+
nargs="?",
|
|
3113
|
+
default=".",
|
|
3114
|
+
help="dbt project directory (default: current directory)",
|
|
3115
|
+
)
|
|
3116
|
+
project_create.add_argument(
|
|
3117
|
+
"--project-dir",
|
|
3118
|
+
dest="project_dir_opt",
|
|
3119
|
+
help="dbt project directory for --use",
|
|
3120
|
+
)
|
|
3121
|
+
project_create.set_defaults(func=cmd_project_create)
|
|
3122
|
+
project_key = project_sub.add_parser("key", help="Manage project API keys")
|
|
3123
|
+
project_key_sub = project_key.add_subparsers(dest="project_key_command", required=True)
|
|
3124
|
+
project_key_create = project_key_sub.add_parser("create", help="Issue a replacement project API key")
|
|
3125
|
+
project_key_create.add_argument("project", help="Project name or ID")
|
|
3126
|
+
project_key_create.add_argument("--profile", dest="profile", help="Store the key on this Frontier profile")
|
|
3127
|
+
project_key_create.add_argument("--target", help="Default dbt target when creating profile metadata")
|
|
3128
|
+
project_key_create.add_argument("--profiles", help="Optional profiles.yml path")
|
|
3129
|
+
project_key_create.add_argument("--api-url", help="SaaS origin")
|
|
3130
|
+
project_key_create.add_argument("--non-interactive", action="store_true", help="Do not prompt")
|
|
3131
|
+
project_key_create.add_argument(
|
|
3132
|
+
"--project-dir",
|
|
3133
|
+
dest="project_dir_opt",
|
|
3134
|
+
help="dbt project directory",
|
|
3135
|
+
)
|
|
3136
|
+
project_key_create.set_defaults(func=cmd_project_key_create)
|
|
3137
|
+
|
|
3051
3138
|
discover = sub.add_parser(
|
|
3052
3139
|
"discover",
|
|
3053
3140
|
help="Derive and upload a generated semantic configuration from dbt artifacts",
|
|
@@ -3275,7 +3362,17 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
3275
3362
|
parser = build_parser()
|
|
3276
3363
|
args = parser.parse_args(argv)
|
|
3277
3364
|
command = getattr(args, "command", None)
|
|
3278
|
-
if command not in {
|
|
3365
|
+
if command not in {
|
|
3366
|
+
"update-check",
|
|
3367
|
+
"doctor",
|
|
3368
|
+
"login",
|
|
3369
|
+
"logout",
|
|
3370
|
+
"signup",
|
|
3371
|
+
"profile",
|
|
3372
|
+
"organization",
|
|
3373
|
+
"project",
|
|
3374
|
+
"auth",
|
|
3375
|
+
}:
|
|
3279
3376
|
from frontier.onboard.constants import DEFAULT_API_URL
|
|
3280
3377
|
|
|
3281
3378
|
maybe_version_notice(str(getattr(args, "api_url", None) or DEFAULT_API_URL))
|
|
@@ -56,6 +56,17 @@ SOURCE_ENV_PROFILE = "env"
|
|
|
56
56
|
SOURCE_LOCAL = "local"
|
|
57
57
|
SOURCE_NONE = "none"
|
|
58
58
|
|
|
59
|
+
PROFILE_SOURCE_LABELS = {
|
|
60
|
+
SOURCE_FLAG: "--profile",
|
|
61
|
+
SOURCE_ENV_PROFILE: "FRONTIER_PROFILE",
|
|
62
|
+
SOURCE_LOCAL: "local selection",
|
|
63
|
+
SOURCE_NONE: "none (legacy)",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def profile_source_label(source: str) -> str:
|
|
68
|
+
return PROFILE_SOURCE_LABELS.get(source, source)
|
|
69
|
+
|
|
59
70
|
|
|
60
71
|
@dataclass(frozen=True)
|
|
61
72
|
class ExecutionContext:
|
|
@@ -17,6 +17,8 @@ SOURCE_KEYRING = "keyring"
|
|
|
17
17
|
SOURCE_FILE = "file"
|
|
18
18
|
SOURCE_DEMO = "FRONTIER_DEMO_API_KEY"
|
|
19
19
|
PROFILE_KEYRING_PREFIX = "profile:"
|
|
20
|
+
USER_KEYRING_PREFIX = "user:"
|
|
21
|
+
SOURCE_USER_ENV = "FRONTIER_USER_TOKEN"
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
@dataclass(frozen=True)
|
|
@@ -69,6 +71,11 @@ def keyring_username(profile_name: str | None = None) -> str:
|
|
|
69
71
|
return f"{PROFILE_KEYRING_PREFIX}{name}"
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
def user_keyring_username(api_url: str) -> str:
|
|
75
|
+
origin = (api_url or "").strip().rstrip("/").lower()
|
|
76
|
+
return f"{USER_KEYRING_PREFIX}{origin or 'default'}"
|
|
77
|
+
|
|
78
|
+
|
|
72
79
|
def key_prefix(api_key: str) -> str:
|
|
73
80
|
key = api_key.strip()
|
|
74
81
|
if len(key) <= 12:
|
|
@@ -248,17 +255,37 @@ def _read_file_store(path: Path) -> tuple[StoredCredentials | None, dict[str, St
|
|
|
248
255
|
return parsed, {}
|
|
249
256
|
|
|
250
257
|
|
|
258
|
+
def _read_user_store(path: Path) -> dict[str, "StoredUserAuthorization"]:
|
|
259
|
+
if not path.is_file():
|
|
260
|
+
return {}
|
|
261
|
+
try:
|
|
262
|
+
payload = json.loads(path.read_text())
|
|
263
|
+
except json.JSONDecodeError:
|
|
264
|
+
return {}
|
|
265
|
+
if not isinstance(payload, dict) or not isinstance(payload.get("users"), dict):
|
|
266
|
+
return {}
|
|
267
|
+
users: dict[str, StoredUserAuthorization] = {}
|
|
268
|
+
for origin, item in payload["users"].items():
|
|
269
|
+
if not isinstance(item, dict):
|
|
270
|
+
continue
|
|
271
|
+
parsed = _user_from_mapping(item, source=SOURCE_FILE)
|
|
272
|
+
if parsed:
|
|
273
|
+
users[str(origin)] = parsed
|
|
274
|
+
return users
|
|
275
|
+
|
|
276
|
+
|
|
251
277
|
def _write_file_store(
|
|
252
278
|
path: Path,
|
|
253
279
|
*,
|
|
254
280
|
legacy: StoredCredentials | None,
|
|
255
281
|
named: dict[str, StoredCredentials],
|
|
256
282
|
) -> None:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
283
|
+
users = _read_user_store(path)
|
|
284
|
+
if not named and legacy is None and not users:
|
|
285
|
+
if path.is_file():
|
|
286
|
+
path.unlink()
|
|
287
|
+
return
|
|
288
|
+
if not named and not users and legacy is not None:
|
|
262
289
|
_write_fallback(path, json.dumps(legacy.to_payload()))
|
|
263
290
|
return
|
|
264
291
|
payload: dict[str, Any] = {
|
|
@@ -267,6 +294,10 @@ def _write_file_store(
|
|
|
267
294
|
}
|
|
268
295
|
if legacy is not None:
|
|
269
296
|
payload["legacy"] = legacy.to_payload()
|
|
297
|
+
if users:
|
|
298
|
+
payload["users"] = {origin: auth.to_payload() for origin, auth in users.items()}
|
|
299
|
+
if not payload["profiles"]:
|
|
300
|
+
payload.pop("profiles")
|
|
270
301
|
_write_fallback(path, json.dumps(payload))
|
|
271
302
|
|
|
272
303
|
|
|
@@ -409,3 +440,169 @@ def delete_credentials(
|
|
|
409
440
|
|
|
410
441
|
def redact_credentials_dict(value: dict[str, Any]) -> dict[str, Any]:
|
|
411
442
|
return redact(value)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@dataclass(frozen=True)
|
|
446
|
+
class StoredUserAuthorization:
|
|
447
|
+
api_url: str
|
|
448
|
+
access_token: str
|
|
449
|
+
email: str
|
|
450
|
+
user_id: str
|
|
451
|
+
expires_at: str
|
|
452
|
+
source: str = SOURCE_KEYRING
|
|
453
|
+
issuer: str = ""
|
|
454
|
+
audience: str = "frontier-cli"
|
|
455
|
+
|
|
456
|
+
def to_payload(self) -> dict[str, str]:
|
|
457
|
+
return {
|
|
458
|
+
"apiUrl": self.api_url,
|
|
459
|
+
"accessToken": self.access_token,
|
|
460
|
+
"email": self.email,
|
|
461
|
+
"userId": self.user_id,
|
|
462
|
+
"expiresAt": self.expires_at,
|
|
463
|
+
"issuer": self.issuer or self.api_url,
|
|
464
|
+
"audience": self.audience,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
def expired(self) -> bool:
|
|
468
|
+
value = (self.expires_at or "").strip()
|
|
469
|
+
if not value:
|
|
470
|
+
return True
|
|
471
|
+
try:
|
|
472
|
+
from datetime import datetime, timezone
|
|
473
|
+
|
|
474
|
+
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
475
|
+
except ValueError:
|
|
476
|
+
return True
|
|
477
|
+
now = datetime.now(tz=timezone.utc)
|
|
478
|
+
if parsed.tzinfo is None:
|
|
479
|
+
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
480
|
+
return parsed <= now
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def _user_from_mapping(payload: dict[str, Any], *, source: str) -> StoredUserAuthorization | None:
|
|
484
|
+
token = str(payload.get("accessToken") or "").strip()
|
|
485
|
+
url = str(payload.get("apiUrl") or "").strip()
|
|
486
|
+
if not token or not url:
|
|
487
|
+
return None
|
|
488
|
+
return StoredUserAuthorization(
|
|
489
|
+
api_url=url.rstrip("/"),
|
|
490
|
+
access_token=token,
|
|
491
|
+
email=str(payload.get("email") or "").strip(),
|
|
492
|
+
user_id=str(payload.get("userId") or "").strip(),
|
|
493
|
+
expires_at=str(payload.get("expiresAt") or "").strip(),
|
|
494
|
+
source=source,
|
|
495
|
+
issuer=str(payload.get("issuer") or url).strip().rstrip("/"),
|
|
496
|
+
audience=str(payload.get("audience") or "frontier-cli").strip() or "frontier-cli",
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _write_user_file_store(path: Path, users: dict[str, StoredUserAuthorization]) -> None:
|
|
501
|
+
legacy, named = _read_file_store(path)
|
|
502
|
+
if not named and legacy is None and not users:
|
|
503
|
+
if path.is_file():
|
|
504
|
+
path.unlink()
|
|
505
|
+
return
|
|
506
|
+
if not named and not users and legacy is not None:
|
|
507
|
+
_write_fallback(path, json.dumps(legacy.to_payload()))
|
|
508
|
+
return
|
|
509
|
+
payload: dict[str, Any] = {
|
|
510
|
+
"version": 1,
|
|
511
|
+
"profiles": {name: creds.to_payload() for name, creds in named.items()},
|
|
512
|
+
"users": {origin: auth.to_payload() for origin, auth in users.items()},
|
|
513
|
+
}
|
|
514
|
+
if legacy is not None:
|
|
515
|
+
payload["legacy"] = legacy.to_payload()
|
|
516
|
+
if not payload["profiles"]:
|
|
517
|
+
payload.pop("profiles")
|
|
518
|
+
if not payload["users"]:
|
|
519
|
+
payload.pop("users")
|
|
520
|
+
_write_fallback(path, json.dumps(payload))
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def save_user_authorization(
|
|
524
|
+
auth: StoredUserAuthorization,
|
|
525
|
+
*,
|
|
526
|
+
fallback_path: Path | None = None,
|
|
527
|
+
) -> str:
|
|
528
|
+
encoded = json.dumps(auth.to_payload())
|
|
529
|
+
if _set_keyring_entry(user_keyring_username(auth.api_url), encoded):
|
|
530
|
+
return SOURCE_KEYRING
|
|
531
|
+
path = fallback_path or default_fallback_path()
|
|
532
|
+
users = _read_user_store(path)
|
|
533
|
+
stored = StoredUserAuthorization(
|
|
534
|
+
api_url=auth.api_url,
|
|
535
|
+
access_token=auth.access_token,
|
|
536
|
+
email=auth.email,
|
|
537
|
+
user_id=auth.user_id,
|
|
538
|
+
expires_at=auth.expires_at,
|
|
539
|
+
source=SOURCE_FILE,
|
|
540
|
+
issuer=auth.issuer,
|
|
541
|
+
audience=auth.audience,
|
|
542
|
+
)
|
|
543
|
+
users[auth.api_url.rstrip("/").lower()] = stored
|
|
544
|
+
_write_user_file_store(path, users)
|
|
545
|
+
mode = path.stat().st_mode & 0o777
|
|
546
|
+
if mode != 0o600:
|
|
547
|
+
raise ConfigError("PROJECT_KEY_STORAGE_FAILED: credential file must be mode 0600.")
|
|
548
|
+
return SOURCE_FILE
|
|
549
|
+
|
|
550
|
+
|
|
551
|
+
def load_user_authorization(
|
|
552
|
+
api_url: str,
|
|
553
|
+
*,
|
|
554
|
+
fallback_path: Path | None = None,
|
|
555
|
+
) -> StoredUserAuthorization | None:
|
|
556
|
+
env_token = (os.environ.get(SOURCE_USER_ENV) or "").strip()
|
|
557
|
+
env_url = (os.environ.get("FRONTIER_API_URL") or "").strip() or api_url
|
|
558
|
+
if env_token:
|
|
559
|
+
auth = StoredUserAuthorization(
|
|
560
|
+
api_url=env_url.rstrip("/"),
|
|
561
|
+
access_token=env_token,
|
|
562
|
+
email="",
|
|
563
|
+
user_id="",
|
|
564
|
+
expires_at="9999-12-31T00:00:00+00:00",
|
|
565
|
+
source=SOURCE_USER_ENV,
|
|
566
|
+
)
|
|
567
|
+
return None if auth.expired() else auth
|
|
568
|
+
origin = api_url.rstrip("/")
|
|
569
|
+
raw = _get_keyring_entry(user_keyring_username(origin))
|
|
570
|
+
if raw:
|
|
571
|
+
try:
|
|
572
|
+
payload = json.loads(raw)
|
|
573
|
+
except json.JSONDecodeError:
|
|
574
|
+
payload = None
|
|
575
|
+
parsed = _user_from_mapping(payload, source=SOURCE_KEYRING) if isinstance(payload, dict) else None
|
|
576
|
+
if parsed and not parsed.expired():
|
|
577
|
+
return parsed
|
|
578
|
+
path = fallback_path or default_fallback_path()
|
|
579
|
+
stored = _read_user_store(path).get(origin.lower())
|
|
580
|
+
if stored and not stored.expired():
|
|
581
|
+
return stored
|
|
582
|
+
return None
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def delete_user_authorization(
|
|
586
|
+
api_url: str,
|
|
587
|
+
*,
|
|
588
|
+
fallback_path: Path | None = None,
|
|
589
|
+
) -> None:
|
|
590
|
+
origin = api_url.rstrip("/")
|
|
591
|
+
_delete_keyring_entry(user_keyring_username(origin))
|
|
592
|
+
path = fallback_path or default_fallback_path()
|
|
593
|
+
users = _read_user_store(path)
|
|
594
|
+
users.pop(origin.lower(), None)
|
|
595
|
+
_write_user_file_store(path, users)
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
def user_authorization_backend(api_url: str) -> str | None:
|
|
599
|
+
origin = api_url.rstrip("/")
|
|
600
|
+
raw = _get_keyring_entry(user_keyring_username(origin))
|
|
601
|
+
if raw:
|
|
602
|
+
return SOURCE_KEYRING
|
|
603
|
+
path = default_fallback_path()
|
|
604
|
+
if origin.lower() in _read_user_store(path):
|
|
605
|
+
return SOURCE_FILE
|
|
606
|
+
if (os.environ.get(SOURCE_USER_ENV) or "").strip():
|
|
607
|
+
return SOURCE_USER_ENV
|
|
608
|
+
return None
|
|
@@ -94,10 +94,14 @@ def cmd_login(args: Any) -> int:
|
|
|
94
94
|
profile_name = (getattr(args, "frontier_profile", None) or "").strip() or None
|
|
95
95
|
api_url = (getattr(args, "api_url", None) or DEFAULT_API_URL).rstrip("/")
|
|
96
96
|
if not getattr(args, "api_key", False):
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
97
|
+
from frontier.onboard.user_auth import complete_user_login
|
|
98
|
+
|
|
99
|
+
return complete_user_login(
|
|
100
|
+
args,
|
|
101
|
+
sleeper=getattr(args, "_sleep", None),
|
|
102
|
+
opener=getattr(args, "_open_browser", None),
|
|
103
|
+
clock=getattr(args, "_clock", None),
|
|
104
|
+
)
|
|
101
105
|
reader = getattr(args, "_getpass", None) or getpass.getpass
|
|
102
106
|
try:
|
|
103
107
|
api_key = str(reader("Project API key: ")).strip()
|
|
@@ -179,10 +183,22 @@ def cmd_logout(args: Any) -> int:
|
|
|
179
183
|
|
|
180
184
|
|
|
181
185
|
def cmd_auth_status(args: Any) -> int:
|
|
186
|
+
from frontier.credentials import load_user_authorization
|
|
187
|
+
from frontier.onboard.user_auth import require_user_authorization
|
|
188
|
+
|
|
189
|
+
api_url = (getattr(args, "api_url", None) or DEFAULT_API_URL).rstrip("/")
|
|
190
|
+
user_line = "User: not authenticated"
|
|
191
|
+
if load_user_authorization(api_url):
|
|
192
|
+
try:
|
|
193
|
+
user = require_user_authorization(api_url)
|
|
194
|
+
user_line = f"User: {user.email or user.user_id} (expires {user.expires_at})"
|
|
195
|
+
except InstallError:
|
|
196
|
+
user_line = "User: stored authorization was rejected"
|
|
197
|
+
print(user_line)
|
|
182
198
|
ctx = resolve_execution_context(args, _project_dir_from_args(args), require_credential=False)
|
|
183
199
|
creds = ctx.creds
|
|
184
200
|
if not creds:
|
|
185
|
-
print("
|
|
201
|
+
print("Project key: not authenticated")
|
|
186
202
|
if ctx.profile_name:
|
|
187
203
|
print(f"Frontier profile: {ctx.profile_name}")
|
|
188
204
|
print(f"Next: frontier login --api-key --profile {ctx.profile_name}")
|
|
@@ -199,6 +215,7 @@ def cmd_auth_status(args: Any) -> int:
|
|
|
199
215
|
print("Next: frontier login --api-key")
|
|
200
216
|
return 1
|
|
201
217
|
print("Authenticated")
|
|
218
|
+
print("Project key: authenticated")
|
|
202
219
|
if ctx.profile_name:
|
|
203
220
|
print(f"Frontier profile: {ctx.profile_name}")
|
|
204
221
|
print(f"Organization: {identity.organization or creds.organization or '(unknown)'}")
|
|
@@ -6,11 +6,8 @@ from pathlib import Path
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
from frontier.context import (
|
|
9
|
-
SOURCE_FLAG,
|
|
10
|
-
SOURCE_ENV_PROFILE,
|
|
11
|
-
SOURCE_LOCAL,
|
|
12
|
-
SOURCE_NONE,
|
|
13
9
|
ensure_named_profile_ids,
|
|
10
|
+
profile_source_label,
|
|
14
11
|
resolve_execution_context,
|
|
15
12
|
)
|
|
16
13
|
from frontier.credentials import (
|
|
@@ -171,6 +168,10 @@ def _maybe_validate_target(project_dir: Path, target: str | None, profiles: str
|
|
|
171
168
|
|
|
172
169
|
|
|
173
170
|
def cmd_profile_create(args: Any) -> int:
|
|
171
|
+
if str(getattr(args, "create_project", "") or "").strip():
|
|
172
|
+
from frontier.onboard.project_commands import cmd_profile_create_project
|
|
173
|
+
|
|
174
|
+
return cmd_profile_create_project(args)
|
|
174
175
|
name = validate_profile_name(str(getattr(args, "name", "") or ""))
|
|
175
176
|
api_url = _resolve_create_api_url(args)
|
|
176
177
|
want_prompt = bool(getattr(args, "api_key", False))
|
|
@@ -306,14 +307,8 @@ def _identity_status(ctx) -> str:
|
|
|
306
307
|
def cmd_profile_status(args: Any) -> int:
|
|
307
308
|
project_dir = _project_dir(args)
|
|
308
309
|
ctx = resolve_execution_context(args, project_dir, honor_local_config_target=False, require_credential=False)
|
|
309
|
-
source_label = {
|
|
310
|
-
SOURCE_FLAG: "--profile",
|
|
311
|
-
SOURCE_ENV_PROFILE: "FRONTIER_PROFILE",
|
|
312
|
-
SOURCE_LOCAL: "local selection",
|
|
313
|
-
SOURCE_NONE: "none (legacy)",
|
|
314
|
-
}.get(ctx.profile_source, ctx.profile_source)
|
|
315
310
|
print(f"Frontier profile: {ctx.profile_name or '(none)'}")
|
|
316
|
-
print(f"Selection source: {
|
|
311
|
+
print(f"Selection source: {profile_source_label(ctx.profile_source)}")
|
|
317
312
|
print(
|
|
318
313
|
f"Organization: {format_organization_identity(ctx.organization_name, ctx.organization_id)}"
|
|
319
314
|
)
|