devopsdriver 0.1.34__tar.gz → 0.1.35__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/PKG-INFO +8 -4
  2. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/README.md +5 -3
  3. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/devopsdriver/__init__.py +3 -1
  4. devopsdriver-0.1.35/devopsdriver/azure/__init__.py +10 -0
  5. devopsdriver-0.1.35/devopsdriver/azure/clients.py +53 -0
  6. devopsdriver-0.1.35/devopsdriver/azure/workitem/__init__.py +4 -0
  7. devopsdriver-0.1.35/devopsdriver/azure/workitem/client.py +88 -0
  8. devopsdriver-0.1.35/devopsdriver/azure/workitem/wiql.py +282 -0
  9. devopsdriver-0.1.35/devopsdriver/azure/workitem/workitem.py +53 -0
  10. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/devopsdriver/settings.py +9 -2
  11. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/devopsdriver.egg-info/PKG-INFO +8 -4
  12. devopsdriver-0.1.35/devopsdriver.egg-info/SOURCES.txt +21 -0
  13. devopsdriver-0.1.35/devopsdriver.egg-info/requires.txt +4 -0
  14. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/pyproject.toml +2 -0
  15. devopsdriver-0.1.35/tests/test_azure_clients.py +144 -0
  16. devopsdriver-0.1.35/tests/test_azure_workitem.py +95 -0
  17. devopsdriver-0.1.35/tests/test_azure_workitem_client.py +61 -0
  18. devopsdriver-0.1.35/tests/test_azure_workitem_wiql.py +71 -0
  19. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/tests/test_settings.py +24 -59
  20. devopsdriver-0.1.34/devopsdriver.egg-info/SOURCES.txt +0 -11
  21. devopsdriver-0.1.34/devopsdriver.egg-info/requires.txt +0 -2
  22. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/LICENSE +0 -0
  23. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/devopsdriver.egg-info/dependency_links.txt +0 -0
  24. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/devopsdriver.egg-info/top_level.txt +0 -0
  25. {devopsdriver-0.1.34 → devopsdriver-0.1.35}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devopsdriver
3
- Version: 0.1.34
3
+ Version: 0.1.35
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.
@@ -48,10 +48,15 @@ Description-Content-Type: text/markdown
48
48
  License-File: LICENSE
49
49
  Requires-Dist: PyYAML==6.0.1
50
50
  Requires-Dist: keyring==25.0.0
51
+ Requires-Dist: setuptools==69.0.2
52
+ Requires-Dist: azure-devops==7.1.0b4
51
53
 
52
54
  ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
