1password-secrets 0.0.1.dev218__tar.gz → 0.0.1.dev238__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 (27) hide show
  1. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/1password_secrets.egg-info/PKG-INFO +4 -4
  2. 1password_secrets-0.0.1.dev238/1password_secrets.egg-info/SOURCES.txt +22 -0
  3. 1password_secrets-0.0.1.dev238/1password_secrets.egg-info/requires.txt +4 -0
  4. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/PKG-INFO +4 -4
  5. 1password_secrets-0.0.1.dev238/onepassword_secrets/__init__.py +3 -0
  6. 1password_secrets-0.0.1.dev238/onepassword_secrets/__main__.py +4 -0
  7. 1password_secrets-0.0.1.dev238/onepassword_secrets/cli.py +103 -0
  8. 1password_secrets-0.0.1.dev238/onepassword_secrets/commands.py +141 -0
  9. 1password_secrets-0.0.1.dev238/onepassword_secrets/constants.py +10 -0
  10. 1password_secrets-0.0.1.dev238/onepassword_secrets/envs.py +20 -0
  11. 1password_secrets-0.0.1.dev238/onepassword_secrets/errors.py +12 -0
  12. 1password_secrets-0.0.1.dev238/onepassword_secrets/fly.py +145 -0
  13. 1password_secrets-0.0.1.dev238/onepassword_secrets/git.py +66 -0
  14. 1password_secrets-0.0.1.dev238/onepassword_secrets/logs.py +90 -0
  15. 1password_secrets-0.0.1.dev238/onepassword_secrets/op.py +188 -0
  16. 1password_secrets-0.0.1.dev238/onepassword_secrets/prompts.py +71 -0
  17. 1password_secrets-0.0.1.dev238/onepassword_secrets/utils.py +19 -0
  18. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/setup.py +1 -1
  19. 1password_secrets-0.0.1.dev218/1password_secrets.egg-info/SOURCES.txt +0 -10
  20. 1password_secrets-0.0.1.dev218/1password_secrets.egg-info/requires.txt +0 -4
  21. 1password_secrets-0.0.1.dev218/onepassword_secrets.py +0 -733
  22. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/1password_secrets.egg-info/dependency_links.txt +0 -0
  23. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/1password_secrets.egg-info/entry_points.txt +0 -0
  24. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/1password_secrets.egg-info/top_level.txt +0 -0
  25. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/README.md +0 -0
  26. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/pyproject.toml +0 -0
  27. {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev238}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: 1password-secrets
3
- Version: 0.0.1.dev218
3
+ Version: 0.0.1.dev238
4
4
  Summary: 1password-secrets is a set of utilities to sync 1password secrets.
5
5
  Home-page: https://github.com/significa/fly-1password-secrets
6
6
  Author: Significa
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: python-dotenv==1.2.1
22
- Requires-Dist: sgqlc==18
23
- Requires-Dist: rich==14.3.2
21
+ Requires-Dist: python-dotenv==1.2.4
22
+ Requires-Dist: requests==2.32.5
23
+ Requires-Dist: rich==15.0.0
24
24
  Requires-Dist: inquirer==3.4.1
25
25
  Dynamic: author
26
26
  Dynamic: classifier
@@ -0,0 +1,22 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ 1password_secrets.egg-info/PKG-INFO
5
+ 1password_secrets.egg-info/SOURCES.txt
6
+ 1password_secrets.egg-info/dependency_links.txt
7
+ 1password_secrets.egg-info/entry_points.txt
8
+ 1password_secrets.egg-info/requires.txt
9
+ 1password_secrets.egg-info/top_level.txt
10
+ onepassword_secrets/__init__.py
11
+ onepassword_secrets/__main__.py
12
+ onepassword_secrets/cli.py
13
+ onepassword_secrets/commands.py
14
+ onepassword_secrets/constants.py
15
+ onepassword_secrets/envs.py
16
+ onepassword_secrets/errors.py
17
+ onepassword_secrets/fly.py
18
+ onepassword_secrets/git.py
19
+ onepassword_secrets/logs.py
20
+ onepassword_secrets/op.py
21
+ onepassword_secrets/prompts.py
22
+ onepassword_secrets/utils.py
@@ -0,0 +1,4 @@
1
+ python-dotenv==1.2.4
2
+ requests==2.32.5
3
+ rich==15.0.0
4
+ inquirer==3.4.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: 1password-secrets
3
- Version: 0.0.1.dev218
3
+ Version: 0.0.1.dev238
4
4
  Summary: 1password-secrets is a set of utilities to sync 1password secrets.
