devopsdriver 0.1.51__tar.gz → 0.1.53__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.
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/PKG-INFO +7 -7
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/README.md +2 -2
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/__init__.py +1 -2
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/__init__.py +1 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/azureobject.py +2 -3
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/client.py +0 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/clients.py +0 -2
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/client.py +0 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/log.py +1 -3
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/pipeline.py +0 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/run.py +0 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/timestamp.py +0 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/client.py +10 -11
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/wiql.py +29 -8
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/dataobject.py +0 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/github/client.py +0 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/manage_settings.py +3 -4
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/sendmail.py +12 -6
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/settings.py +112 -19
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/template.py +0 -1
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/PKG-INFO +7 -7
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/requires.txt +4 -4
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/pyproject.toml +4 -4
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_workitem_wiql.py +47 -5
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_settings.py +242 -7
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/LICENSE +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/__init__.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/build.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/__init__.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/__init__.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/github/__init__.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/templates/manage_settings.txt.mako +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/SOURCES.txt +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/dependency_links.txt +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/entry_points.txt +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/top_level.txt +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/setup.cfg +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_azureobject.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_build.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_build_client.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_clients.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_pipeline.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_pipeline_client.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_pipeline_run.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_timestamp.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_workitem_client.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_dataobject.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_manage_settings.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_sendmail.py +0 -0
- {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_template.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devopsdriver
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.53
|
|
4
4
|
Summary: DevOps tools
|
|
5
5
|
Author-email: Marc Page <marcallenpage@gmail.com>
|
|
6
6
|
License: This is free and unencumbered software released into the public domain.
|
|
@@ -47,11 +47,11 @@ Requires-Python: >=3.10
|
|
|
47
47
|
Description-Content-Type: text/markdown
|
|
48
48
|
License-File: LICENSE
|
|
49
49
|
Requires-Dist: PyYAML==6.0.3
|
|
50
|
-
Requires-Dist: keyring==25.
|
|
51
|
-
Requires-Dist: setuptools==
|
|
50
|
+
Requires-Dist: keyring==25.7.0
|
|
51
|
+
Requires-Dist: setuptools==82.0.1
|
|
52
52
|
Requires-Dist: azure-devops==7.1.0b4
|
|
53
|
-
Requires-Dist: Mako==1.3.
|
|
54
|
-
Requires-Dist: PyGithub==2.
|
|
53
|
+
Requires-Dist: Mako==1.3.11
|
|
54
|
+
Requires-Dist: PyGithub==2.9.1
|
|
55
55
|
Provides-Extra: dev
|
|
56
56
|
Requires-Dist: black>=24.3.0; extra == "dev"
|
|
57
57
|
Requires-Dist: pylint>=3.1.0; extra == "dev"
|
|
@@ -64,7 +64,7 @@ Dynamic: license-file
|
|
|
64
64
|
# devops-driver
|
|
65
65
|
|
|
66
66
|

|
|
67
|
-
[](https://pypi.org/project/devopsdriver/0.1.53/)
|
|
68
68
|
[](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
|
|
69
69
|
[](https://github.com/marcpage/devops-driver/graphs/contributors)
|
|
70
70
|
[](http://makeapullrequest.com)
|
|
@@ -75,7 +75,7 @@ Dynamic: license-file
|
|
|
75
75
|
[](https://github.com/marcpage/devops-driver)
|
|
76
76
|
|
|
77
77
|
[](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
|
|
78
|
-
[](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
|
|
79
79
|
[](https://github.com/marcpage/devops-driver/issues)
|
|
80
80
|
[](https://github.com/marcpage/devops-driver/pulls)
|
|
81
81
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# devops-driver
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-
[](https://pypi.org/project/devopsdriver/0.1.53/)
|
|
5
5
|
[](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
|
|
6
6
|
[](https://github.com/marcpage/devops-driver/graphs/contributors)
|
|
7
7
|
[](http://makeapullrequest.com)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://github.com/marcpage/devops-driver)
|
|
13
13
|
|
|
14
14
|
[](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
|
|
15
|
-
[](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
|
|
16
16
|
[](https://github.com/marcpage/devops-driver/issues)
|
|
17
17
|
[](https://github.com/marcpage/devops-driver/pulls)
|
|
18
18
|
|
|
@@ -6,6 +6,6 @@ from .clients import Azure
|
|
|
6
6
|
from .timestamp import Timestamp
|
|
7
7
|
|
|
8
8
|
from .workitem.wiql import Wiql, Value, Field
|
|
9
|
-
from .workitem.wiql import Ascending, Descending, And, Or, In, NotIn
|
|
9
|
+
from .workitem.wiql import Ascending, Descending, And, Or, In, NotIn, Under
|
|
10
10
|
from .workitem.wiql import Equal, NotEqual, LessThanOrEqual, GreaterThanOrEqual
|
|
11
11
|
from .workitem.wiql import IsEmpty, IsNotEmpty, LessThan, GreaterThan
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
"""An Azure Devops WorkItem"""
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
from msrest.serialization import Model
|
|
8
7
|
|
|
9
8
|
from devopsdriver.azdo.timestamp import Timestamp
|
|
@@ -17,13 +16,13 @@ class AzureObject(DataObject): # pylint: disable=too-few-public-methods
|
|
|
17
16
|
self.raw = azure_object
|
|
18
17
|
super().__init__(self.raw.as_dict())
|
|
19
18
|
|
|
20
|
-
def _parse_value(self, data
|
|
19
|
+
def _parse_value(self, data):
|
|
21
20
|
if isinstance(data, str) and Timestamp.is_timestamp(data):
|
|
22
21
|
return Timestamp(data)
|
|
23
22
|
|
|
24
23
|
return super()._parse_value(data)
|
|
25
24
|
|
|
26
|
-
def _get_field(self, name: str, data: dict)
|
|
25
|
+
def _get_field(self, name: str, data: dict):
|
|
27
26
|
value = super()._get_field(name, data)
|
|
28
27
|
|
|
29
28
|
if value is None and "fields" in data:
|
|
@@ -7,7 +7,6 @@ API Documented here:
|
|
|
7
7
|
https://github.com/microsoft/azure-devops-python-api
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
|
|
11
10
|
from azure.devops.connection import Connection as AzureConnection
|
|
12
11
|
from msrest.authentication import BasicAuthentication as MSBasicAuthentication
|
|
13
12
|
|
|
@@ -16,7 +15,6 @@ from devopsdriver.azdo.workitem.client import Client as WIClient
|
|
|
16
15
|
from devopsdriver.azdo.pipeline.client import Client as PLClient
|
|
17
16
|
from devopsdriver.azdo.builds.client import Client as BClient
|
|
18
17
|
|
|
19
|
-
|
|
20
18
|
# for testing
|
|
21
19
|
CONNECTION = AzureConnection
|
|
22
20
|
AUTHENTICATION = MSBasicAuthentication
|
|
@@ -3,15 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
"""Azure Pipeline Run Log"""
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
from azure.devops.v7_1.pipelines.models import Log as AzureLog
|
|
8
7
|
|
|
9
8
|
from requests import get as get_url
|
|
10
9
|
|
|
11
10
|
from devopsdriver.azdo.azureobject import AzureObject
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
GET_URL = get_url
|
|
12
|
+
GET_URL = get_url # pylint: disable=invalid-name
|
|
15
13
|
|
|
16
14
|
|
|
17
15
|
class Log(AzureObject): # pylint: disable=too-few-public-methods
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
"""Azure WorkItem Client"""
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
from azure.devops.v7_1.work_item_tracking.models import Wiql as AzureWiql
|
|
8
7
|
from azure.devops.v7_1.work_item_tracking.models import WorkItem as AzureWorkItem
|
|
9
8
|
from azure.devops.v7_1.work_item_tracking.models import TeamContext
|
|
@@ -21,9 +20,9 @@ class Client:
|
|
|
21
20
|
def query(
|
|
22
21
|
self,
|
|
23
22
|
wiql: Wiql | str,
|
|
24
|
-
team_context: TeamContext = None,
|
|
25
|
-
time_precision: bool = None,
|
|
26
|
-
top: int = None,
|
|
23
|
+
team_context: TeamContext | None = None,
|
|
24
|
+
time_precision: bool | None = None,
|
|
25
|
+
top: int | None = None,
|
|
27
26
|
) -> WorkItemQueryResult:
|
|
28
27
|
"""Perform a wiql query
|
|
29
28
|
|
|
@@ -46,15 +45,15 @@ class Client:
|
|
|
46
45
|
def get_history( # pylint: disable=too-many-positional-arguments,too-many-arguments
|
|
47
46
|
self,
|
|
48
47
|
wi_id: int,
|
|
49
|
-
project: str = None,
|
|
50
|
-
top: int = None,
|
|
51
|
-
skip: int = None,
|
|
52
|
-
expand: str = None,
|
|
48
|
+
project: str | None = None,
|
|
49
|
+
top: int | None = None,
|
|
50
|
+
skip: int | None = None,
|
|
51
|
+
expand: str | None = None,
|
|
53
52
|
) -> list[AzureWorkItem]:
|
|
54
53
|
"""Simple wrapper around get_revisions"""
|
|
55
54
|
return self.client.get_revisions(wi_id, project, top, skip, expand)
|
|
56
55
|
|
|
57
|
-
def find_ids(self, wiql: Wiql | str, top: int = None) -> list[int]:
|
|
56
|
+
def find_ids(self, wiql: Wiql | str, top: int | None = None) -> list[int]:
|
|
58
57
|
"""Given a query, find the work item ids
|
|
59
58
|
|
|
60
59
|
Args:
|
|
@@ -73,9 +72,9 @@ class Client:
|
|
|
73
72
|
# query_results_type: workItem
|
|
74
73
|
# query_type: flat
|
|
75
74
|
# columns: list of name, reference_name, url
|
|
76
|
-
return [i.id for i in found.work_items]
|
|
75
|
+
return [i.id for i in found.work_items] if found.work_items else []
|
|
77
76
|
|
|
78
|
-
def find(self, wiql: Wiql | str, top: int = None) -> list[list[AzureObject]]:
|
|
77
|
+
def find(self, wiql: Wiql | str, top: int | None = None) -> list[list[AzureObject]]:
|
|
79
78
|
"""Gets the full history of items found in a WIQL search
|
|
80
79
|
|
|
81
80
|
Args:
|
|
@@ -19,7 +19,6 @@ ORDER BY [System.ChangedDate] DESC
|
|
|
19
19
|
ASOF '02-11-2020'
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
|
-
|
|
23
22
|
from datetime import datetime, date
|
|
24
23
|
|
|
25
24
|
|
|
@@ -145,13 +144,20 @@ class Compare: # pylint: disable=too-few-public-methods
|
|
|
145
144
|
return f"{str(self.left)} {self.operator} {str(self.right)}"
|
|
146
145
|
|
|
147
146
|
|
|
147
|
+
class Under(Compare): # pylint: disable=too-few-public-methods
|
|
148
|
+
"""checks for under, like area path or iteration path"""
|
|
149
|
+
|
|
150
|
+
def __init__(self, field_name: Field | str, value: Value | str):
|
|
151
|
+
super().__init__(field_name, value, "UNDER")
|
|
152
|
+
|
|
153
|
+
|
|
148
154
|
class In(Compare): # pylint: disable=too-few-public-methods
|
|
149
155
|
"""checks for field in a list of values"""
|
|
150
156
|
|
|
151
157
|
def __init__(
|
|
152
158
|
self,
|
|
153
159
|
field: Field | str,
|
|
154
|
-
*values:
|
|
160
|
+
*values: Value | str | date | datetime | int | float,
|
|
155
161
|
):
|
|
156
162
|
super().__init__(
|
|
157
163
|
field,
|
|
@@ -167,7 +173,7 @@ class NotIn(Compare): # pylint: disable=too-few-public-methods
|
|
|
167
173
|
def __init__(
|
|
168
174
|
self,
|
|
169
175
|
field: Field | str,
|
|
170
|
-
*values:
|
|
176
|
+
*values: Value | str | date | datetime | int | float,
|
|
171
177
|
):
|
|
172
178
|
super().__init__(
|
|
173
179
|
field,
|
|
@@ -234,7 +240,7 @@ class GreaterThanOrEqual(Compare): # pylint: disable=too-few-public-methods
|
|
|
234
240
|
class Expression: # pylint: disable=too-few-public-methods
|
|
235
241
|
"""Join several compares"""
|
|
236
242
|
|
|
237
|
-
def __init__(self, operator: str, *compares
|
|
243
|
+
def __init__(self, operator: str, *compares):
|
|
238
244
|
self.operator = operator
|
|
239
245
|
self.expressions = compares
|
|
240
246
|
|
|
@@ -245,14 +251,14 @@ class Expression: # pylint: disable=too-few-public-methods
|
|
|
245
251
|
class And(Expression): # pylint: disable=too-few-public-methods
|
|
246
252
|
"""Join compares via AND"""
|
|
247
253
|
|
|
248
|
-
def __init__(self, *compares:
|
|
254
|
+
def __init__(self, *compares: Compare | Expression):
|
|
249
255
|
super().__init__("AND", *compares)
|
|
250
256
|
|
|
251
257
|
|
|
252
258
|
class Or(Expression): # pylint: disable=too-few-public-methods
|
|
253
259
|
"""join compares via OR"""
|
|
254
260
|
|
|
255
|
-
def __init__(self, *compares:
|
|
261
|
+
def __init__(self, *compares: Compare | Expression):
|
|
256
262
|
super().__init__("OR", *compares)
|
|
257
263
|
|
|
258
264
|
|
|
@@ -264,8 +270,10 @@ class Wiql:
|
|
|
264
270
|
self.search = None
|
|
265
271
|
self.order = []
|
|
266
272
|
self.snapshot = None
|
|
273
|
+
self.source = "WorkItems"
|
|
274
|
+
self.mode_type = None
|
|
267
275
|
|
|
268
|
-
def select(self, *fields:
|
|
276
|
+
def select(self, *fields: Field | str):
|
|
269
277
|
"""The fields to select
|
|
270
278
|
|
|
271
279
|
Returns:
|
|
@@ -286,6 +294,18 @@ class Wiql:
|
|
|
286
294
|
self.search = expression
|
|
287
295
|
return self
|
|
288
296
|
|
|
297
|
+
def from_source(self, source: str):
|
|
298
|
+
"""Sets the FROM field"""
|
|
299
|
+
self.source = source
|
|
300
|
+
return self
|
|
301
|
+
|
|
302
|
+
def mode(self, results_mode: str):
|
|
303
|
+
"""Sets the mode for link queries
|
|
304
|
+
results_mode may be one of: MustContain, MayContain, DoesNotContain, Recursive, None
|
|
305
|
+
"""
|
|
306
|
+
self.mode_type = results_mode
|
|
307
|
+
return self
|
|
308
|
+
|
|
289
309
|
def order_by(self, *orders):
|
|
290
310
|
"""Set the fields to order the results by
|
|
291
311
|
|
|
@@ -315,4 +335,5 @@ class Wiql:
|
|
|
315
335
|
f" ORDER BY {', '.join(str(o) for o in self.order)}" if self.order else ""
|
|
316
336
|
)
|
|
317
337
|
asof = f" ASOF {str(self.snapshot)}" if self.snapshot else ""
|
|
318
|
-
|
|
338
|
+
mode = f" MODE({self.mode_type})" if self.mode_type else ""
|
|
339
|
+
return f"SELECT {select} FROM {self.source}{where}{order}{asof}{mode}"
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
"""Module Doc"""
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
from os.path import dirname, join, abspath
|
|
8
7
|
from sys import argv as sys_argv
|
|
9
8
|
from getpass import getpass as os_getpass
|
|
@@ -14,9 +13,9 @@ from .settings import Settings
|
|
|
14
13
|
from .template import Template
|
|
15
14
|
|
|
16
15
|
ARGV = sys_argv
|
|
17
|
-
PRINT = print
|
|
18
|
-
SET_PASSWORD = set_password
|
|
19
|
-
GET_PASS = os_getpass
|
|
16
|
+
PRINT = print # pylint: disable=invalid-name
|
|
17
|
+
SET_PASSWORD = set_password # pylint: disable=invalid-name
|
|
18
|
+
GET_PASS = os_getpass # pylint: disable=invalid-name
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def main() -> None:
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
"""Ability to send emails with embedded images"""
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
from smtplib import SMTP as OS_SMTP, SMTP_SSL as OS_SMTP_SSL
|
|
8
7
|
from email.mime.multipart import MIMEMultipart as OS_MIMEMultipart
|
|
9
8
|
from email.mime.text import MIMEText as OS_MIMEText
|
|
@@ -11,7 +10,6 @@ from email.mime.image import MIMEImage as OS_MIMEImage
|
|
|
11
10
|
|
|
12
11
|
from devopsdriver.settings import Settings
|
|
13
12
|
|
|
14
|
-
|
|
15
13
|
IMAGE_HEADERS = {".png": b"\x89PNG\r\n\x1a\n", ".jpg": b"\xff\xd8\xff"}
|
|
16
14
|
|
|
17
15
|
# for testing
|
|
@@ -41,11 +39,11 @@ def image_extension(data: bytes) -> str:
|
|
|
41
39
|
raise AttributeError("Image not a known format: " + ",".join(IMAGE_HEADERS))
|
|
42
40
|
|
|
43
41
|
|
|
44
|
-
def send_email(
|
|
42
|
+
def send_email( # pylint: disable=too-many-locals
|
|
45
43
|
recipients: str | list[str],
|
|
46
44
|
subject: str,
|
|
47
45
|
html_body: str,
|
|
48
|
-
settings: Settings = None,
|
|
46
|
+
settings: Settings | None = None,
|
|
49
47
|
**image_data,
|
|
50
48
|
):
|
|
51
49
|
"""Sends an email with embedded images
|
|
@@ -65,7 +63,9 @@ def send_email(
|
|
|
65
63
|
", ".join(missing) + " not found in:\n" + "\n".join(settings.search_files)
|
|
66
64
|
)
|
|
67
65
|
sender = settings["smtp.sender"]
|
|
66
|
+
assert isinstance(sender, str), sender
|
|
68
67
|
username = settings.get("smtp.username", sender)
|
|
68
|
+
assert isinstance(username, str), username
|
|
69
69
|
message = MIMEMULTIPART()
|
|
70
70
|
message["Subject"] = subject
|
|
71
71
|
message["From"] = sender
|
|
@@ -85,8 +85,14 @@ def send_email(
|
|
|
85
85
|
)
|
|
86
86
|
message.attach(image)
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
server = settings["smtp.server"]
|
|
89
|
+
assert isinstance(server, str), server
|
|
90
|
+
port = settings["smtp.port"]
|
|
91
|
+
assert isinstance(port, int), port
|
|
92
|
+
password = settings["smtp.password"]
|
|
93
|
+
assert isinstance(password, str), password
|
|
94
|
+
with connection_type(server, port) as smtp:
|
|
89
95
|
smtp.set_debuglevel(False)
|
|
90
|
-
smtp.login(username,
|
|
96
|
+
smtp.login(username, password)
|
|
91
97
|
smtp.sendmail(sender, recipients, message.as_string())
|
|
92
98
|
smtp.quit()
|
|
@@ -75,7 +75,8 @@ api:
|
|
|
75
75
|
johndoe and janedoe share the same password, so you just need to update the `user`
|
|
76
76
|
"""
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
from __future__ import annotations
|
|
79
|
+
from dataclasses import MISSING, fields, is_dataclass
|
|
79
80
|
from json import load
|
|
80
81
|
from os.path import dirname, basename, splitext, join
|
|
81
82
|
from os import environ as os_environ, makedirs as os_makedirs
|
|
@@ -83,22 +84,81 @@ from re import compile as regex
|
|
|
83
84
|
from platform import system as os_system
|
|
84
85
|
from sys import argv as sys_argv
|
|
85
86
|
from getpass import getpass as os_getpass
|
|
87
|
+
from typing import Any, TypeVar, Union, cast, get_args, get_origin, get_type_hints
|
|
86
88
|
|
|
87
89
|
from yaml import safe_load
|
|
88
90
|
from keyring import get_password, set_password
|
|
89
91
|
from keyring.backends import fail
|
|
90
92
|
|
|
91
|
-
|
|
92
93
|
# for testing
|
|
93
94
|
ENVIRON = os_environ
|
|
94
95
|
ARGV = sys_argv
|
|
95
|
-
SYSTEM = os_system
|
|
96
|
-
MAKEDIRS = os_makedirs
|
|
96
|
+
SYSTEM = os_system # pylint: disable=invalid-name
|
|
97
|
+
MAKEDIRS = os_makedirs # pylint: disable=invalid-name
|
|
97
98
|
SHARED = "devopsdriver"
|
|
98
|
-
PRINT = print
|
|
99
|
-
GET_PASSWORD = get_password
|
|
100
|
-
SET_PASSWORD = set_password
|
|
101
|
-
GET_PASS = os_getpass
|
|
99
|
+
PRINT = print # pylint: disable=invalid-name
|
|
100
|
+
GET_PASSWORD = get_password # pylint: disable=invalid-name
|
|
101
|
+
SET_PASSWORD = set_password # pylint: disable=invalid-name
|
|
102
|
+
GET_PASS = os_getpass # pylint: disable=invalid-name
|
|
103
|
+
T = TypeVar("T")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _is_instance_of_type( # pylint: disable=too-many-branches, too-many-return-statements
|
|
107
|
+
value: Any, expected: Any
|
|
108
|
+
) -> bool:
|
|
109
|
+
"""Check if a value matches an expected type, including support for common typing constructs."""
|
|
110
|
+
if expected is Any:
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
origin = get_origin(expected)
|
|
114
|
+
args = get_args(expected)
|
|
115
|
+
|
|
116
|
+
if origin is Union: # Optional[T] / Union[T1, T2, ...]
|
|
117
|
+
return any(_is_instance_of_type(value, t) for t in args)
|
|
118
|
+
|
|
119
|
+
if origin is list: # list[T]
|
|
120
|
+
if not isinstance(value, list):
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
if not args:
|
|
124
|
+
return True
|
|
125
|
+
|
|
126
|
+
item_type = args[0]
|
|
127
|
+
return all(_is_instance_of_type(v, item_type) for v in value)
|
|
128
|
+
|
|
129
|
+
if origin is dict: # dict[K, V]
|
|
130
|
+
if not isinstance(value, dict):
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
if len(args) != 2:
|
|
134
|
+
return True
|
|
135
|
+
|
|
136
|
+
key_type, val_type = args
|
|
137
|
+
return all(
|
|
138
|
+
_is_instance_of_type(k, key_type) and _is_instance_of_type(v, val_type)
|
|
139
|
+
for k, v in value.items()
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if origin is tuple: # tuple[T1, T2] or tuple[T, ...]
|
|
143
|
+
if not isinstance(value, tuple):
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
if not args:
|
|
147
|
+
return True
|
|
148
|
+
|
|
149
|
+
if len(args) == 2 and args[1] is Ellipsis:
|
|
150
|
+
return all(_is_instance_of_type(v, args[0]) for v in value)
|
|
151
|
+
|
|
152
|
+
if len(value) != len(args):
|
|
153
|
+
return False
|
|
154
|
+
|
|
155
|
+
return all(_is_instance_of_type(v, t) for v, t in zip(value, args))
|
|
156
|
+
|
|
157
|
+
try: # Fallback to normal isinstance
|
|
158
|
+
return isinstance(value, expected)
|
|
159
|
+
|
|
160
|
+
except TypeError:
|
|
161
|
+
return False # Covers unsupported runtime checks for some typing constructs
|
|
102
162
|
|
|
103
163
|
|
|
104
164
|
def load_json(path: str) -> dict:
|
|
@@ -147,7 +207,9 @@ class Settings:
|
|
|
147
207
|
}
|
|
148
208
|
ENV_VAR_PATTERN = regex(r"\${(\S+)}")
|
|
149
209
|
|
|
150
|
-
def __init__(
|
|
210
|
+
def __init__(
|
|
211
|
+
self, file: str, *directories, shared_name: str | None = None, **settings
|
|
212
|
+
):
|
|
151
213
|
"""Create a settings object using a file, directories to search, and settings overrides
|
|
152
214
|
|
|
153
215
|
Args:
|
|
@@ -172,7 +234,7 @@ class Settings:
|
|
|
172
234
|
self.environ = {}
|
|
173
235
|
self.secrets = {}
|
|
174
236
|
|
|
175
|
-
def __bypass(self, key: str, name: str, store: dict):
|
|
237
|
+
def __bypass(self, key: str, name: str | None, store: dict):
|
|
176
238
|
if name is None:
|
|
177
239
|
for setting_key, store_name in self.settings.get(key, {}).items():
|
|
178
240
|
store[setting_key] = store_name
|
|
@@ -181,7 +243,7 @@ class Settings:
|
|
|
181
243
|
store[key] = name
|
|
182
244
|
return self
|
|
183
245
|
|
|
184
|
-
def key(self, key: str, name: str = None):
|
|
246
|
+
def key(self, key: str, name: str | None = None):
|
|
185
247
|
"""Sets a keychain name to map to a settings value.
|
|
186
248
|
|
|
187
249
|
Args:
|
|
@@ -195,7 +257,7 @@ class Settings:
|
|
|
195
257
|
"""
|
|
196
258
|
return self.__bypass(key, name, self.secrets)
|
|
197
259
|
|
|
198
|
-
def cli(self, key: str, name: str = None):
|
|
260
|
+
def cli(self, key: str, name: str | None = None):
|
|
199
261
|
"""Sets a command line switch to map to a settings value.
|
|
200
262
|
|
|
201
263
|
Args:
|
|
@@ -209,7 +271,7 @@ class Settings:
|
|
|
209
271
|
"""
|
|
210
272
|
return self.__bypass(key, name, self.opts)
|
|
211
273
|
|
|
212
|
-
def env(self, key: str, name: str = None):
|
|
274
|
+
def env(self, key: str, name: str | None = None):
|
|
213
275
|
"""Sets an environment variable to map to a settings value.
|
|
214
276
|
|
|
215
277
|
Args:
|
|
@@ -223,6 +285,37 @@ class Settings:
|
|
|
223
285
|
"""
|
|
224
286
|
return self.__bypass(key, name, self.environ)
|
|
225
287
|
|
|
288
|
+
def parse(self, cls: type[T]) -> T:
|
|
289
|
+
"""Extract settings into a dataclass for type safety"""
|
|
290
|
+
if not is_dataclass(cls):
|
|
291
|
+
raise TypeError(f"{cls} must be a dataclass type")
|
|
292
|
+
|
|
293
|
+
type_hints = get_type_hints(cls)
|
|
294
|
+
kwargs = {}
|
|
295
|
+
|
|
296
|
+
for f in fields(cls):
|
|
297
|
+
expected_type = type_hints.get(f.name, Any)
|
|
298
|
+
|
|
299
|
+
# Pull from settings if present, otherwise use dataclass default/default_factory
|
|
300
|
+
if self.has(f.name):
|
|
301
|
+
value = self.get(f.name)
|
|
302
|
+
elif f.default is not MISSING:
|
|
303
|
+
value = f.default
|
|
304
|
+
elif f.default_factory is not MISSING:
|
|
305
|
+
value = f.default_factory()
|
|
306
|
+
else:
|
|
307
|
+
raise KeyError(f"Missing required setting: {f.name}")
|
|
308
|
+
|
|
309
|
+
if not _is_instance_of_type(value, expected_type):
|
|
310
|
+
raise TypeError(
|
|
311
|
+
f"Invalid type for '{f.name}': expected {expected_type}, "
|
|
312
|
+
+ f"got {type(value).__name__}"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
kwargs[f.name] = value
|
|
316
|
+
|
|
317
|
+
return cast(T, cls(**kwargs))
|
|
318
|
+
|
|
226
319
|
@staticmethod
|
|
227
320
|
def __patch_instance(key: str) -> str:
|
|
228
321
|
for env_key, value in ENVIRON.items():
|
|
@@ -232,7 +325,7 @@ class Settings:
|
|
|
232
325
|
return "${" + key + "}"
|
|
233
326
|
|
|
234
327
|
@staticmethod
|
|
235
|
-
def __patch(value
|
|
328
|
+
def __patch(value):
|
|
236
329
|
if isinstance(value, str):
|
|
237
330
|
return Settings.ENV_VAR_PATTERN.sub(
|
|
238
331
|
lambda m: Settings.__patch_instance(m.group(1)), value
|
|
@@ -257,7 +350,7 @@ class Settings:
|
|
|
257
350
|
secret_name = parts[1] if len(parts) == 2 else parts[0]
|
|
258
351
|
return (service, secret_name)
|
|
259
352
|
|
|
260
|
-
def __lookup(self, key: str, check: bool, default
|
|
353
|
+
def __lookup(self, key: str, check: bool, default=None):
|
|
261
354
|
# Settings passed in override everything
|
|
262
355
|
if key in self.overrides:
|
|
263
356
|
return True if check else self.overrides[key]
|
|
@@ -293,7 +386,7 @@ class Settings:
|
|
|
293
386
|
|
|
294
387
|
return Settings.__patch(level.get(keys[-1], default))
|
|
295
388
|
|
|
296
|
-
def get(self, key: str, default
|
|
389
|
+
def get(self, key: str, default=None):
|
|
297
390
|
"""Dictionary-like get
|
|
298
391
|
|
|
299
392
|
Args:
|
|
@@ -314,12 +407,12 @@ class Settings:
|
|
|
314
407
|
Returns:
|
|
315
408
|
bool: True if the key exists
|
|
316
409
|
"""
|
|
317
|
-
return self.__lookup(key, check=True)
|
|
410
|
+
return bool(self.__lookup(key, check=True))
|
|
318
411
|
|
|
319
412
|
def __contains__(self, key: str) -> bool:
|
|
320
413
|
return self.has(key)
|
|
321
414
|
|
|
322
|
-
def __getitem__(self, key: str)
|
|
415
|
+
def __getitem__(self, key: str):
|
|
323
416
|
if not self.has(key):
|
|
324
417
|
raise KeyError(key)
|
|
325
418
|
|
|
@@ -371,6 +464,6 @@ class Settings:
|
|
|
371
464
|
|
|
372
465
|
for extension, name, directory, loader in search_info:
|
|
373
466
|
contents = loader(join(directory, name + extension))
|
|
374
|
-
Settings.__merge(settings, contents)
|
|
467
|
+
Settings.__merge(settings, contents if contents else {})
|
|
375
468
|
|
|
376
469
|
return settings
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devopsdriver
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.53
|
|
4
4
|
Summary: DevOps tools
|
|
5
5
|
Author-email: Marc Page <marcallenpage@gmail.com>
|
|
6
6
|
License: This is free and unencumbered software released into the public domain.
|
|
@@ -47,11 +47,11 @@ Requires-Python: >=3.10
|
|
|
47
47
|
Description-Content-Type: text/markdown
|
|
48
48
|
License-File: LICENSE
|
|
49
49
|
Requires-Dist: PyYAML==6.0.3
|
|
50
|
-
Requires-Dist: keyring==25.
|
|
51
|
-
Requires-Dist: setuptools==
|
|
50
|
+
Requires-Dist: keyring==25.7.0
|
|
51
|
+
Requires-Dist: setuptools==82.0.1
|
|
52
52
|
Requires-Dist: azure-devops==7.1.0b4
|
|
53
|
-
Requires-Dist: Mako==1.3.
|
|
54
|
-
Requires-Dist: PyGithub==2.
|
|
53
|
+
Requires-Dist: Mako==1.3.11
|
|
54
|
+
Requires-Dist: PyGithub==2.9.1
|
|
55
55
|
Provides-Extra: dev
|
|
56
56
|
Requires-Dist: black>=24.3.0; extra == "dev"
|
|
57
57
|
Requires-Dist: pylint>=3.1.0; extra == "dev"
|
|
@@ -64,7 +64,7 @@ Dynamic: license-file
|
|
|
64
64
|
# devops-driver
|
|
65
65
|
|
|
66
66
|

|
|
67
|
-
[](https://pypi.org/project/devopsdriver/0.1.53/)
|
|
68
68
|
[](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
|
|
69
69
|
[](https://github.com/marcpage/devops-driver/graphs/contributors)
|
|
70
70
|
[](http://makeapullrequest.com)
|
|
@@ -75,7 +75,7 @@ Dynamic: license-file
|
|
|
75
75
|
[](https://github.com/marcpage/devops-driver)
|
|
76
76
|
|
|
77
77
|
[](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
|
|
78
|
-
[](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
|
|
79
79
|
[](https://github.com/marcpage/devops-driver/issues)
|
|
80
80
|
[](https://github.com/marcpage/devops-driver/pulls)
|
|
81
81
|
|
|
@@ -7,11 +7,11 @@ dynamic = ["version"]
|
|
|
7
7
|
requires-python = ">= 3.10"
|
|
8
8
|
dependencies = [
|
|
9
9
|
"PyYAML==6.0.3",
|
|
10
|
-
"keyring==25.
|
|
11
|
-
"setuptools==
|
|
10
|
+
"keyring==25.7.0",
|
|
11
|
+
"setuptools==82.0.1", # neded for azure-devops to use 7.1 API
|
|
12
12
|
"azure-devops==7.1.0b4",
|
|
13
|
-
"Mako==1.3.
|
|
14
|
-
"PyGithub==2.
|
|
13
|
+
"Mako==1.3.11",
|
|
14
|
+
"PyGithub==2.9.1",
|
|
15
15
|
]
|
|
16
16
|
keywords = [
|
|
17
17
|
"azure",
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
"""
|
|
3
|
+
"""Test work item query language"""
|
|
4
4
|
|
|
5
5
|
from datetime import date, datetime
|
|
6
6
|
|
|
7
7
|
from devopsdriver.azdo import Wiql
|
|
8
8
|
from devopsdriver.azdo import Ascending, Descending, Value
|
|
9
|
-
from devopsdriver.azdo import IsEmpty, IsNotEmpty, And, Or, In, NotIn
|
|
9
|
+
from devopsdriver.azdo import IsEmpty, IsNotEmpty, And, Or, In, NotIn, Under
|
|
10
10
|
from devopsdriver.azdo import GreaterThan, LessThan, Equal, NotEqual
|
|
11
11
|
from devopsdriver.azdo import GreaterThanOrEqual, LessThanOrEqual
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def test_no_params() -> None:
|
|
15
15
|
"""Test empty, default wiql"""
|
|
16
|
-
assert str(Wiql()) == "SELECT [System.Id] FROM
|
|
16
|
+
assert str(Wiql()) == "SELECT [System.Id] FROM WorkItems", str(Wiql())
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def test_expressions() -> None:
|
|
@@ -43,7 +43,7 @@ def test_expressions() -> None:
|
|
|
43
43
|
.asof(start)
|
|
44
44
|
)
|
|
45
45
|
expected = (
|
|
46
|
-
"""SELECT [System.State], [System.Id] FROM
|
|
46
|
+
"""SELECT [System.State], [System.Id] FROM WorkItems """
|
|
47
47
|
+ """WHERE [System.State] = "New" AND [System.Title] IS EMPTY """
|
|
48
48
|
+ """AND [Microsoft.VSTS.Common.Priority] IS NOT EMPTY """
|
|
49
49
|
+ """AND [System.CreatedDate] > "06/30/2024" """
|
|
@@ -71,14 +71,56 @@ def test_in_and_not_in() -> None:
|
|
|
71
71
|
And(In("State", "New", "Ready for Development"), NotIn("Priority", 1, 2))
|
|
72
72
|
)
|
|
73
73
|
expected = (
|
|
74
|
-
"""SELECT [System.Id] FROM
|
|
74
|
+
"""SELECT [System.Id] FROM WorkItems WHERE [System.State] """
|
|
75
75
|
+ """IN ("New", "Ready for Development") AND [Microsoft.VSTS.Common.Priority] """
|
|
76
76
|
+ """NOT IN (1, 2)"""
|
|
77
77
|
)
|
|
78
78
|
assert str(builder) == expected, str(builder)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def test_under() -> None:
|
|
82
|
+
"""Test under operator"""
|
|
83
|
+
builder = Wiql().where(Under("System.AreaPath", "/Project/Team/Product"))
|
|
84
|
+
expected = (
|
|
85
|
+
"""SELECT [System.Id] FROM WorkItems WHERE [System.AreaPath] """
|
|
86
|
+
+ """UNDER "/Project/Team/Product\""""
|
|
87
|
+
)
|
|
88
|
+
assert str(builder) == expected, str(builder)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_from() -> None:
|
|
92
|
+
"""Test from"""
|
|
93
|
+
builder = (
|
|
94
|
+
Wiql()
|
|
95
|
+
.from_source("Wiki")
|
|
96
|
+
.where(Under("System.AreaPath", "/Project/Team/Product"))
|
|
97
|
+
)
|
|
98
|
+
expected = (
|
|
99
|
+
"""SELECT [System.Id] FROM Wiki WHERE [System.AreaPath] """
|
|
100
|
+
+ """UNDER "/Project/Team/Product\""""
|
|
101
|
+
)
|
|
102
|
+
assert str(builder) == expected, str(builder)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_mode() -> None:
|
|
106
|
+
"""Test mode"""
|
|
107
|
+
builder = (
|
|
108
|
+
Wiql()
|
|
109
|
+
.mode("Recursive")
|
|
110
|
+
.where(Under("System.AreaPath", "/Project/Team/Product"))
|
|
111
|
+
)
|
|
112
|
+
expected = (
|
|
113
|
+
"""SELECT [System.Id] FROM WorkItems WHERE [System.AreaPath] """
|
|
114
|
+
+ """UNDER "/Project/Team/Product" """
|
|
115
|
+
+ """MODE(Recursive)"""
|
|
116
|
+
)
|
|
117
|
+
assert str(builder) == expected, [str(builder), expected]
|
|
118
|
+
|
|
119
|
+
|
|
81
120
|
if __name__ == "__main__":
|
|
121
|
+
test_under()
|
|
122
|
+
test_from()
|
|
123
|
+
test_mode()
|
|
82
124
|
test_in_and_not_in()
|
|
83
125
|
test_invalid_value_type()
|
|
84
126
|
test_expressions()
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
"""
|
|
3
|
+
"""Tests Settings class"""
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from itertools import product
|
|
6
7
|
from os.path import join
|
|
7
8
|
from string import ascii_lowercase
|
|
8
|
-
from
|
|
9
|
+
from tempfile import TemporaryDirectory
|
|
10
|
+
from typing import Any
|
|
9
11
|
|
|
10
12
|
from helpers import setup_settings, ensure, write
|
|
11
13
|
|
|
@@ -13,6 +15,21 @@ from devopsdriver import settings # debug access
|
|
|
13
15
|
from devopsdriver.settings import Settings
|
|
14
16
|
|
|
15
17
|
|
|
18
|
+
@dataclass
|
|
19
|
+
class MockSettings: # pylint: disable=too-many-instance-attributes
|
|
20
|
+
"""Testing type-safe class"""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
names: list[str]
|
|
24
|
+
age_names: dict[int, str]
|
|
25
|
+
age: int
|
|
26
|
+
stuff: list
|
|
27
|
+
info: dict
|
|
28
|
+
alive: bool = True
|
|
29
|
+
years: list[int] = field(default_factory=lambda: [2004, 2104])
|
|
30
|
+
random: Any = None
|
|
31
|
+
|
|
32
|
+
|
|
16
33
|
def __setup_files(directory: str, dir1: str, dir2: str) -> None:
|
|
17
34
|
"""
|
|
18
35
|
Priorities:
|
|
@@ -67,7 +84,7 @@ def __setup_files(directory: str, dir1: str, dir2: str) -> None:
|
|
|
67
84
|
letters.pop()
|
|
68
85
|
|
|
69
86
|
|
|
70
|
-
def test_basic():
|
|
87
|
+
def test_basic() -> None: # pylint: disable=too-many-statements
|
|
71
88
|
"""test the basic functionality"""
|
|
72
89
|
with TemporaryDirectory() as working_dir:
|
|
73
90
|
base_dir = join(working_dir, "base")
|
|
@@ -196,7 +213,7 @@ def test_basic():
|
|
|
196
213
|
pass
|
|
197
214
|
|
|
198
215
|
|
|
199
|
-
def test_cli_env_in_yaml():
|
|
216
|
+
def test_cli_env_in_yaml() -> None:
|
|
200
217
|
"""test setting cli and env lookups in the yaml itself"""
|
|
201
218
|
with TemporaryDirectory() as working_dir:
|
|
202
219
|
base_dir = join(working_dir, "base")
|
|
@@ -245,7 +262,7 @@ def test_cli_env_in_yaml():
|
|
|
245
262
|
assert opts["zz"] == "environ zz", opts["zz"]
|
|
246
263
|
|
|
247
264
|
|
|
248
|
-
def test_environ_values():
|
|
265
|
+
def test_environ_values() -> None:
|
|
249
266
|
"""test environment variable substitution"""
|
|
250
267
|
with TemporaryDirectory() as working_dir:
|
|
251
268
|
base_dir = join(working_dir, "base")
|
|
@@ -272,7 +289,7 @@ def test_environ_values():
|
|
|
272
289
|
assert opts["value"] == "testing ${noenv} for noenv", opts["value"]
|
|
273
290
|
|
|
274
291
|
|
|
275
|
-
def test_secret():
|
|
292
|
+
def test_secret() -> None:
|
|
276
293
|
"""test os secret storage"""
|
|
277
294
|
with TemporaryDirectory() as working_dir:
|
|
278
295
|
base_dir = join(working_dir, "base")
|
|
@@ -290,7 +307,225 @@ def test_secret():
|
|
|
290
307
|
assert opts["password"] == "setec astronomy", opts["password"]
|
|
291
308
|
|
|
292
309
|
|
|
310
|
+
def test_parse(): # pylint: disable=too-many-statements
|
|
311
|
+
"""Test parsing into type-safe type"""
|
|
312
|
+
with TemporaryDirectory() as working_dir:
|
|
313
|
+
base_dir = join(working_dir, "base")
|
|
314
|
+
settings_path = join(base_dir, "parse.yml")
|
|
315
|
+
setup_settings(
|
|
316
|
+
os="Unknown",
|
|
317
|
+
shared="test",
|
|
318
|
+
Linux=join(base_dir, "Linux"),
|
|
319
|
+
Darwin=join(base_dir, "macOS"),
|
|
320
|
+
Windows=join(base_dir, "Windows"),
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
write(
|
|
324
|
+
settings_path,
|
|
325
|
+
name="John Doe",
|
|
326
|
+
names=["John", "Doe"],
|
|
327
|
+
age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
|
|
328
|
+
age=50,
|
|
329
|
+
alive=True,
|
|
330
|
+
years=(1973, 2073),
|
|
331
|
+
stuff=[],
|
|
332
|
+
info={},
|
|
333
|
+
)
|
|
334
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
335
|
+
|
|
336
|
+
assert test_settings.name == "John Doe", test_settings.name
|
|
337
|
+
assert len(test_settings.names) == 2, test_settings.names
|
|
338
|
+
assert test_settings.names[0] == "John", test_settings.names
|
|
339
|
+
assert test_settings.names[1] == "Doe", test_settings.names
|
|
340
|
+
assert len(test_settings.age_names) == 3, test_settings.age_names
|
|
341
|
+
assert 0 in test_settings.age_names, test_settings.age_names
|
|
342
|
+
assert 50 in test_settings.age_names, test_settings.age_names
|
|
343
|
+
assert 100 in test_settings.age_names, test_settings.age_names
|
|
344
|
+
assert test_settings.age_names[0] == "Newborn", test_settings.age_names
|
|
345
|
+
assert test_settings.age_names[50] == "Middle age", test_settings.age_names
|
|
346
|
+
assert test_settings.age_names[100] == "Old", test_settings.age_names
|
|
347
|
+
assert test_settings.age == 50
|
|
348
|
+
assert test_settings.alive
|
|
349
|
+
assert len(test_settings.years) == 2, test_settings.years
|
|
350
|
+
assert test_settings.years[0] == 1973, test_settings.years
|
|
351
|
+
assert test_settings.years[1] == 2073, test_settings.years
|
|
352
|
+
assert test_settings.random is None, test_settings.random
|
|
353
|
+
assert len(test_settings.stuff) == 0, test_settings.stuff
|
|
354
|
+
assert len(test_settings.info) == 0, test_settings.info
|
|
355
|
+
|
|
356
|
+
write(
|
|
357
|
+
settings_path,
|
|
358
|
+
name="John Doe",
|
|
359
|
+
names=["John", "Doe"],
|
|
360
|
+
age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
|
|
361
|
+
age=50,
|
|
362
|
+
stuff=[],
|
|
363
|
+
info={},
|
|
364
|
+
)
|
|
365
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
366
|
+
|
|
367
|
+
assert test_settings.name == "John Doe", test_settings.name
|
|
368
|
+
assert len(test_settings.names) == 2, test_settings.names
|
|
369
|
+
assert test_settings.names[0] == "John", test_settings.names
|
|
370
|
+
assert test_settings.names[1] == "Doe", test_settings.names
|
|
371
|
+
assert len(test_settings.age_names) == 3, test_settings.age_names
|
|
372
|
+
assert 0 in test_settings.age_names, test_settings.age_names
|
|
373
|
+
assert 50 in test_settings.age_names, test_settings.age_names
|
|
374
|
+
assert 100 in test_settings.age_names, test_settings.age_names
|
|
375
|
+
assert test_settings.age_names[0] == "Newborn", test_settings.age_names
|
|
376
|
+
assert test_settings.age_names[50] == "Middle age", test_settings.age_names
|
|
377
|
+
assert test_settings.age_names[100] == "Old", test_settings.age_names
|
|
378
|
+
assert test_settings.age == 50
|
|
379
|
+
assert test_settings.alive
|
|
380
|
+
assert len(test_settings.years) == 2, test_settings.years
|
|
381
|
+
assert test_settings.years[0] == 2004, test_settings.years
|
|
382
|
+
assert test_settings.years[1] == 2104, test_settings.years
|
|
383
|
+
assert test_settings.random is None, test_settings.random
|
|
384
|
+
assert len(test_settings.stuff) == 0, test_settings.stuff
|
|
385
|
+
assert len(test_settings.info) == 0, test_settings.info
|
|
386
|
+
|
|
387
|
+
write(
|
|
388
|
+
settings_path,
|
|
389
|
+
name=None,
|
|
390
|
+
names=["John", "Doe"],
|
|
391
|
+
age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
|
|
392
|
+
age=50,
|
|
393
|
+
alive=True,
|
|
394
|
+
years=(1973, 2073),
|
|
395
|
+
stuff=[],
|
|
396
|
+
info={},
|
|
397
|
+
)
|
|
398
|
+
try:
|
|
399
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
400
|
+
assert False, "Expected TypeError Exception"
|
|
401
|
+
except TypeError:
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
write(
|
|
405
|
+
settings_path,
|
|
406
|
+
name="John Doe",
|
|
407
|
+
names=[1, 2],
|
|
408
|
+
age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
|
|
409
|
+
age=50,
|
|
410
|
+
alive=True,
|
|
411
|
+
years=(1973, 2073),
|
|
412
|
+
stuff=[],
|
|
413
|
+
info={},
|
|
414
|
+
)
|
|
415
|
+
try:
|
|
416
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
417
|
+
assert False, "Expected TypeError Exception"
|
|
418
|
+
except TypeError:
|
|
419
|
+
pass
|
|
420
|
+
|
|
421
|
+
write(
|
|
422
|
+
settings_path,
|
|
423
|
+
name="John Doe",
|
|
424
|
+
names=None,
|
|
425
|
+
age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
|
|
426
|
+
age=50,
|
|
427
|
+
alive=True,
|
|
428
|
+
years=(1973, 2073),
|
|
429
|
+
stuff=[],
|
|
430
|
+
info={},
|
|
431
|
+
)
|
|
432
|
+
try:
|
|
433
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
434
|
+
assert False, "Expected TypeError Exception"
|
|
435
|
+
except TypeError:
|
|
436
|
+
pass
|
|
437
|
+
|
|
438
|
+
write(
|
|
439
|
+
settings_path,
|
|
440
|
+
name="John Doe",
|
|
441
|
+
names=["John", "Doe"],
|
|
442
|
+
age_names={"0": "Newborn", "50": "Middle age", "100": "Old"},
|
|
443
|
+
age=50,
|
|
444
|
+
alive=True,
|
|
445
|
+
years=(1973, 2073),
|
|
446
|
+
stuff=[],
|
|
447
|
+
info={},
|
|
448
|
+
)
|
|
449
|
+
try:
|
|
450
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
451
|
+
assert False, "Expected TypeError Exception"
|
|
452
|
+
except TypeError:
|
|
453
|
+
pass
|
|
454
|
+
|
|
455
|
+
write(
|
|
456
|
+
settings_path,
|
|
457
|
+
name="John Doe",
|
|
458
|
+
names=["John", "Doe"],
|
|
459
|
+
age_names=None,
|
|
460
|
+
age=50,
|
|
461
|
+
alive=True,
|
|
462
|
+
years=(1973, 2073),
|
|
463
|
+
stuff=[],
|
|
464
|
+
info={},
|
|
465
|
+
)
|
|
466
|
+
try:
|
|
467
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
468
|
+
assert False, "Expected TypeError Exception"
|
|
469
|
+
except TypeError:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
write(
|
|
473
|
+
settings_path,
|
|
474
|
+
name="John Doe",
|
|
475
|
+
names=["John", "Doe"],
|
|
476
|
+
age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
|
|
477
|
+
age=None,
|
|
478
|
+
alive=True,
|
|
479
|
+
years=(1973, 2073),
|
|
480
|
+
stuff=[],
|
|
481
|
+
info={},
|
|
482
|
+
)
|
|
483
|
+
try:
|
|
484
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
485
|
+
assert False, "Expected TypeError Exception"
|
|
486
|
+
except TypeError:
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
write(
|
|
490
|
+
settings_path,
|
|
491
|
+
name="John Doe",
|
|
492
|
+
names=["John", "Doe"],
|
|
493
|
+
age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
|
|
494
|
+
age=50,
|
|
495
|
+
alive=None,
|
|
496
|
+
years=(1973, 2073),
|
|
497
|
+
stuff=[],
|
|
498
|
+
info={},
|
|
499
|
+
)
|
|
500
|
+
try:
|
|
501
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
502
|
+
assert False, "Expected TypeError Exception"
|
|
503
|
+
except TypeError:
|
|
504
|
+
pass
|
|
505
|
+
|
|
506
|
+
write(
|
|
507
|
+
settings_path,
|
|
508
|
+
name="John Doe",
|
|
509
|
+
names=["John", "Doe"],
|
|
510
|
+
age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
|
|
511
|
+
stuff=[],
|
|
512
|
+
info={},
|
|
513
|
+
)
|
|
514
|
+
try:
|
|
515
|
+
test_settings = Settings(settings_path).parse(MockSettings)
|
|
516
|
+
assert False, "Expected KeyError Exception"
|
|
517
|
+
except KeyError:
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
try:
|
|
521
|
+
test_settings = Settings(settings_path).parse(Settings)
|
|
522
|
+
assert False, "Expected TypeError Exception"
|
|
523
|
+
except TypeError:
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
|
|
293
527
|
if __name__ == "__main__":
|
|
528
|
+
test_parse()
|
|
294
529
|
test_secret()
|
|
295
530
|
test_environ_values()
|
|
296
531
|
test_cli_env_in_yaml()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|