53
- [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.34&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.34/)
55
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.35&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.35/)
54
56
  [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
57
+ [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
58
+ [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
59
+
55
60
  [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
56
61
  [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
57
62
  [![GitHub top language](https://img.shields.io/github/languages/top/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
@@ -61,8 +66,6 @@ Requires-Dist: keyring==25.0.0
61
66
  [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=99%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
62
67
  [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
63
68
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
64
- [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
65
- [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
66
69
 
67
70
  [![follow sheild](https://img.shields.io/github/followers/marcpage?label=Follow&style=social)](https://github.com/marcpage?tab=followers)
68
71
  [![watch sheild](https://img.shields.io/github/watchers/marcpage/devops-driver?label=Watch&style=social)](https://github.com/marcpage/devops-driver/watchers)
@@ -70,3 +73,4 @@ Requires-Dist: keyring==25.0.0
70
73
  # devops-driver
71
74
 
72
75
  Devops-driver is a set of tools to help streamline developer's experience. It is a collection of tools to help gain insights into various processes.
76
+
@@ -1,6 +1,9 @@
1
1
  ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
2
- [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.34&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.34/)
2
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.35&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.35/)
3
3
  [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
4
+ [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
5
+ [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
6
+
4
7
  [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
5
8
  [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
6
9
  [![GitHub top language](https://img.shields.io/github/languages/top/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
@@ -10,8 +13,6 @@
10
13
  [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=99%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
11
14
  [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
12
15
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
13
- [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
14
- [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
15
16
 
16
17
  [![follow sheild](https://img.shields.io/github/followers/marcpage?label=Follow&style=social)](https://github.com/marcpage?tab=followers)
17
18
  [![watch sheild](https://img.shields.io/github/watchers/marcpage/devops-driver?label=Watch&style=social)](https://github.com/marcpage/devops-driver/watchers)
@@ -19,3 +20,4 @@
19
20
  # devops-driver
20
21
 
21
22
  Devops-driver is a set of tools to help streamline developer's experience. It is a collection of tools to help gain insights into various processes.
23
+
@@ -1,5 +1,7 @@
1
1
  """ DevOps tools """
2
2
 
3
- __version__ = "0.1.34"
3
+ from .azure import Azure
4
+
5
+ __version__ = "0.1.35"
4
6
  __author__ = "Marc Page"
5
7
  __credits__ = ""
@@ -0,0 +1,10 @@
1
+ """ initialize azure module """
2
+
3
+ # re export symbols for easier use
4
+ from .clients import Azure
5
+
6
+ from .workitem import WorkItem
7
+ from .workitem.wiql import Wiql, Value, Field
8
+ from .workitem.wiql import Ascending, Descending, And, Or
9
+ from .workitem.wiql import Equal, NotEqual, LessThanOrEqual, GreaterThanOrEqual
10
+ from .workitem.wiql import IsEmpty, IsNotEmpty, LessThan, GreaterThan
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ """ Establish a connection to Azure Devops
5
+
6
+ API Documented here:
7
+ https://github.com/microsoft/azure-devops-python-api
8
+ """
9
+
10
+ from azure.devops.connection import Connection as AzureConnection
11
+ from msrest.authentication import BasicAuthentication as MSBasicAuthentication
12
+
13
+ from devopsdriver.settings import Settings
14
+ from devopsdriver.azure.workitem.client import Client as WIClient
15
+
16
+ CONNECTION = AzureConnection
17
+ AUTHENTICATION = MSBasicAuthentication
18
+
19
+
20
+ class Azure: # pylint: disable=too-few-public-methods
21
+ """A connection to Azure clients"""
22
+
23
+ SUPPORTED_CLIENTS = {"workitem", "pipeline"}
24
+
25
+ def __init__(
26
+ self, settings: Settings = None, token: str = None, url: str = None, **clients
27
+ ):
28
+ unsupported_clients = set(clients) - Azure.SUPPORTED_CLIENTS
29
+ assert not unsupported_clients, f"{unsupported_clients} not supported"
30
+ settings = (
31
+ Settings(__file__).key("secrets")
32
+ if settings is None and token is None and url is None
33
+ else settings
34
+ )
35
+ assert (token or "azure.token" in settings) and (
36
+ url or "azure.url" in settings
37
+ ), "azure.token and azure.url not found in:\n" + "\n".join(
38
+ settings.search_files
39
+ )
40
+ url = settings["azure.url"] if url is None else url
41
+ token = settings["azure.token"] if token is None else token
42
+ self.connection = CONNECTION(base_url=url, creds=AUTHENTICATION("", token))
43
+ client_calls = {
44
+ "workitem": self.connection.clients_v7_1.get_work_item_tracking_client
45
+ }
46
+ self.workitem = WIClient(Azure.__client("workitem", clients, client_calls))
47
+
48
+ @staticmethod
49
+ def __client(name: str, clients: dict, calls: dict) -> any:
50
+ if clients and not clients.get(name, False):
51
+ return None
52
+
53
+ return calls[name]()
@@ -0,0 +1,4 @@
1
+ """ init workitem module """
2
+
3
+ # Re-export symbols to make things make sense
4
+ from .workitem import WorkItem
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """ Azure WorkItem Client """
4
+
5
+ from azure.devops.v7_1.work_item_tracking.models import Wiql as AzureWiql
6
+ from azure.devops.v7_1.work_item_tracking.models import WorkItem as AzureWorkItem
7
+ from azure.devops.v7_1.work_item_tracking.models import TeamContext
8
+ from azure.devops.v7_1.work_item_tracking.models import WorkItemQueryResult
9
+ from devopsdriver.azure.workitem import WorkItem
10
+ from devopsdriver.azure.workitem.wiql import Wiql
11
+
12
+
13
+ class Client:
14
+ """Wraps work item client"""
15
+
16
+ def __init__(self, client):
17
+ self.client = client
18
+
19
+ def query(
20
+ self,
21
+ wiql: Wiql | str,
22
+ team_context: TeamContext = None,
23
+ time_precision: bool = None,
24
+ top: int = None,
25
+ ) -> WorkItemQueryResult:
26
+ """Perform a wiql query
27
+
28
+ Args:
29
+ wiql (Wiql | str): The query
30
+ team_context (TeamContext, optional): context object. Defaults to None.
31
+ time_precision (bool, optional): True for precision time. Defaults to None.
32
+ top (int, optional): Count of items to get. Defaults to None.
33
+
34
+ Returns:
35
+ WorkItemQueryResult: The results
36
+ """
37
+ return self.client.query_by_wiql(
38
+ AzureWiql(query=str(wiql)),
39
+ team_context=team_context,
40
+ time_precision=time_precision,
41
+ top=top,
42
+ )
43
+
44
+ def history( # pylint: disable=too-many-arguments
45
+ self,
46
+ wi_id: int,
47
+ project: str = None,
48
+ top: int = None,
49
+ skip: int = None,
50
+ expand: str = None,
51
+ ) -> list[AzureWorkItem]:
52
+ """Simple wrapper around get_revisions"""
53
+ return self.client.get_revisions(wi_id, project, top, skip, expand)
54
+
55
+ def find_ids(self, wiql: Wiql | str, top: int = None) -> list[int]:
56
+ """Given a query, find the work item ids
57
+
58
+ Args:
59
+ wiql (Wiql | str): The query
60
+ top (int, optional): The number of results to return. Defaults to None.
61
+
62
+ Returns:
63
+ list: List of item ids
64
+ """
65
+ if isinstance(wiql, Wiql):
66
+ wiql.select("Id")
67
+
68
+ found = self.query(wiql, top=top)
69
+ # top-level items: as_of, columns, query_results_type, query_type, work_items
70
+ # work_items fields: id, url
71
+ # query_results_type: workItem
72
+ # query_type: flat
73
+ # columns: list of name, reference_name, url
74
+ return [i.id for i in found.work_items]
75
+
76
+ def find(self, wiql: Wiql | str, top: int = None) -> list[list[WorkItem]]:
77
+ """Gets the full history of items found in a WIQL search
78
+
79
+ Args:
80
+ wiql (Wiql | str): The query
81
+ top (int, optional): The number of work items to return. Defaults to None.
82
+
83
+ Returns:
84
+ list[list[WorkItem]]: List of work items, each is a history of work items
85
+ """
86
+ return [
87
+ [WorkItem(e) for e in self.history(i)] for i in self.find_ids(wiql, top)
88
+ ]
@@ -0,0 +1,282 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """ Builds a WIQL query
4
+ https://learn.microsoft.com/en-us/azure/devops/boards/queries/wiql-syntax?view=azure-devops
5
+
6
+ SELECT
7
+ [System.Id],
8
+ [System.AssignedTo],
9
+ [System.State],
10
+ [System.Title],
11
+ [System.Tags]
12
+ FROM workitems
13
+ WHERE
14
+ [System.TeamProject] = 'Design Agile'
15
+ AND [System.WorkItemType] = 'User Story'
16
+ AND [System.State] = 'Active'
17
+ ORDER BY [System.ChangedDate] DESC
18
+ ASOF '02-11-2020'
19
+ """
20
+
21
+
22
+ from datetime import datetime, date
23
+
24
+
25
+ class Field: # pylint: disable=too-few-public-methods
26
+ """column name"""
27
+
28
+ SYSTEM = {
29
+ "Id",
30
+ "AssignTo",
31
+ "State",
32
+ "Title",
33
+ "Tags",
34
+ "TeamProject",
35
+ "WorkItemType",
36
+ "ChangedDate",
37
+ "CreatedDate",
38
+ }
39
+ COMMON = {"Priority"}
40
+
41
+ def __init__(self, value: str):
42
+ self.value = value
43
+
44
+ def __str__(self) -> str:
45
+ if self.value in Field.SYSTEM:
46
+ prefix = "System."
47
+
48
+ elif self.value in Field.COMMON:
49
+ prefix = "Microsoft.VSTS.Common."
50
+
51
+ else:
52
+ prefix = ""
53
+
54
+ return f"[{prefix}{self.value}]"
55
+
56
+
57
+ class OrderBy: # pylint: disable=too-few-public-methods
58
+ """Order by field"""
59
+
60
+ def __init__(self, field: Field | str, order: str = "DESC"):
61
+ self.field = field if isinstance(field, Field) else Field(field)
62
+ self.order = order
63
+
64
+ def __str__(self) -> str:
65
+ return f"{self.field} {self.order}"
66
+
67
+
68
+ class Ascending(OrderBy): # pylint: disable=too-few-public-methods
69
+ """Order by field ascending"""
70
+
71
+ def __init__(self, field: Field | str):
72
+ super().__init__(field, "ASC")
73
+
74
+
75
+ class Descending(OrderBy): # pylint: disable=too-few-public-methods
76
+ """Order by field descending"""
77
+
78
+ def __init__(self, field: Field | str):
79
+ super().__init__(field, "DESC")
80
+
81
+
82
+ class Value: # pylint: disable=too-few-public-methods
83
+ """A constant value"""
84
+
85
+ def __init__(self, value: date | datetime | int | float | str):
86
+ self.value = value
87
+
88
+ def __str__(self) -> str:
89
+ if isinstance(self.value, datetime):
90
+ return self.value.strftime("%Y-%m-%d %H:%M:%S")
91
+
92
+ if isinstance(self.value, date):
93
+ return self.value.strftime("%Y-%m-%d")
94
+
95
+ if isinstance(self.value, int):
96
+ return str(self.value)
97
+
98
+ if isinstance(self.value, float):
99
+ return f"{self.value:0.3f}"
100
+
101
+ if isinstance(self.value, str):
102
+ return f'"{self.value}"' # TODO: escape value # pylint: disable=fixme
103
+
104
+ raise AssertionError("Unknown type")
105
+
106
+
107
+ class IsEmpty: # pylint: disable=too-few-public-methods
108
+ """Compare a field to a value"""
109
+
110
+ def __init__(self, field: Field | str):
111
+ self.field = field if isinstance(field, Field) else Field(field)
112
+
113
+ def __str__(self) -> str:
114
+ return f"{str(self.field)} IS EMPTY"
115
+
116
+
117
+ class IsNotEmpty: # pylint: disable=too-few-public-methods
118
+ """Compare a field to a value"""
119
+
120
+ def __init__(self, field: Field | str):
121
+ self.field = field if isinstance(field, Field) else Field(field)
122
+
123
+ def __str__(self) -> str:
124
+ return f"{str(self.field)} IS NOT EMPTY"
125
+
126
+
127
+ class Compare: # pylint: disable=too-few-public-methods
128
+ """Compare a field to a value"""
129
+
130
+ def __init__(
131
+ self,
132
+ field: Field | str,
133
+ value: Value | str | date | datetime | int | float,
134
+ operator: str,
135
+ ):
136
+ self.left = field if isinstance(field, Field) else Field(field)
137
+ self.right = value if isinstance(value, Value) else Value(value)
138
+ self.operator = operator
139
+
140
+ def __str__(self) -> str:
141
+ return f"{str(self.left)} {self.operator} {str(self.right)}"
142
+
143
+
144
+ class Equal(Compare): # pylint: disable=too-few-public-methods
145
+ """checks for equality"""
146
+
147
+ def __init__(
148
+ self, field: Field | str, value: Value | str | date | datetime | int | float
149
+ ):
150
+ super().__init__(field, value, "=")
151
+
152
+
153
+ class NotEqual(Compare): # pylint: disable=too-few-public-methods
154
+ """checks for equality"""
155
+
156
+ def __init__(
157
+ self, field: Field | str, value: Value | str | date | datetime | int | float
158
+ ):
159
+ super().__init__(field, value, "<>")
160
+
161
+
162
+ class LessThan(Compare): # pylint: disable=too-few-public-methods
163
+ """checks for lass than"""
164
+
165
+ def __init__(
166
+ self, field: Field | str, value: Value | str | date | datetime | int | float
167
+ ):
168
+ super().__init__(field, value, "<")
169
+
170
+
171
+ class GreaterThan(Compare): # pylint: disable=too-few-public-methods
172
+ """checks for greater than"""
173
+
174
+ def __init__(
175
+ self, field: Field | str, value: Value | str | date | datetime | int | float
176
+ ):
177
+ super().__init__(field, value, ">")
178
+
179
+
180
+ class LessThanOrEqual(Compare): # pylint: disable=too-few-public-methods
181
+ """checks for less than or equal"""
182
+
183
+ def __init__(
184
+ self, field: Field | str, value: Value | str | date | datetime | int | float
185
+ ):
186
+ super().__init__(field, value, "<=")
187
+
188
+
189
+ class GreaterThanOrEqual(Compare): # pylint: disable=too-few-public-methods
190
+ """checks for greater than or equal"""
191
+
192
+ def __init__(
193
+ self, field: Field | str, value: Value | str | date | datetime | int | float
194
+ ):
195
+ super().__init__(field, value, ">=")
196
+
197
+
198
+ class Expression: # pylint: disable=too-few-public-methods
199
+ """Join several compares"""
200
+
201
+ def __init__(self, operator: str, *compares: list[Compare]):
202
+ self.operator = operator
203
+ self.expressions = compares
204
+
205
+ def __str__(self) -> str:
206
+ return f" {self.operator} ".join(str(e) for e in self.expressions)
207
+
208
+
209
+ class And(Expression): # pylint: disable=too-few-public-methods
210
+ """Join compares via AND"""
211
+
212
+ def __init__(self, *compares: list[Compare | Expression]):
213
+ super().__init__("AND", *compares)
214
+
215
+
216
+ class Or(Expression): # pylint: disable=too-few-public-methods
217
+ """join compares via OR"""
218
+
219
+ def __init__(self, *compares: list[Compare | Expression]):
220
+ super().__init__("OR", *compares)
221
+
222
+
223
+ class Wiql:
224
+ """Build a WIQL query"""
225
+
226
+ def __init__(self):
227
+ self.selected = [Field("Id")]
228
+ self.search = None
229
+ self.order = []
230
+ self.snapshot = None
231
+
232
+ def select(self, *fields: list[Field | str]):
233
+ """The fields to select
234
+
235
+ Returns:
236
+ Builder: Returns self for chaining
237
+ """
238
+ self.selected = [f if isinstance(f, Field) else Field(f) for f in fields]
239
+ return self
240
+
241
+ def where(self, expression: Compare | And | Or):
242
+ """Search criteria
243
+
244
+ Args:
245
+ expression (Expression|Compare): An expression of what to search for.
246
+
247
+ Returns:
248
+ Builder: self for chaining calls
249
+ """
250
+ self.search = expression
251
+ return self
252
+
253
+ def order_by(self, *orders):
254
+ """Set the fields to order the results by
255
+
256
+ Returns:
257
+ Builder: Returns self for chaining
258
+ """
259
+ self.order = orders
260
+ return self
261
+
262
+ def asof(self, stamp: Value | date | datetime | str):
263
+ """Set the view of the data
264
+
265
+ Args:
266
+ stamp (Value): The date or datetime in a Value
267
+
268
+ Returns:
269
+ Builder: self for chainingd
270
+ """
271
+ assert isinstance(stamp, (Value, date, datetime, str)), stamp
272
+ self.snapshot = stamp if isinstance(stamp, (Value, str)) else Value(stamp)
273
+ return self
274
+
275
+ def __str__(self) -> str:
276
+ select = ", ".join(str(s) for s in self.selected)
277
+ where = f" WHERE {self.search}" if self.search else ""
278
+ order = (
279
+ f" ORDER BY {', '.join(str(o) for o in self.order)}" if self.order else ""
280
+ )
281
+ asof = f" ASOF {str(self.snapshot)}" if self.snapshot else ""
282
+ return f"SELECT {select} FROM workitems{where}{order}{asof}"
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """ An Azure Devops WorkItem """
4
+
5
+ from typing import Any
6
+ from azure.devops.v7_1.work_item_tracking.models import WorkItem as AzureWorkItem
7
+
8
+
9
+ class WorkItem: # pylint: disable=too-few-public-methods
10
+ """Azure WorkItem"""
11
+
12
+ def __init__(self, work_item: AzureWorkItem):
13
+ self.raw = work_item
14
+
15
+ @staticmethod
16
+ def __matches_field(name: str, field: str) -> bool:
17
+ name = name.lower()
18
+ field = field.lower()
19
+
20
+ if name == field:
21
+ return True
22
+
23
+ if name == field.replace(".", "_"):
24
+ return True
25
+
26
+ if name == field.split(".")[-1]:
27
+ return True
28
+
29
+ return False
30
+
31
+ @staticmethod
32
+ def _parse_field(name: str, data: dict) -> any:
33
+ assert name and data
34
+ found = [f for f in data if WorkItem.__matches_field(name, f)]
35
+
36
+ if len(found) == 1:
37
+ return (
38
+ WorkItem._Dict(data[found[0]])
39
+ if isinstance(data[found[0]], dict)
40
+ else data[found[0]]
41
+ )
42
+
43
+ if "fields" in data:
44
+ return WorkItem._parse_field(name, data["fields"])
45
+
46
+ raise AttributeError(f"'WorkItem' object has no attribute '{name}'")
47
+
48
+ class _Dict(dict):
49
+ def __getattr__(self, name: str) -> Any:
50
+ return WorkItem._parse_field(name, self)
51
+
52
+ def __getattr__(self, name: str) -> Any:
53
+ return WorkItem._parse_field(name, self.raw.as_dict())
@@ -304,6 +304,9 @@ class Settings:
304
304
  """
305
305
  return self.__lookup(key, check=True)
306
306
 
307
+ def __contains__(self, key: str) -> bool:
308
+ return self.has(key)
309
+
307
310
  def __getitem__(self, key: str) -> any:
308
311
  if not self.has(key):
309
312
  raise KeyError(key)
@@ -362,15 +365,19 @@ def main() -> None:
362
365
 
363
366
  args = list(ARGV[1:])
364
367
 
365
- if "--set_secrets" in args:
366
- args.remove("--set_secrets")
368
+ if "--secrets" in args:
369
+ args.remove("--secrets")
367
370
 
368
371
  for secret, key in settings.secrets.items():
372
+ PRINT(f"secret: {secret} key: {key}")
373
+
369
374
  if not settings.has(secret):
370
375
  value = GET_PASS(f"{secret} ({key}): ")
371
376
 
372
377
  if value:
373
378
  SET_PASSWORD(*Settings.split_key(key), value)
379
+ else:
380
+ PRINT("\tValue set")
374
381
 
375
382
  for arg in args:
376
383
  PRINT(settings.get(arg))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devopsdriver
3
- Version: 0.1.34
3
+ Version: 0.1.35
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.
@@ -48,10 +48,15 @@ Description-Content-Type: text/markdown
48
48
  License-File: LICENSE
49
49
  Requires-Dist: PyYAML==6.0.1
50
50
  Requires-Dist: keyring==25.0.0
51
+ Requires-Dist: setuptools==69.0.2
52
+ Requires-Dist: azure-devops==7.1.0b4
51
53
 
52
54
  ![status sheild](https://img.shields.io/static/v1?label=status&message=starting...&color=inactive&style=plastic)
53
- [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.34&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.34/)
55
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.35&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.35/)
54
56
  [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
57
+ [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
58
+ [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
59
+
55
60
  [![commit sheild](https://img.shields.io/github/last-commit/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
56
61
  [![activity sheild](https://img.shields.io/github/commit-activity/m/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/commits)
57
62
  [![GitHub top language](https://img.shields.io/github/languages/top/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
@@ -61,8 +66,6 @@ Requires-Dist: keyring==25.0.0
61
66
  [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=99%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
62
67
  [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
63
68
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
64
- [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
65
- [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
66
69
 
67
70
  [![follow sheild](https://img.shields.io/github/followers/marcpage?label=Follow&style=social)](https://github.com/marcpage?tab=followers)
68
71
  [![watch sheild](https://img.shields.io/github/watchers/marcpage/devops-driver?label=Watch&style=social)](https://github.com/marcpage/devops-driver/watchers)
@@ -70,3 +73,4 @@ Requires-Dist: keyring==25.0.0
70
73
  # devops-driver
71
74
 
72
75
  Devops-driver is a set of tools to help streamline developer's experience. It is a collection of tools to help gain insights into various processes.
76
+
@@ -0,0 +1,21 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ devopsdriver/__init__.py
5
+ devopsdriver/settings.py
6
+ devopsdriver.egg-info/PKG-INFO
7
+ devopsdriver.egg-info/SOURCES.txt
8
+ devopsdriver.egg-info/dependency_links.txt
9
+ devopsdriver.egg-info/requires.txt
10
+ devopsdriver.egg-info/top_level.txt
11
+ devopsdriver/azure/__init__.py
12
+ devopsdriver/azure/clients.py
13
+ devopsdriver/azure/workitem/__init__.py
14
+ devopsdriver/azure/workitem/client.py
15
+ devopsdriver/azure/workitem/wiql.py
16
+ devopsdriver/azure/workitem/workitem.py
17
+ tests/test_azure_clients.py
18
+ tests/test_azure_workitem.py
19
+ tests/test_azure_workitem_client.py
20
+ tests/test_azure_workitem_wiql.py
21
+ tests/test_settings.py
@@ -0,0 +1,4 @@
1
+ PyYAML==6.0.1
2
+ keyring==25.0.0
3
+ setuptools==69.0.2
4
+ azure-devops==7.1.0b4
@@ -8,6 +8,8 @@ requires-python = ">= 3.10"
8
8
  dependencies = [
9
9
  "PyYAML==6.0.1",
10
10
  "keyring==25.0.0",
11
+ "setuptools==69.0.2", # neded for azure-devops to use 7.1 API
12
+ "azure-devops==7.1.0b4",
11
13
  ]
12
14
  keywords = ["azure", "devops", "jira", "confluence", "email", "pipelines", "tools"]
13
15
  classifiers=[
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ """ test azure client logic """
5
+
6
+
7
+ from tempfile import TemporaryDirectory
8
+ from types import SimpleNamespace
9
+ from os.path import join
10
+
11
+ from helpers import setup_settings, write
12
+
13
+ from devopsdriver import Azure
14
+ from devopsdriver.azure import clients
15
+
16
+
17
+ class MockConnection: # pylint: disable=too-few-public-methods
18
+ """Fakes an Azure connection"""
19
+
20
+ def __init__(self, base_url: str, creds: SimpleNamespace):
21
+ self.base_url = base_url
22
+ self.creds = creds
23
+
24
+ class Clients71: # pylint: disable=too-few-public-methods
25
+ """Fakes a 7.1 clients factory"""
26
+
27
+ def get_work_item_tracking_client(self) -> str:
28
+ """fakes getting work item client"""
29
+ return "work_item_tracking_client"
30
+
31
+ self.clients_v7_1 = Clients71()
32
+
33
+
34
+ def test_basic() -> None:
35
+ """test the basic calling"""
36
+ clients.CONNECTION = MockConnection
37
+ clients.AUTHENTICATION = lambda a, b: SimpleNamespace(empty=a, token=b)
38
+ azure = Azure(None, "token", "https://url.com/project")
39
+ assert (
40
+ azure.connection.base_url == "https://url.com/project"
41
+ ), azure.connection.base_url
42
+ assert azure.connection.creds.token == "token", azure.connection.creds.token
43
+ assert azure.connection.creds.empty == "", azure.connection.creds.empty
44
+ assert azure.workitem.client == "work_item_tracking_client"
45
+
46
+
47
+ def test_settings() -> None:
48
+ """test the basic calling"""
49
+ clients.CONNECTION = MockConnection
50
+ clients.AUTHENTICATION = lambda a, b: SimpleNamespace(empty=a, token=b)
51
+ azure = Azure({"azure.token": "token", "azure.url": "https://url.com/project"})
52
+ assert (
53
+ azure.connection.base_url == "https://url.com/project"
54
+ ), azure.connection.base_url
55
+ assert azure.connection.creds.token == "token", azure.connection.creds.token
56
+ assert azure.connection.creds.empty == "", azure.connection.creds.empty
57
+ assert azure.workitem.client == "work_item_tracking_client"
58
+
59
+
60
+ def test_mixed_settings() -> None:
61
+ """test the basic calling"""
62
+ clients.CONNECTION = MockConnection
63
+ clients.AUTHENTICATION = lambda a, b: SimpleNamespace(empty=a, token=b)
64
+ azure = Azure(
65
+ {"azure.token": "token", "azure.url": "https://url.com/project"},
66
+ token="fake token",
67
+ )
68
+ assert (
69
+ azure.connection.base_url == "https://url.com/project"
70
+ ), azure.connection.base_url
71
+ assert azure.connection.creds.token == "fake token", azure.connection.creds.token
72
+ assert azure.connection.creds.empty == "", azure.connection.creds.empty
73
+ assert azure.workitem.client == "work_item_tracking_client"
74
+
75
+ azure = Azure(
76
+ {"azure.token": "token", "azure.url": "https://url.com/project"},
77
+ url="https://fake.com/project",
78
+ )
79
+ assert (
80
+ azure.connection.base_url == "https://fake.com/project"
81
+ ), azure.connection.base_url
82
+ assert azure.connection.creds.token == "token", azure.connection.creds.token
83
+ assert azure.connection.creds.empty == "", azure.connection.creds.empty
84
+ assert azure.workitem.client == "work_item_tracking_client"
85
+
86
+ azure = Azure(
87
+ {"azure.token": "token", "azure.url": "https://url.com/project"},
88
+ token="fake token",
89
+ url="https://fake.com/project",
90
+ )
91
+ assert (
92
+ azure.connection.base_url == "https://fake.com/project"
93
+ ), azure.connection.base_url
94
+ assert azure.connection.creds.token == "fake token", azure.connection.creds.token
95
+ assert azure.connection.creds.empty == "", azure.connection.creds.empty
96
+ assert azure.workitem.client == "work_item_tracking_client"
97
+
98
+
99
+ def test_load_settings() -> None:
100
+ """test the basic calling"""
101
+ with TemporaryDirectory() as working_dir:
102
+ base_dir = join(working_dir, "base")
103
+ setup_settings(
104
+ os="Linux",
105
+ shared="test",
106
+ Linux=join(base_dir, "Linux"),
107
+ Darwin=join(base_dir, "macOS"),
108
+ Windows=join(base_dir, "Windows"),
109
+ )
110
+ write(
111
+ join(base_dir, "Linux", "test.yml"),
112
+ azure={"token": "token", "url": "https://url.com/project"},
113
+ )
114
+ clients.CONNECTION = MockConnection
115
+ clients.AUTHENTICATION = lambda a, b: SimpleNamespace(empty=a, token=b)
116
+
117
+ azure = Azure()
118
+ assert (
119
+ azure.connection.base_url == "https://url.com/project"
120
+ ), azure.connection.base_url
121
+ assert azure.connection.creds.token == "token", azure.connection.creds.token
122
+ assert azure.connection.creds.empty == "", azure.connection.creds.empty
123
+ assert azure.workitem.client == "work_item_tracking_client"
124
+
125
+
126
+ def test_not_all_clients() -> None:
127
+ """test the basic calling"""
128
+ clients.CONNECTION = MockConnection
129
+ clients.AUTHENTICATION = lambda a, b: SimpleNamespace(empty=a, token=b)
130
+ azure = Azure(None, "token", "https://url.com/project", pipeline=True)
131
+ assert (
132
+ azure.connection.base_url == "https://url.com/project"
133
+ ), azure.connection.base_url
134
+ assert azure.connection.creds.token == "token", azure.connection.creds.token
135
+ assert azure.connection.creds.empty == "", azure.connection.creds.empty
136
+ assert azure.workitem.client is None
137
+
138
+
139
+ if __name__ == "__main__":
140
+ test_not_all_clients()
141
+ test_load_settings()
142
+ test_mixed_settings()
143
+ test_settings()
144
+ test_basic()
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """ Test work item """
4
+
5
+ from devopsdriver.azure import WorkItem
6
+
7
+
8
+ class MockAzureWorkItem:
9
+ """mock out work item"""
10
+
11
+ def as_dict(self):
12
+ """mock out as_dict"""
13
+ return {
14
+ "id": 5,
15
+ "rev": 1,
16
+ "url": "https://dev.azure.com/MyOrg/faf4b2ab-a8b4-4ab8-bca8-6f1f63fe6a91/"
17
+ + "_apis/wit/workItems/5/revisions/1",
18
+ "fields": {
19
+ "System.WorkItemType": "User Story",
20
+ "System.State": "New",
21
+ "System.Reason": "New",
22
+ "System.CreatedDate": "2023-11-16T03:12:32.94Z",
23
+ "System.CreatedBy": {
24
+ "displayName": "Edna Johnson",
25
+ "url": "https://spsprodcus5.vssps.visualstudio.com/"
26
+ + "A3eb27a26-75f2-40f9-87dc-cc10e8e565e4/_apis/Identities"
27
+ + "/45fcf770-0670-69d4-8e48-3ae6e0bf9b5c",
28
+ "_links": {
29
+ "avatar": {
30
+ "href": "https://dev.azure.com/MyOrg/_apis/GraphProfile/"
31
+ + "MemberAvatars/aad.NDVmY2Y3NzAtMDY3MC03OWQ0LThlNDgtM2FlNmUwYmY5YjVj"
32
+ }
33
+ },
34
+ "id": "45fcf770-0670-69d4-8e48-3ae6e0bf9b5c",
35
+ "uniqueName": "edna@company.com",
36
+ "imageUrl": "https://dev.azure.com/MyOrg/_apis/GraphProfile/"
37
+ + "MemberAvatars/aad.NDVmY2Y3NzAtMDY3MC03OWQ0LThlNDgtM2FlNmUwYmY5YjVj",
38
+ "inactive": True,
39
+ "descriptor": "aad.NDVmY2Y3NzAtMDY3MC03OWQ0LThlNDgtM2FlNmUwYmY5YjVj",
40
+ },
41
+ "System.ChangedDate": "2023-11-16T03:12:32.94Z",
42
+ "System.ChangedBy": {
43
+ "displayName": "Edna Johnson",
44
+ "url": "https://spsprodcus5.vssps.visualstudio.com/"
45
+ + "A3eb27a26-75f2-40f9-87dc-cc10e8e565e4/_apis/Identities/"
46
+ + "45fcf770-0670-69d4-8e48-3ae6e0bf9b5c",
47
+ "_links": {
48
+ "avatar": {
49
+ "href": "https://dev.azure.com/MyOrg/_apis/GraphProfile/"
50
+ + "MemberAvatars/aad.NDVmY2Y3NzAtMDY3MC03OWQ0LThlNDgtM2FlNmUwYmY5YjVj"
51
+ }
52
+ },
53
+ "id": "45fcf770-0670-69d4-8e48-3ae6e0bf9b5c",
54
+ "uniqueName": "edna@company.com",
55
+ "imageUrl": "https://dev.azure.com/MyOrg/_apis/GraphProfile/"
56
+ + "MemberAvatars/aad.NDVmY2Y3NzAtMDY3MC03OWQ0LThlNDgtM2FlNmUwYmY5YjVj",
57
+ "inactive": True,
58
+ "descriptor": "aad.NDVmY2Y3NzAtMDY3MC03OWQ0LThlNDgtM2FlNmUwYmY5YjVj",
59
+ },
60
+ "System.CommentCount": 0,
61
+ "System.TeamProject": "Creative",
62
+ "System.AreaPath": "Creative",
63
+ "System.IterationPath": "Creative\\November 2 2023",
64
+ "System.Title": "test",
65
+ "Microsoft.VSTS.Common.Priority": 2,
66
+ "Microsoft.VSTS.Common.ValueArea": "Business",
67
+ "WEF_FBFB2B85F9CD4A7C9AA907EBB29D5863_Kanban.Column": "To Do",
68
+ "WEF_FBFB2B85F9CD4A7C9AA907EBB29D5863_Kanban.Column.Done": False,
69
+ "System.BoardColumn": "To Do",
70
+ "System.BoardColumnDone": False,
71
+ "Microsoft.VSTS.Common.StateChangeDate": "2023-11-16T03:12:32.94Z",
72
+ },
73
+ }
74
+
75
+
76
+ def test_workitem() -> None:
77
+ """test basic work item"""
78
+ wi = WorkItem(MockAzureWorkItem())
79
+ assert wi.id == 5, wi.id
80
+ assert wi.ID == 5, wi.ID
81
+ assert wi.workitemtype == "User Story", wi.workitemtype
82
+ assert wi.system_workitemtype == "User Story", wi.system_workitemtype
83
+ assert wi.ChangedBy["displayName"] == "Edna Johnson", wi.changedBy
84
+ assert wi.changedby.displayname == "Edna Johnson", wi.changedby.displayname
85
+ assert wi.microsoft_vsts_common_priority == 2, wi.microsoft_vsts_common_priority
86
+
87
+ try:
88
+ _ = wi.not_a_field
89
+
90
+ except AttributeError as error:
91
+ assert "not_a_field" in str(error)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ test_workitem()
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """ Module Doc """
4
+
5
+ from types import SimpleNamespace
6
+
7
+ from devopsdriver.azure.workitem.client import Client
8
+ from devopsdriver.azure import Wiql, Equal
9
+
10
+
11
+ class MockClient: # pylint: disable=too-few-public-methods
12
+ """fake an azure work item client, at least what we use"""
13
+
14
+ def __init__(self):
15
+ self.query = None
16
+
17
+ def query_by_wiql(self, wiql, team_context, time_precision, top) -> SimpleNamespace:
18
+ """mock out the query_by_wiql"""
19
+ self.query = wiql.query
20
+ assert team_context is None, team_context
21
+ assert time_precision is None, time_precision
22
+ assert top is None, top
23
+ return SimpleNamespace(
24
+ work_items=[SimpleNamespace(id=number) for number in range(0, 20)]
25
+ )
26
+
27
+ def get_revisions(self, wi_id, project, top, skip, expand):
28
+ """Mock out get_revisions"""
29
+ assert project is None, project
30
+ assert top is None, top
31
+ assert skip is None, skip
32
+ assert expand is None, expand
33
+ assert 0 <= wi_id < 20
34
+ return []
35
+
36
+
37
+ def test_basic() -> None:
38
+ """Perform basic test on search and find_ids"""
39
+ client = Client(MockClient())
40
+ ids = client.find_ids(Wiql().select("State").where(Equal("State", "New")))
41
+ assert ids == list(range(0, 20))
42
+
43
+
44
+ def test_history() -> None:
45
+ """test history"""
46
+ client = Client(MockClient())
47
+ history = client.history(2)
48
+ assert not history
49
+
50
+
51
+ def test_find() -> None:
52
+ """Tests the find with the devops azure WorkItem"""
53
+ client = Client(MockClient())
54
+ found = client.find(Wiql().select("State").where(Equal("State", "New")))
55
+ assert len(found) == 20
56
+
57
+
58
+ if __name__ == "__main__":
59
+ test_find()
60
+ test_history()
61
+ test_basic()
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """ Test work item query language """
4
+
5
+ from datetime import date, datetime
6
+
7
+ from devopsdriver.azure import Wiql
8
+ from devopsdriver.azure import Ascending, Descending, Value
9
+ from devopsdriver.azure import IsEmpty, IsNotEmpty, And, Or
10
+ from devopsdriver.azure import GreaterThan, LessThan, Equal, NotEqual
11
+ from devopsdriver.azure import GreaterThanOrEqual, LessThanOrEqual
12
+
13
+
14
+ def test_no_params() -> None:
15
+ """Test empty, default wiql"""
16
+ assert str(Wiql()) == "SELECT [System.Id] FROM workitems", str(Wiql())
17
+
18
+
19
+ def test_expressions() -> None:
20
+ """Test much of the expression"""
21
+ start = date(2024, 6, 30)
22
+ end = datetime(2025, 1, 1, 18, 30, 15)
23
+ builder = (
24
+ Wiql()
25
+ .select("State", "Id")
26
+ .where(
27
+ And(
28
+ Equal("State", "New"),
29
+ IsEmpty("Title"),
30
+ IsNotEmpty("Priority"),
31
+ GreaterThan("CreatedDate", start),
32
+ LessThan("CreatedDate", end),
33
+ Or(
34
+ IsEmpty("RootCause"),
35
+ NotEqual("Priority", 1),
36
+ LessThan("Rank", 5.0),
37
+ LessThanOrEqual("time", 4.0),
38
+ GreaterThanOrEqual("BusinessValue", 4),
39
+ ),
40
+ )
41
+ )
42
+ .order_by(Descending("Priority"), Ascending("CreatedDate"))
43
+ .asof(start)
44
+ )
45
+ expected = (
46
+ """SELECT [System.State], [System.Id] FROM workitems """
47
+ + """WHERE [System.State] = "New" AND [System.Title] IS EMPTY """
48
+ + """AND [Microsoft.VSTS.Common.Priority] IS NOT EMPTY """
49
+ + """AND [System.CreatedDate] > 2024-06-30 """
50
+ + """AND [System.CreatedDate] < 2025-01-01 18:30:15 AND [RootCause] IS EMPTY """
51
+ + """OR [Microsoft.VSTS.Common.Priority] <> 1 OR [Rank] < 5.000 """
52
+ + """OR [time] <= 4.000 OR [BusinessValue] >= 4 """
53
+ + """ORDER BY [Microsoft.VSTS.Common.Priority] DESC, [System.CreatedDate] ASC """
54
+ + """ASOF 2024-06-30"""
55
+ )
56
+ assert str(builder) == expected, str(builder)
57
+
58
+
59
+ def test_invalid_value_type() -> None:
60
+ """tests assertion that field has known types"""
61
+ try:
62
+ assert str(Value(test_no_params)) is None, str(Value(test_no_params))
63
+
64
+ except AssertionError:
65
+ pass
66
+
67
+
68
+ if __name__ == "__main__":
69
+ test_invalid_value_type()
70
+ test_expressions()
71
+ test_no_params()
@@ -3,54 +3,15 @@
3
3
  """ Tests Settings class """
4
4
 
5
5
  from tempfile import TemporaryDirectory
6
- from os.path import join, splitext, dirname
7
- from os import makedirs
8
- from json import dump
6
+ from os.path import join
9
7
  from string import ascii_lowercase
10
8
  from itertools import product
11
9
 
12
- from yaml import safe_dump
10
+ from helpers import setup_settings, ensure, write
13
11
 
14
12
  import devopsdriver.settings as settings
15
13
 
16
14
 
17
- def __setup_settings(os: str = "Linux", shared: str = "test", **pref_dirs) -> None:
18
- settings.ENVIRON = {}
19
- settings.ARGV = []
20
- settings.SYSTEM = lambda: os
21
- settings.SHARED = shared
22
- settings.PRINT = lambda s: s
23
- settings.GET_PASSWORD = lambda s, n: f"{s}:{n}"
24
- settings.GET_PASS = lambda p: p
25
- settings.SET_PASSWORD = lambda s, n, p: f"{s} {n} {p}"
26
- # settings.MAKEDIRS = lambda p: p
27
- # settings.Settings.FORMATS = None
28
- settings.Settings.PREF_DIR = pref_dirs
29
-
30
-
31
- def ensure(directory: str) -> str:
32
- """Ensures that a directory exists before using
33
-
34
- Args:
35
- directory (str): The directory to create
36
-
37
- Returns:
38
- str: The directory that now exists
39
- """
40
- makedirs(directory, exist_ok=True)
41
- return directory
42
-
43
-
44
- def __write(path: str, **options) -> None:
45
- ensure(dirname(path))
46
-
47
- with open(path, "w", encoding="utf-8") as settings_file:
48
- if splitext(path)[1] == ".json":
49
- dump(options, settings_file)
50
- else:
51
- safe_dump(options, settings_file)
52
-
53
-
54
15
  def __setup_files(directory: str, dir1: str, dir2: str) -> None:
55
16
  """
56
17
  Priorities:
@@ -86,7 +47,7 @@ def __setup_files(directory: str, dir1: str, dir2: str) -> None:
86
47
 
87
48
  for name in ("test", "main"):
88
49
  for letter, os_dir in (("l", lin_dir), ("w", win_dir), ("m", mac_dir)):
89
- __write(
50
+ write(
90
51
  join(os_dir, "test.json"),
91
52
  **{l: f"{name[0]}{letter}{l}" for l in letters},
92
53
  dp={l: f"{name[0]}{letter}{l}" for l in letters},
@@ -97,7 +58,7 @@ def __setup_files(directory: str, dir1: str, dir2: str) -> None:
97
58
  for directory, name, ext in product(
98
59
  (dir2, dir1, directory), ("test", "main"), (".json", ".yaml", ".yml")
99
60
  ):
100
- __write(
61
+ write(
101
62
  join(directory, name + ext),
102
63
  **{l: f"{directory[-1]}{ext[2]}{name[0]}{letter}{l}" for l in letters},
103
64
  dp={l: f"{directory[-1]}{ext[2]}{name[0]}{letter}{l}" for l in letters},
@@ -111,7 +72,7 @@ def test_basic():
111
72
  base_dir = join(working_dir, "base")
112
73
 
113
74
  for os in ("Linux", "Darwin", "Windows", "Unknown"):
114
- __setup_settings(
75
+ setup_settings(
115
76
  os=os,
116
77
  shared="test",
117
78
  Linux=join(base_dir, "Linux"),
@@ -206,14 +167,14 @@ def test_cli_env_in_yaml():
206
167
  """test setting cli and env lookups in the yaml itself"""
207
168
  with TemporaryDirectory() as working_dir:
208
169
  base_dir = join(working_dir, "base")
209
- __setup_settings(
170
+ setup_settings(
210
171
  os="Linux",
211
172
  shared="test",
212
173
  Linux=join(base_dir, "Linux"),
213
174
  Darwin=join(base_dir, "macOS"),
214
175
  Windows=join(base_dir, "Windows"),
215
176
  )
216
- __write(
177
+ write(
217
178
  join(base_dir, "main.yml"),
218
179
  env={"aa": "alpha", "yy": "yota"},
219
180
  cli={"bb": "--beta"},
@@ -221,7 +182,7 @@ def test_cli_env_in_yaml():
221
182
  aa="main aa",
222
183
  bb="main bb",
223
184
  )
224
- __write(
185
+ write(
225
186
  join(base_dir, "test.yml"),
226
187
  env={"zz": "zeta"},
227
188
  cli={"dd": "--delta"},
@@ -259,14 +220,14 @@ def test_environ_values():
259
220
  """test environment variable substitution"""
260
221
  with TemporaryDirectory() as working_dir:
261
222
  base_dir = join(working_dir, "base")
262
- __setup_settings(
223
+ setup_settings(
263
224
  os="Linux",
264
225
  shared="test",
265
226
  Linux=join(base_dir, "Linux"),
266
227
  Darwin=join(base_dir, "macOS"),
267
228
  Windows=join(base_dir, "Windows"),
268
229
  )
269
- __write(
230
+ write(
270
231
  join(base_dir, "main.yml"),
271
232
  output="${home}/reports",
272
233
  settings="${appDir}/settings.json",
@@ -289,9 +250,9 @@ def test_environ_values():
289
250
  def test_main():
290
251
  """test the main entry point"""
291
252
  with TemporaryDirectory() as working_dir:
292
- __setup_settings(shared="test", Linux=join(working_dir, "Linux"))
253
+ setup_settings(shared="test", Linux=join(working_dir, "Linux"))
293
254
  settings.ARGV = ["ignore", "test"]
294
- __write(join(working_dir, "Linux", "test.yml"), test=3)
255
+ write(join(working_dir, "Linux", "test.yml"), test=3)
295
256
  settings.main()
296
257
 
297
258
 
@@ -300,7 +261,7 @@ def test_secret():
300
261
  with TemporaryDirectory() as working_dir:
301
262
  base_dir = join(working_dir, "base")
302
263
  passwords = {"system": {"john": "setec astronomy"}}
303
- __setup_settings(
264
+ setup_settings(
304
265
  os="Linux",
305
266
  shared="test",
306
267
  Linux=join(base_dir, "Linux"),
@@ -308,7 +269,7 @@ def test_secret():
308
269
  Windows=join(base_dir, "Windows"),
309
270
  )
310
271
  settings.GET_PASSWORD = lambda s, e: passwords.get(s, {}).get(e, None)
311
- __write(join(base_dir, "main.yml"), password="main")
272
+ write(join(base_dir, "main.yml"), password="main")
312
273
  opts = settings.Settings(join(base_dir, "main.py")).key(
313
274
  "password", "system/john"
314
275
  )
@@ -319,17 +280,21 @@ def test_main_set_secret():
319
280
  """test the main entry point when settings keychain secrets"""
320
281
 
321
282
  def set_password(s, n, p):
322
- assert s == "azure" and n == "token" and p == "setec astronomy", f"{s} {n} {p}"
283
+ assert (
284
+ s in ("azure", "jira") and n == "token" and p == "setec astronomy"
285
+ ), f"{s} {n} {p}"
323
286
 
324
287
  with TemporaryDirectory() as working_dir:
325
- __setup_settings(shared="test", Linux=join(working_dir, "Linux"))
326
- settings.ARGV = ["ignore", "--set_secrets"]
327
- settings.GET_PASSWORD = lambda s, n: None
288
+ setup_settings(shared="test", Linux=join(working_dir, "Linux"))
289
+ settings.ARGV = ["ignore", "--secrets"]
290
+ settings.GET_PASSWORD = lambda s, n: (
291
+ "password" if f"{s}/{n}" == "azure/token" else None
292
+ )
328
293
  settings.GET_PASS = lambda p: "setec astronomy"
329
294
  settings.SET_PASSWORD = set_password
330
- __write(
295
+ write(
331
296
  join(working_dir, "Linux", "test.yml"),
332
- secrets={"azure.token": "azure/token"},
297
+ secrets={"azure.token": "azure/token", "jira.token": "jira/token"},
333
298
  )
334
299
  settings.main()
335
300
 
@@ -1,11 +0,0 @@
1
- LICENSE
2
- README.md
3
- pyproject.toml
4
- devopsdriver/__init__.py
5
- devopsdriver/settings.py
6
- devopsdriver.egg-info/PKG-INFO
7
- devopsdriver.egg-info/SOURCES.txt
8
- devopsdriver.egg-info/dependency_links.txt
9
- devopsdriver.egg-info/requires.txt
10
- devopsdriver.egg-info/top_level.txt
11
- tests/test_settings.py
@@ -1,2 +0,0 @@
1
- PyYAML==6.0.1
2
- keyring==25.0.0
File without changes
File without changes