5
5
  Home-page: https://github.com/significa/fly-1password-secrets
6
6
  Author: Significa
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
- Requires-Dist: python-dotenv==1.2.1
22
- Requires-Dist: sgqlc==18
23
- Requires-Dist: rich==14.3.2
21
+ Requires-Dist: python-dotenv==1.2.4
22
+ Requires-Dist: requests==2.32.5
23
+ Requires-Dist: rich==15.0.0
24
24
  Requires-Dist: inquirer==3.4.1
25
25
  Dynamic: author
26
26
  Dynamic: classifier
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,103 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ from importlib.metadata import version
5
+
6
+ from .commands import (
7
+ create_local_secrets,
8
+ edit_1password_fly_secrets,
9
+ import_1password_secrets_to_fly,
10
+ pull_local_secrets,
11
+ push_local_secrets,
12
+ )
13
+ from .constants import DEFAULT_REMOTE_NAME
14
+ from .errors import UserError
15
+ from .logs import logger
16
+
17
+ try:
18
+ APP_VERSION = version("1password-secrets")
19
+ except ImportError:
20
+ APP_VERSION = "unknown"
21
+
22
+
23
+ def main():
24
+ parser = argparse.ArgumentParser(
25
+ description="1password-secrets is a set of utilities to sync 1Password secrets."
26
+ )
27
+
28
+ parser.add_argument(
29
+ "-v",
30
+ "--version",
31
+ action="version",
32
+ version=f"%(prog)s {APP_VERSION}",
33
+ help="show program's version number and exit",
34
+ )
35
+
36
+ parser.add_argument(
37
+ "--debug",
38
+ action=argparse.BooleanOptionalAction,
39
+ default=False,
40
+ help=(
41
+ "run in debug mode, logging every command, HTTP request and response "
42
+ "(authorization headers and the fly auth token are redacted)"
43
+ ),
44
+ )
45
+
46
+ parser.add_argument(
47
+ "--vault",
48
+ type=str,
49
+ default=None,
50
+ help=(
51
+ "Specify a vault name or id to operate on. "
52
+ "Defaults to all vaults across the logged in account."
53
+ ),
54
+ )
55
+
56
+ parser.add_argument(
57
+ "--remote",
58
+ type=str,
59
+ default=DEFAULT_REMOTE_NAME,
60
+ help='Construct secret name based on this git remote. Defaults to "origin"',
61
+ )
62
+
63
+ subparsers = parser.add_subparsers(dest="subcommand", required=True)
64
+
65
+ fly_parser = subparsers.add_parser("fly", help="manage fly secrets")
66
+ fly_parser.add_argument("action", type=str, choices=["import", "edit"])
67
+ fly_parser.add_argument("app_name", type=str, help="fly application name")
68
+
69
+ local_parser = subparsers.add_parser("local", help="manage local secrets")
70
+ local_subparsers = local_parser.add_subparsers(dest="action", required=True)
71
+
72
+ local_subparsers.add_parser("pull")
73
+ local_subparsers.add_parser("push")
74
+
75
+ create_parser = local_subparsers.add_parser("create")
76
+ create_parser.add_argument(
77
+ "secrets_file_path",
78
+ type=str,
79
+ help="secrets file path",
80
+ )
81
+
82
+ args = parser.parse_args()
83
+
84
+ if args.debug:
85
+ logger.setLevel(logging.DEBUG)
86
+
87
+ try:
88
+ if args.subcommand == "fly":
89
+ if args.action == "import":
90
+ import_1password_secrets_to_fly(args.app_name, vault=args.vault)
91
+ elif args.action == "edit":
92
+ edit_1password_fly_secrets(args.app_name, vault=args.vault)
93
+
94
+ elif args.subcommand == "local":
95
+ if args.action == "pull":
96
+ pull_local_secrets(remote=args.remote, vault=args.vault)
97
+ elif args.action == "push":
98
+ push_local_secrets(remote=args.remote, vault=args.vault)
99
+ elif args.action == "create":
100
+ create_local_secrets(args.secrets_file_path, vault=args.vault, remote=args.remote)
101
+
102
+ except UserError:
103
+ sys.exit(1)
@@ -0,0 +1,141 @@
1
+ """High level workflows wiring 1Password, Fly and the local filesystem together."""
2
+
3
+ import json
4
+ import subprocess
5
+ from datetime import datetime, timezone
6
+ from tempfile import NamedTemporaryFile
7
+
8
+ from rich.panel import Panel
9
+
10
+ from .constants import DATE_FORMAT, DEFAULT_ENV_FILE_NAME, DEFAULT_REMOTE_NAME
11
+ from .envs import get_secrets_from_envs
12
+ from .fly import update_fly_secrets
13
+ from .git import get_secret_name_label_from_current_directory
14
+ from .logs import console, debug_command, logger
15
+ from .op import (
16
+ create_1password_secrets,
17
+ get_1password_env_file_item_id,
18
+ get_envs_from_1password,
19
+ get_filename_from_1password,
20
+ get_item_share_link,
21
+ update_1password_custom_field,
22
+ update_1password_secrets,
23
+ )
24
+ from .prompts import boolean_prompt, prompt_secret_diff
25
+ from .utils import get_file_contents
26
+
27
+
28
+ def import_1password_secrets_to_fly(app_id, vault=None):
29
+ item_id = get_1password_env_file_item_id(f"fly:{app_id}", vault=vault)
30
+
31
+ secrets = get_secrets_from_envs(get_envs_from_1password(item_id, vault=vault))
32
+
33
+ logger.debug(f"Secrets loaded from env: {json.dumps(secrets, indent=2)}\n")
34
+
35
+ update_fly_secrets(app_id, secrets)
36
+
37
+ now_formatted = datetime.now(tz=timezone.utc).strftime(DATE_FORMAT)
38
+ update_1password_custom_field(item_id, "last imported at", now_formatted, vault=vault)
39
+
40
+
41
+ def edit_1password_fly_secrets(app_id, vault=None):
42
+ item_id = get_1password_env_file_item_id(f"fly:{app_id}", vault=vault)
43
+
44
+ current_raw_secrets = get_envs_from_1password(item_id, vault=vault)
45
+
46
+ with NamedTemporaryFile("w+", suffix=".env") as file:
47
+ file.writelines(current_raw_secrets)
48
+ file.flush()
49
+
50
+ console.print()
51
+ console.print(
52
+ Panel(
53
+ "[bold]Edit the secrets in your editor, then save and close the file to continue.[/bold]\n"
54
+ "[dim]Waiting for editor to close...[/dim]",
55
+ title="Editor",
56
+ border_style="cyan",
57
+ )
58
+ )
59
+
60
+ editor_command_args = ["code", "--wait", "--disable-extensions", file.name]
61
+ debug_command(editor_command_args)
62
+ editor_output = subprocess.check_output(editor_command_args) # noqa: S603
63
+ debug_command(editor_command_args, output=editor_output, returncode=0)
64
+
65
+ console.print("[green]Editor closed.[/green]")
66
+ console.print()
67
+
68
+ file.seek(0)
69
+ new_raw_secrets = file.read()
70
+
71
+ update_1password_secrets(
72
+ item_id,
73
+ new_raw_secrets=new_raw_secrets,
74
+ previous_raw_secrets=current_raw_secrets,
75
+ vault=vault,
76
+ )
77
+
78
+ console.print()
79
+ if boolean_prompt(f"Secrets updated in 1Password. Import to Fly app '{app_id}'"):
80
+ import_1password_secrets_to_fly(app_id, vault=vault)
81
+
82
+
83
+ def pull_local_secrets(remote=DEFAULT_REMOTE_NAME, vault=None):
84
+ secret_note_label = get_secret_name_label_from_current_directory(remote=remote)
85
+ item_id = get_1password_env_file_item_id(secret_note_label, vault=vault)
86
+
87
+ secrets = get_envs_from_1password(item_id, vault=vault)
88
+
89
+ env_file_name = get_filename_from_1password(item_id, vault=vault) or DEFAULT_ENV_FILE_NAME
90
+
91
+ previous_raw_secrets = get_file_contents(env_file_name, raise_if_not_found=False)
92
+
93
+ if previous_raw_secrets:
94
+ prompt_secret_diff(
95
+ previous_raw_secrets=previous_raw_secrets,
96
+ new_raw_secrets=secrets,
97
+ )
98
+
99
+ with open(env_file_name, "w") as file:
100
+ file.writelines(secrets)
101
+
102
+ console.print(f"[bold green]Successfully updated {env_file_name} from 1Password[/bold green]")
103
+
104
+
105
+ def push_local_secrets(remote=DEFAULT_REMOTE_NAME, vault=None):
106
+ secret_note_label = get_secret_name_label_from_current_directory(remote=remote)
107
+ item_id = get_1password_env_file_item_id(secret_note_label, vault=vault)
108
+
109
+ env_file_name = get_filename_from_1password(item_id) or DEFAULT_ENV_FILE_NAME
110
+
111
+ secrets = get_file_contents(env_file_name, raise_if_not_found=True)
112
+
113
+ update_1password_secrets(item_id, secrets, vault=vault)
114
+
115
+ console.print(
116
+ f"[bold green]Successfully pushed secrets from {env_file_name} to 1Password[/bold green]"
117
+ )
118
+
119
+
120
+ def create_local_secrets(secrets_file_path, vault=None, remote=DEFAULT_REMOTE_NAME):
121
+ secret_note_label = get_secret_name_label_from_current_directory(remote=remote)
122
+
123
+ raw_secrets = get_file_contents(secrets_file_path, raise_if_not_found=True)
124
+
125
+ title = f"{secrets_file_path} local development {secret_note_label}"
126
+
127
+ item = create_1password_secrets(
128
+ file_path=secrets_file_path, raw_secrets=raw_secrets, title=title, vault=vault
129
+ )
130
+
131
+ item_url = get_item_share_link(item["id"], vault=vault)
132
+
133
+ app_url = item_url.replace("https://start.1password.com/", "onepassword://")
134
+
135
+ console.print()
136
+ console.print(f"[bold green]Item '{title}' created in 1Password![/bold green]")
137
+ console.print()
138
+ console.print(
139
+ Panel(f"[link={item_url}]{item_url}[/link]", title="Web Link", border_style="blue")
140
+ )
141
+ console.print(Panel(f"[link={app_url}]{app_url}[/link]", title="App Link", border_style="blue"))
@@ -0,0 +1,10 @@
1
+ FLY_MACHINES_API_ENDPOINT = "https://api.machines.dev/v1"
2
+ FLY_API_TIMEOUT_SECONDS = 30
3
+ DATE_FORMAT = "%Y/%m/%d %H:%M:%S"
4
+ DEFAULT_ENV_FILE_NAME = ".env"
5
+ ONE_PASSWORD_FILE_PATH_FIELD_NAME = "file_name" # noqa: S105
6
+ ONE_PASSWORD_NOTES_CONTENT_FIELD_NAME = "notesPlain" # noqa: S105
7
+ ONE_PASSWORD_SECURE_NOTE_CATEGORY = "Secure Note" # noqa: S105
8
+ DEFAULT_REMOTE_NAME = "origin"
9
+ REDACTED_PLACEHOLDER = "<REDACTED>" # noqa: S105
10
+ SENSITIVE_HEADER_NAMES = frozenset({"authorization", "proxy-authorization", "cookie", "set-cookie"})
@@ -0,0 +1,20 @@
1
+ from io import StringIO
2
+
3
+ from dotenv import dotenv_values
4
+
5
+ from .errors import raise_error
6
+
7
+
8
+ def get_secrets_from_envs(input: str):
9
+ secrets = dotenv_values(stream=StringIO(input))
10
+
11
+ keys_with_values_null_values = [key for key, value in secrets.items() if value is None]
12
+
13
+ if len(keys_with_values_null_values) > 0:
14
+ raise_error(
15
+ "Failed to parse env file, values for the following keys are null: {}".format(
16
+ ", ".join(keys_with_values_null_values)
17
+ )
18
+ )
19
+
20
+ return secrets
@@ -0,0 +1,12 @@
1
+ from typing import NoReturn
2
+
3
+ from .logs import console
4
+
5
+
6
+ class UserError(RuntimeError):
7
+ pass
8
+
9
+
10
+ def raise_error(message) -> NoReturn:
11
+ console.print(f"[bold red]Error:[/bold red] {message}")
12
+ raise UserError(message)
@@ -0,0 +1,145 @@
1
+ """Interactions with Fly.io: the machines REST API and the `fly` CLI."""
2
+
3
+ import json
4
+ import subprocess
5
+
6
+ import requests
7
+
8
+ from .constants import FLY_API_TIMEOUT_SECONDS, FLY_MACHINES_API_ENDPOINT
9
+ from .errors import raise_error
10
+ from .logs import console, debug_command, debug_request, debug_response
11
+ from .prompts import boolean_prompt
12
+
13
+
14
+ def get_fly_auth_token():
15
+ command_args = ["fly", "auth", "token", "--json"]
16
+ output = subprocess.check_output(command_args) # noqa: S603
17
+ debug_command(command_args, output=output, returncode=0, redact_output=True)
18
+
19
+ return json.loads(output)["token"]
20
+
21
+
22
+ def make_fly_api_request(
23
+ method,
24
+ path,
25
+ json_body=None,
26
+ token=None,
27
+ status_message="Communicating with Fly.io",
28
+ ):
29
+ url = f"{FLY_MACHINES_API_ENDPOINT}{path}"
30
+ headers = {
31
+ "Authorization": f"Bearer {token or get_fly_auth_token()}",
32
+ "Content-Type": "application/json",
33
+ }
34
+
35
+ debug_request(method, url, headers, json_body)
36
+
37
+ with console.status(f"[bold cyan]{status_message}...", spinner="dots"):
38
+ try:
39
+ response = requests.request(
40
+ method,
41
+ url,
42
+ headers=headers,
43
+ json=json_body,
44
+ timeout=FLY_API_TIMEOUT_SECONDS,
45
+ )
46
+ except requests.RequestException as error:
47
+ raise_error(f"Failed to reach the Fly API: {error}")
48
+
49
+ debug_response(response)
50
+
51
+ if not response.ok:
52
+ try:
53
+ body = response.json()
54
+ except ValueError:
55
+ body = response.text
56
+
57
+ message = body.get("error") if isinstance(body, dict) else body
58
+ raise_error(f"Fly API error ({response.status_code}): {message or 'unknown error'}")
59
+
60
+ if not response.content:
61
+ return None
62
+
63
+ try:
64
+ return response.json()
65
+ except ValueError:
66
+ return None
67
+
68
+
69
+ def get_fly_secret_names(app_id, token=None) -> set[str]:
70
+ response = (
71
+ make_fly_api_request(
72
+ "GET",
73
+ f"/apps/{app_id}/secrets",
74
+ token=token,
75
+ status_message="Fetching current Fly secrets",
76
+ )
77
+ or {}
78
+ )
79
+
80
+ return {secret["name"] for secret in response.get("secrets") or [] if secret.get("name")}
81
+
82
+
83
+ def set_fly_secrets(app_id, values, token=None) -> None:
84
+ make_fly_api_request(
85
+ "POST",
86
+ f"/apps/{app_id}/secrets",
87
+ json_body={"values": values},
88
+ token=token,
89
+ status_message="Uploading secrets to Fly",
90
+ )
91
+
92
+
93
+ def deploy_fly_secrets(app_id):
94
+ """Deploy secrets to a Fly app using the fly CLI."""
95
+ console.print()
96
+ console.print(f"[bold cyan]Deploying secrets to Fly app '{app_id}'...[/bold cyan]")
97
+ console.print()
98
+
99
+ command_args = ["fly", "secrets", "deploy", "-a", app_id]
100
+ debug_command(command_args)
101
+
102
+ try:
103
+ result = subprocess.run(command_args, check=False) # noqa: S603
104
+ except FileNotFoundError:
105
+ raise_error("The 'fly' CLI was not found. See https://fly.io/docs/flyctl/install/")
106
+
107
+ debug_command(command_args, returncode=result.returncode)
108
+
109
+ if result.returncode != 0:
110
+ raise_error(f"Failed to deploy secrets (exit code {result.returncode})")
111
+
112
+ console.print()
113
+ console.print(f"[bold green]Secrets deployed to Fly app '{app_id}'[/bold green]")
114
+
115
+
116
+ def update_fly_secrets(app_id, secrets):
117
+ token = get_fly_auth_token()
118
+
119
+ secret_names_in_fly = get_fly_secret_names(app_id, token=token)
120
+
121
+ secrets_names_in_fly_only = secret_names_in_fly.difference(secrets.keys())
122
+
123
+ deletions = {}
124
+
125
+ if len(secrets_names_in_fly_only) > 0 and boolean_prompt(
126
+ "The following secrets will be deleted from Fly.io: {}. Are you sure".format(
127
+ ", ".join(sorted(secrets_names_in_fly_only))
128
+ )
129
+ ):
130
+ deletions = dict.fromkeys(secrets_names_in_fly_only, None)
131
+
132
+ values = {**secrets, **deletions}
133
+
134
+ if not values:
135
+ console.print()
136
+ console.print("[dim]No secret changes to push to Fly.[/dim]")
137
+ return
138
+
139
+ set_fly_secrets(app_id, values, token=token)
140
+
141
+ console.print()
142
+ console.print(f"[bold green]Secrets staged on Fly app '{app_id}'[/bold green]")
143
+
144
+ if boolean_prompt("Deploy secrets now (run fly secrets deploy)?"):
145
+ deploy_fly_secrets(app_id)
@@ -0,0 +1,66 @@
1
+ import os
2
+ import re
3
+ import subprocess
4
+
5
+ from .constants import DEFAULT_REMOTE_NAME
6
+ from .logs import console, debug_command
7
+
8
+
9
+ def get_git_remote_name(remote=DEFAULT_REMOTE_NAME) -> tuple[str | None, str | None]:
10
+ git_repository_regex = r"^(\w+)(:\/\/|@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$"
11
+
12
+ git_remote_url = None
13
+ command_args = ["git", "config", "--get", f"remote.{remote}.url"]
14
+
15
+ try:
16
+ git_remote_url = (
17
+ subprocess.check_output(command_args) # noqa: S603
18
+ .decode("utf-8")
19
+ .strip()
20
+ )
21
+ debug_command(command_args, output=git_remote_url, returncode=0)
22
+
23
+ except FileNotFoundError:
24
+ debug_command(command_args, output="git not in the PATH")
25
+ return ("git not in the PATH", None)
26
+
27
+ except subprocess.CalledProcessError as error:
28
+ exit_code, _command = error.args
29
+ debug_command(command_args, returncode=exit_code)
30
+
31
+ if exit_code == 1:
32
+ return (f"Either not in a git repository or remote {remote!r} is not set", None)
33
+
34
+ return (f"Failed to retrieve the git remote {remote!r} url", None)
35
+
36
+ regex_match = re.match(git_repository_regex, git_remote_url)
37
+
38
+ if regex_match is None:
39
+ return (f'Failed to parse git remote "{remote}"', None)
40
+
41
+ return (
42
+ None,
43
+ f"{regex_match.group(4)}/{regex_match.group(5)}",
44
+ )
45
+
46
+
47
+ def get_secret_name_label_from_current_directory(remote=DEFAULT_REMOTE_NAME) -> str:
48
+ """
49
+ Returns a predictable label for identifying the secrets based on the current directory.
50
+ If within a git repository with a remote named "origin" (and git is installed), it will output
51
+ something like: `repo:my-org/my-repo` or `repo:my-org/my-team/my-repo`.
52
+ Otherwise it will return the name of the directory as `local-dir:my-directory-name`.
53
+ """
54
+
55
+ error_message, git_remote_name = get_git_remote_name(remote=remote)
56
+
57
+ if not error_message:
58
+ return f"repo:{git_remote_name}"
59
+
60
+ directory_name = os.path.basename(os.getcwd())
61
+ label = f"local-dir:{directory_name}"
62
+
63
+ console.print(
64
+ f"[dim]{error_message}, using the label based on the current directory: {label!r}[/dim]"
65
+ )
66
+ return label
@@ -0,0 +1,90 @@
1
+ import json
2
+ import logging
3
+
4
+ from rich.console import Console
5
+
6
+ from .constants import REDACTED_PLACEHOLDER, SENSITIVE_HEADER_NAMES
7
+
8
+ console = Console()
9
+
10
+
11
+ def _setup_logger():
12
+ class Formatter(logging.Formatter):
13
+ def format(self, record):
14
+ if record.levelno == logging.INFO:
15
+ self._style._fmt = "%(message)s"
16
+ else:
17
+ self._style._fmt = "%(levelname)s: %(message)s"
18
+ return super().format(record)
19
+
20
+ logger = logging.getLogger()
21
+ logger.setLevel(logging.INFO)
22
+
23
+ if logger.hasHandlers():
24
+ logger.handlers.clear()
25
+
26
+ stdout_handler = logging.StreamHandler()
27
+ stdout_handler.setFormatter(Formatter())
28
+ logger.addHandler(stdout_handler)
29
+
30
+ return logger
31
+
32
+
33
+ logger = _setup_logger()
34
+
35
+
36
+ def redact_headers(headers) -> dict:
37
+ return {
38
+ name: (REDACTED_PLACEHOLDER if name.lower() in SENSITIVE_HEADER_NAMES else value)
39
+ for name, value in dict(headers or {}).items()
40
+ }
41
+
42
+
43
+ def format_command(command_args) -> str:
44
+ return " ".join((f'"{arg}"' if " " in arg else arg) for arg in command_args)
45
+
46
+
47
+ def debug_enabled() -> bool:
48
+ return logger.isEnabledFor(logging.DEBUG)
49
+
50
+
51
+ def debug_request(method, url, headers, json_body) -> None:
52
+ if not debug_enabled():
53
+ return
54
+
55
+ lines = [f"Request: {method} {url}", "Request headers:"]
56
+ lines += [f" {name}: {value}" for name, value in redact_headers(headers).items()]
57
+
58
+ if json_body is not None:
59
+ lines += ["Request body:", json.dumps(json_body, indent=2)]
60
+
61
+ logger.debug("\n".join(lines))
62
+
63
+
64
+ def debug_response(response) -> None:
65
+ if not debug_enabled():
66
+ return
67
+
68
+ lines = [f"Response: {response.status_code}", "Response headers:"]
69
+ lines += [f" {name}: {value}" for name, value in redact_headers(response.headers).items()]
70
+ lines += ["Response body:", response.text]
71
+
72
+ logger.debug("\n".join(lines))
73
+
74
+
75
+ def debug_command(command_args, output=None, returncode=None, redact_output=False) -> None:
76
+ if not debug_enabled():
77
+ return
78
+
79
+ lines = [f"Running command: {format_command(command_args)}"]
80
+
81
+ if returncode is not None:
82
+ lines.append(f"Exit code: {returncode}")
83
+
84
+ if output is not None:
85
+ if isinstance(output, bytes):
86
+ output = output.decode("utf-8", errors="replace")
87
+
88
+ lines += ["Output:", REDACTED_PLACEHOLDER if redact_output else output]
89
+
90
+ logger.debug("\n".join(lines))