vgs-cli 0.0.1.dev0__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.
- vgs_cli-0.0.1.dev0.data/data/vgscli/calm.yaml +16 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/checkout.yaml +21 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/http-route-template.yaml +61 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/mft-route-template.yaml +10 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/payments-admin.yaml +25 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/service-account-schema.yaml +54 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/sub-account-checkout.yaml +23 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-resources.yaml +710 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-schema.yaml +36 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vault-template.yaml +12 -0
- vgs_cli-0.0.1.dev0.data/data/vgscli/vgs-cli.yaml +17 -0
- vgs_cli-0.0.1.dev0.dist-info/METADATA +139 -0
- vgs_cli-0.0.1.dev0.dist-info/RECORD +56 -0
- vgs_cli-0.0.1.dev0.dist-info/WHEEL +5 -0
- vgs_cli-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- vgs_cli-0.0.1.dev0.dist-info/licenses/LICENSE +22 -0
- vgs_cli-0.0.1.dev0.dist-info/top_level.txt +1 -0
- vgscli/__init__.py +0 -0
- vgscli/_version.py +32 -0
- vgscli/access_logs.py +65 -0
- vgscli/audits_api.py +102 -0
- vgscli/auth.py +68 -0
- vgscli/auth_server.py +131 -0
- vgscli/auth_utils.py +24 -0
- vgscli/callback_server.py +41 -0
- vgscli/cert_manager_api.py +34 -0
- vgscli/cli/__init__.py +23 -0
- vgscli/cli/commands/__init__.py +3 -0
- vgscli/cli/commands/apply.py +307 -0
- vgscli/cli/commands/generate.py +134 -0
- vgscli/cli/commands/get.py +200 -0
- vgscli/cli/types/__init__.py +2 -0
- vgscli/cli/types/resource_id.py +39 -0
- vgscli/cli/types/variable.py +21 -0
- vgscli/cli_utils.py +132 -0
- vgscli/click_extensions.py +88 -0
- vgscli/config_file.py +58 -0
- vgscli/errors.py +263 -0
- vgscli/file_token_util.py +30 -0
- vgscli/id_generator.py +46 -0
- vgscli/keyring_token_util.py +128 -0
- vgscli/resource-templates/http-route-template.yaml +61 -0
- vgscli/resource-templates/mft-route-template.yaml +10 -0
- vgscli/resource-templates/service-account/calm.yaml +16 -0
- vgscli/resource-templates/service-account/checkout.yaml +21 -0
- vgscli/resource-templates/service-account/payments-admin.yaml +25 -0
- vgscli/resource-templates/service-account/sub-account-checkout.yaml +23 -0
- vgscli/resource-templates/service-account/vgs-cli.yaml +17 -0
- vgscli/resource-templates/vault-template.yaml +12 -0
- vgscli/testing.py +48 -0
- vgscli/text.py +9 -0
- vgscli/token_handler.py +11 -0
- vgscli/validation-schemas/service-account-schema.yaml +54 -0
- vgscli/validation-schemas/vault-resources.yaml +710 -0
- vgscli/validation-schemas/vault-schema.yaml +36 -0
- vgscli/vgs.py +249 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from click_plugins import with_plugins
|
|
3
|
+
from simple_rest_client.exceptions import ClientError, ServerError
|
|
4
|
+
from vgs.sdk.routes import dump_all_routes, normalize
|
|
5
|
+
from vgs.sdk.serializers import yaml # this one correctly outputs blocks
|
|
6
|
+
from vgs.sdk.vaults_api import create_api as create_vault_mgmt_api_routes
|
|
7
|
+
|
|
8
|
+
from vgscli.auth import handshake, token_util
|
|
9
|
+
from vgscli.cert_manager_api import create_cert_manager_api
|
|
10
|
+
from vgscli.cli import create_account_mgmt_api, create_vault_mgmt_api
|
|
11
|
+
from vgscli.cli.types import ResourceId, ResourceIdParamType
|
|
12
|
+
from vgscli.cli_utils import dump_camelized_yaml, iter_entry_points
|
|
13
|
+
from vgscli.errors import ServiceClientListingError, handle_errors
|
|
14
|
+
|
|
15
|
+
from .apply import wrap_in_http_envelope
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@with_plugins(iter_entry_points("vgs.get.plugins"))
|
|
19
|
+
@click.group("get")
|
|
20
|
+
def get() -> None:
|
|
21
|
+
"""
|
|
22
|
+
Get VGS resource.
|
|
23
|
+
"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@get.command("service-accounts")
|
|
28
|
+
@click.option(
|
|
29
|
+
"-O",
|
|
30
|
+
"--organization",
|
|
31
|
+
"org_id",
|
|
32
|
+
type=ResourceIdParamType(prefix="AC"),
|
|
33
|
+
help="Organization ID which service accounts will be listed",
|
|
34
|
+
)
|
|
35
|
+
@click.pass_context
|
|
36
|
+
@handle_errors()
|
|
37
|
+
def get_service_accounts(ctx: click.Context, org_id: ResourceId):
|
|
38
|
+
"""
|
|
39
|
+
Get service accounts from the organization.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
account_mgmt = create_account_mgmt_api(ctx)
|
|
43
|
+
try:
|
|
44
|
+
# noinspection PyUnresolvedReferences
|
|
45
|
+
response = account_mgmt.service_accounts.get(org_id.base58)
|
|
46
|
+
except (ClientError, ServerError) as e:
|
|
47
|
+
raise ServiceClientListingError(e)
|
|
48
|
+
|
|
49
|
+
accounts = response.body["data"]["attributes"]["service_accounts"]
|
|
50
|
+
|
|
51
|
+
for account in accounts:
|
|
52
|
+
click.echo("---")
|
|
53
|
+
click.echo(
|
|
54
|
+
dump_camelized_yaml(
|
|
55
|
+
{
|
|
56
|
+
"apiVersion": "1.0.0",
|
|
57
|
+
"kind": "ServiceAccount",
|
|
58
|
+
"data": account,
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@get.command("access-credentials")
|
|
65
|
+
@click.option("--vault", "-V", help="Vault ID", required=True)
|
|
66
|
+
@click.pass_context
|
|
67
|
+
@handle_errors()
|
|
68
|
+
def get_access_credentials(ctx, vault):
|
|
69
|
+
"""
|
|
70
|
+
Get access-credentials
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
account_mgmt = create_account_mgmt_api(ctx)
|
|
74
|
+
|
|
75
|
+
response = account_mgmt.vaults.get_by_id(vault)
|
|
76
|
+
|
|
77
|
+
vault_mgmt = create_vault_mgmt_api(
|
|
78
|
+
ctx, response.body["data"][0]["links"]["vault_management_api"]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
response = vault_mgmt.credentials.list(headers={"VGS-Tenant": vault})
|
|
82
|
+
|
|
83
|
+
click.echo(
|
|
84
|
+
dump_camelized_yaml(
|
|
85
|
+
{
|
|
86
|
+
"apiVersion": "1.0.0",
|
|
87
|
+
"kind": "AccessCredentials",
|
|
88
|
+
"data": response.body["data"],
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@get.command("organizations")
|
|
95
|
+
@click.pass_context
|
|
96
|
+
@handle_errors()
|
|
97
|
+
def get_organizations(ctx):
|
|
98
|
+
"""
|
|
99
|
+
Get organizations
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
account_mgmt = create_account_mgmt_api(ctx)
|
|
103
|
+
|
|
104
|
+
response = account_mgmt.organizations.list()
|
|
105
|
+
|
|
106
|
+
click.echo(
|
|
107
|
+
dump_camelized_yaml(
|
|
108
|
+
{
|
|
109
|
+
"apiVersion": "1.0.0",
|
|
110
|
+
"kind": "Organizations",
|
|
111
|
+
"data": response.body["data"],
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@get.command("vaults")
|
|
118
|
+
@click.pass_context
|
|
119
|
+
@handle_errors()
|
|
120
|
+
def get_vaults(ctx):
|
|
121
|
+
"""
|
|
122
|
+
Get vaults
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
account_mgmt = create_account_mgmt_api(ctx)
|
|
126
|
+
|
|
127
|
+
response = account_mgmt.vaults.list()
|
|
128
|
+
|
|
129
|
+
click.echo(
|
|
130
|
+
dump_camelized_yaml(
|
|
131
|
+
{
|
|
132
|
+
"apiVersion": "1.0.0",
|
|
133
|
+
"kind": "Vaults",
|
|
134
|
+
"data": response.body["data"],
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@get.command("http-routes")
|
|
141
|
+
@click.option("--vault", "-V", help="Vault ID", required=True)
|
|
142
|
+
@click.pass_context
|
|
143
|
+
@handle_errors()
|
|
144
|
+
def get_http_routes(ctx, vault):
|
|
145
|
+
"""
|
|
146
|
+
Get HTTP routes
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
handshake(ctx, ctx.obj.env)
|
|
150
|
+
|
|
151
|
+
vault_management_api = create_vault_mgmt_api_routes(
|
|
152
|
+
ctx, vault, ctx.obj.env, token_util.get_access_token()
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
response = vault_management_api.routes.list()
|
|
156
|
+
routes = normalize(response.body["data"])
|
|
157
|
+
|
|
158
|
+
# for each route, wrap in envelope and return
|
|
159
|
+
wrapped = [wrap_in_http_envelope(route) for route in routes]
|
|
160
|
+
|
|
161
|
+
click.echo(yaml.dump_all(wrapped, indent=2))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@get.command("routes")
|
|
165
|
+
@click.option("--vault", "-V", help="Vault ID", required=True)
|
|
166
|
+
@click.pass_context
|
|
167
|
+
@handle_errors()
|
|
168
|
+
def get_routes(ctx, vault):
|
|
169
|
+
"""
|
|
170
|
+
Get routes
|
|
171
|
+
"""
|
|
172
|
+
handshake(ctx, ctx.obj.env)
|
|
173
|
+
|
|
174
|
+
routes_api = create_vault_mgmt_api_routes(
|
|
175
|
+
ctx, vault, ctx.obj.env, token_util.get_access_token()
|
|
176
|
+
)
|
|
177
|
+
dump = dump_all_routes(routes_api)
|
|
178
|
+
click.echo(dump)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@get.command("certificates")
|
|
182
|
+
@click.option("--vault", "-V", help="Vault ID", required=True)
|
|
183
|
+
@click.pass_context
|
|
184
|
+
@handle_errors()
|
|
185
|
+
def get_certificates(ctx, vault):
|
|
186
|
+
"""
|
|
187
|
+
Get certificates for a vault.
|
|
188
|
+
"""
|
|
189
|
+
token = token_util.get_access_token()
|
|
190
|
+
cert_api = create_cert_manager_api(vault, ctx.obj.env, token)
|
|
191
|
+
response = cert_api.certificates.list(params={"vault_identifier": vault})
|
|
192
|
+
click.echo(
|
|
193
|
+
dump_camelized_yaml(
|
|
194
|
+
{
|
|
195
|
+
"apiVersion": "1.0.0",
|
|
196
|
+
"kind": "Certificates",
|
|
197
|
+
"data": response.body,
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import click
|
|
2
|
+
|
|
3
|
+
from vgscli.id_generator import base58_to_uuid, uuid_to_base58
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ResourceId:
|
|
7
|
+
"""
|
|
8
|
+
ID of a VGS resource as shown on the dashboard (e.g., ACtELxZxTcXrAMk2Gp8Qp5yh).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, prefix: str, uuid_string: str):
|
|
12
|
+
self.uuid = uuid_string
|
|
13
|
+
self._base58 = uuid_to_base58(uuid_string, prefix)
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def base58(self) -> str:
|
|
17
|
+
return self._base58
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def decode_base58(prefix: str, base58_string: str) -> "ResourceId":
|
|
21
|
+
uuid_string = base58_to_uuid(base58_string)
|
|
22
|
+
return ResourceId(prefix, uuid_string)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ResourceIdParamType(click.ParamType):
|
|
26
|
+
name = "resource_id"
|
|
27
|
+
|
|
28
|
+
def __init__(self, **kwargs):
|
|
29
|
+
self.prefix = kwargs.get("prefix", "")
|
|
30
|
+
|
|
31
|
+
def convert(self, value: str, param, ctx) -> ResourceId:
|
|
32
|
+
try:
|
|
33
|
+
if value.startswith(self.prefix):
|
|
34
|
+
base58_string = value[len(self.prefix) :]
|
|
35
|
+
return ResourceId.decode_base58(self.prefix, base58_string)
|
|
36
|
+
|
|
37
|
+
return ResourceId(self.prefix, value)
|
|
38
|
+
except ValueError:
|
|
39
|
+
self.fail(f"{value!r} is not a valid organization ID", param, ctx)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Variable:
|
|
7
|
+
"""
|
|
8
|
+
A parsed "name=value" pair.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, name: str, value: Any):
|
|
12
|
+
self.name = name
|
|
13
|
+
self.value = value
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VariableParamType(click.ParamType):
|
|
17
|
+
name = "name=value"
|
|
18
|
+
|
|
19
|
+
def convert(self, value: str, param, ctx) -> Variable:
|
|
20
|
+
tokens = value.split("=", 2)
|
|
21
|
+
return Variable(tokens[0], tokens[1])
|
vgscli/cli_utils.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import uuid
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from importlib.metadata import entry_points
|
|
6
|
+
|
|
7
|
+
import humps
|
|
8
|
+
import jsonschema
|
|
9
|
+
import yaml
|
|
10
|
+
from vgs.sdk.serializers import dump_yaml
|
|
11
|
+
from vgs.sdk.utils import read_file
|
|
12
|
+
|
|
13
|
+
from vgscli.errors import NoSuchFileOrDirectoryError, SchemaValidationError
|
|
14
|
+
from vgscli.id_generator import uuid_to_base58
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def dump_camelized_yaml(payload):
|
|
20
|
+
"""
|
|
21
|
+
Transform snake_case to camelCase and dump as a yaml document.
|
|
22
|
+
"""
|
|
23
|
+
return dump_yaml(OrderedDict(humps.camelize(payload))).rstrip()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validate_yaml(file, schema_path, schema_root=os.path.dirname(__file__)):
|
|
27
|
+
"""
|
|
28
|
+
Validates the file against the schema.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
file: is the buffered content of the file
|
|
33
|
+
schema_path: is the path relative to the working directory
|
|
34
|
+
schema_root: was added to enable validating from different working directories like vgs-admin-cli-plugin
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
schema = read_file(schema_path, schema_root)
|
|
38
|
+
file_content = yaml.full_load(file.read())
|
|
39
|
+
|
|
40
|
+
jsonschema.validate(file_content, yaml.full_load(schema))
|
|
41
|
+
|
|
42
|
+
return file_content
|
|
43
|
+
except jsonschema.exceptions.ValidationError as e:
|
|
44
|
+
raise SchemaValidationError(str(e))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_multi_yaml(file_to_validate, path_to_schema):
|
|
48
|
+
schema_file = read_file(path_to_schema, os.path.dirname(__file__))
|
|
49
|
+
file_content = yaml.load_all(file_to_validate.read(), Loader=yaml.FullLoader)
|
|
50
|
+
schemas = list(yaml.load_all(schema_file, Loader=yaml.FullLoader))
|
|
51
|
+
for file in file_content:
|
|
52
|
+
if not file:
|
|
53
|
+
logger.debug("Skipping blank file")
|
|
54
|
+
continue
|
|
55
|
+
failures = [
|
|
56
|
+
x for x in [fails_validation(file, schema) for schema in schemas] if x
|
|
57
|
+
]
|
|
58
|
+
if len(failures) == len(schemas):
|
|
59
|
+
for failure in failures:
|
|
60
|
+
logger.exception(failure)
|
|
61
|
+
raise SchemaValidationError(
|
|
62
|
+
f"Failed to validate {file['apiVersion']} {file['kind']} {file['metadata']['name']} against any known schema."
|
|
63
|
+
)
|
|
64
|
+
logger.debug(
|
|
65
|
+
f"Validated {file['apiVersion']} {file['kind']} {file['metadata']['name']}"
|
|
66
|
+
)
|
|
67
|
+
yield file
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def fails_validation(file, schema):
|
|
71
|
+
logger.info(
|
|
72
|
+
f"Validating version={file['apiVersion']} kind={file['kind']} name={file['metadata']['name']}\nagainst\n\t{schema}"
|
|
73
|
+
)
|
|
74
|
+
try:
|
|
75
|
+
jsonschema.validate(file, schema)
|
|
76
|
+
except jsonschema.exceptions.ValidationError as ex:
|
|
77
|
+
return ex
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def read_file(file_path, file_root=os.path.dirname(__file__)):
|
|
82
|
+
full_path = os.path.join(file_root, file_path)
|
|
83
|
+
try:
|
|
84
|
+
with open(full_path, "r") as f:
|
|
85
|
+
schema = f.read()
|
|
86
|
+
f.close()
|
|
87
|
+
return schema
|
|
88
|
+
except FileNotFoundError:
|
|
89
|
+
raise NoSuchFileOrDirectoryError(full_path)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_valid_uuid(uuid_to_test, version=4):
|
|
93
|
+
"""
|
|
94
|
+
Check if uuid_to_test is a valid UUID.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
uuid_to_test : str
|
|
99
|
+
version : {1, 2, 3, 4}
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
`True` if uuid_to_test is a valid UUID, otherwise `False`.
|
|
104
|
+
|
|
105
|
+
Examples
|
|
106
|
+
--------
|
|
107
|
+
>>> is_valid_uuid('c9bf9e57-1685-4c89-bafb-ff5af830be8a')
|
|
108
|
+
True
|
|
109
|
+
>>> is_valid_uuid('c9bf9e58')
|
|
110
|
+
False
|
|
111
|
+
"""
|
|
112
|
+
# noinspection PyBroadException
|
|
113
|
+
try:
|
|
114
|
+
uuid_obj = uuid.UUID(uuid_to_test, version=version)
|
|
115
|
+
except Exception:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
return str(uuid_obj) == uuid_to_test
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def format_org_id(org_id):
|
|
122
|
+
if is_valid_uuid(org_id):
|
|
123
|
+
org_id = uuid_to_base58(org_id, "AC")
|
|
124
|
+
return org_id
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def iter_entry_points(group):
|
|
128
|
+
eps = entry_points()
|
|
129
|
+
if hasattr(eps, "select"):
|
|
130
|
+
return eps.select(group=group)
|
|
131
|
+
else:
|
|
132
|
+
return [ep for ep in eps.get(group, [])]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config(object):
|
|
7
|
+
def __init__(self, debug, env):
|
|
8
|
+
self.debug = debug
|
|
9
|
+
self.env = env
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# https://gist.github.com/jacobtolar/fb80d5552a9a9dfc32b12a829fa21c0c#file-click_mutually_exclusive_argument-py-L4
|
|
13
|
+
class MutuallyExclusiveOption(click.Option):
|
|
14
|
+
def __init__(self, *args, **kwargs):
|
|
15
|
+
self.mutually_exclusive = set(kwargs.pop("mutually_exclusive", []))
|
|
16
|
+
help_cmd = kwargs.get("help", "")
|
|
17
|
+
if self.mutually_exclusive:
|
|
18
|
+
ex_str = ", ".join(self.mutually_exclusive)
|
|
19
|
+
kwargs["help"] = help_cmd + (
|
|
20
|
+
" NOTE: This argument is mutually exclusive with "
|
|
21
|
+
" arguments: [" + ex_str + "]."
|
|
22
|
+
)
|
|
23
|
+
super(MutuallyExclusiveOption, self).__init__(*args, **kwargs)
|
|
24
|
+
|
|
25
|
+
def handle_parse_result(self, ctx, opts, args):
|
|
26
|
+
if self.mutually_exclusive.intersection(opts) and self.name in opts:
|
|
27
|
+
raise click.UsageError(
|
|
28
|
+
"Illegal usage: `{}` is mutually exclusive with "
|
|
29
|
+
"arguments `{}`.".format(self.name, ", ".join(self.mutually_exclusive))
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return super(MutuallyExclusiveOption, self).handle_parse_result(ctx, opts, args)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Duration(click.ParamType):
|
|
36
|
+
name = "duration"
|
|
37
|
+
|
|
38
|
+
def convert(self, value, param, ctx):
|
|
39
|
+
try:
|
|
40
|
+
delta_unites = {"h": "hours", "m": "minutes", "s": "seconds"}
|
|
41
|
+
unit = delta_unites[value[-1:].lower()]
|
|
42
|
+
amount = int(value[:-1])
|
|
43
|
+
delta_options = {unit: amount}
|
|
44
|
+
return (datetime.utcnow() - timedelta(**delta_options)).strftime(
|
|
45
|
+
"%Y-%m-%dT%H:%M:%S"
|
|
46
|
+
)
|
|
47
|
+
except TypeError:
|
|
48
|
+
self.fail(
|
|
49
|
+
"expected string for int() conversion, got "
|
|
50
|
+
f"{value!r} of type {type(value).__name__}",
|
|
51
|
+
param,
|
|
52
|
+
ctx,
|
|
53
|
+
)
|
|
54
|
+
except ValueError:
|
|
55
|
+
self.fail(f"{value!r} is not a valid integer", param, ctx)
|
|
56
|
+
except KeyError:
|
|
57
|
+
self.fail("Only integers that end with h, m or s are allowed", param, ctx)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
DURATION = Duration()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DateTimeDuration(click.ParamType):
|
|
64
|
+
name = "dateduration"
|
|
65
|
+
|
|
66
|
+
def __init__(self, formats=None):
|
|
67
|
+
self.formats = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _try_to_parse_date(value, pattern):
|
|
71
|
+
try:
|
|
72
|
+
return datetime.strptime(value, pattern)
|
|
73
|
+
except ValueError:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def convert(self, value, param, ctx):
|
|
77
|
+
for pattern in self.formats:
|
|
78
|
+
if self._try_to_parse_date(value, pattern):
|
|
79
|
+
return value
|
|
80
|
+
|
|
81
|
+
if len(str(value)) > 7:
|
|
82
|
+
return self.fail(
|
|
83
|
+
"invalid date format: {}. (choose from {})".format(
|
|
84
|
+
value, ", ".join(self.formats)
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return DURATION.convert(value, param, ctx)
|
vgscli/config_file.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
import configobj
|
|
6
|
+
|
|
7
|
+
APP_DIR = click.get_app_dir("vgs", force_posix=True)
|
|
8
|
+
OPTION_NAME = "--config"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigObjProvider:
|
|
12
|
+
"""
|
|
13
|
+
When invoked, reads a config file and returns its content as a dictionary.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, section=None):
|
|
17
|
+
self.section = section
|
|
18
|
+
|
|
19
|
+
def __call__(self, *args, **kwargs):
|
|
20
|
+
config = configobj.ConfigObj(args[0])
|
|
21
|
+
if self.section:
|
|
22
|
+
config = config[self.section] if self.section in config else {}
|
|
23
|
+
return config
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# noinspection PyUnusedLocal
|
|
27
|
+
def callback(provider, ctx: click.Context, param: click.Parameter, value: str):
|
|
28
|
+
"""
|
|
29
|
+
Change the execution flow to populate the default map first.
|
|
30
|
+
"""
|
|
31
|
+
ctx.default_map = ctx.default_map or {}
|
|
32
|
+
|
|
33
|
+
if value:
|
|
34
|
+
try:
|
|
35
|
+
config = provider(value)
|
|
36
|
+
except Exception as cause:
|
|
37
|
+
raise click.BadOptionUsage(
|
|
38
|
+
OPTION_NAME, f"Failed to read configuration file: {cause}", ctx
|
|
39
|
+
)
|
|
40
|
+
ctx.default_map.update(config)
|
|
41
|
+
|
|
42
|
+
return value
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def configuration_option(section=None):
|
|
46
|
+
def decorator(f):
|
|
47
|
+
return click.option(
|
|
48
|
+
OPTION_NAME,
|
|
49
|
+
callback=functools.partial(callback, ConfigObjProvider(section)),
|
|
50
|
+
default=os.path.join(APP_DIR, "config"),
|
|
51
|
+
envvar="VGS_CONFIG_FILE",
|
|
52
|
+
expose_value=False,
|
|
53
|
+
help="Read configuration from FILE.",
|
|
54
|
+
is_eager=True,
|
|
55
|
+
type=click.Path(dir_okay=False),
|
|
56
|
+
)(f)
|
|
57
|
+
|
|
58
|
+
return decorator
|