devopsdriver 0.1.52__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.52 → devopsdriver-0.1.53}/PKG-INFO +7 -7
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/README.md +2 -2
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/__init__.py +1 -2
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/__init__.py +1 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/azureobject.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/client.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/clients.py +0 -2
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/client.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/log.py +1 -3
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/pipeline.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/run.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/timestamp.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/client.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/wiql.py +10 -2
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/dataobject.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/github/client.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/manage_settings.py +3 -4
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/sendmail.py +0 -2
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/settings.py +99 -8
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/template.py +0 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/PKG-INFO +7 -7
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/requires.txt +4 -4
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/pyproject.toml +4 -4
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_workitem_wiql.py +43 -1
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_settings.py +242 -7
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/LICENSE +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/__init__.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/build.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/__init__.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/__init__.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/github/__init__.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/templates/manage_settings.txt.mako +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/SOURCES.txt +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/dependency_links.txt +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/entry_points.txt +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/top_level.txt +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/setup.cfg +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_azureobject.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_build.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_build_client.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_clients.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_pipeline.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_pipeline_client.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_pipeline_run.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_timestamp.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_workitem_client.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_dataobject.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_manage_settings.py +0 -0
- {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_sendmail.py +0 -0
- {devopsdriver-0.1.52 → 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
|
|
@@ -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
|
|
@@ -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,6 +144,13 @@ 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
|
|
|
@@ -294,7 +300,9 @@ class Wiql:
|
|
|
294
300
|
return self
|
|
295
301
|
|
|
296
302
|
def mode(self, results_mode: str):
|
|
297
|
-
"""Sets the mode for link queries
|
|
303
|
+
"""Sets the mode for link queries
|
|
304
|
+
results_mode may be one of: MustContain, MayContain, DoesNotContain, Recursive, None
|
|
305
|
+
"""
|
|
298
306
|
self.mode_type = results_mode
|
|
299
307
|
return self
|
|
300
308
|
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -225,6 +285,37 @@ class Settings:
|
|
|
225
285
|
"""
|
|
226
286
|
return self.__bypass(key, name, self.environ)
|
|
227
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
|
+
|
|
228
319
|
@staticmethod
|
|
229
320
|
def __patch_instance(key: str) -> str:
|
|
230
321
|
for env_key, value in ENVIRON.items():
|
|
@@ -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",
|
|
@@ -6,7 +6,7 @@ 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
|
|
|
@@ -78,7 +78,49 @@ def test_in_and_not_in() -> None:
|
|
|
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
|