devopsdriver 0.1.53__tar.gz → 0.1.55__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 (52) hide show
  1. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/PKG-INFO +3 -3
  2. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/README.md +1 -1
  3. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/__init__.py +1 -1
  4. devopsdriver-0.1.55/devopsdriver/dataobject.py +218 -0
  5. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver.egg-info/PKG-INFO +3 -3
  6. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver.egg-info/requires.txt +1 -1
  7. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/pyproject.toml +1 -1
  8. devopsdriver-0.1.55/tests/test_dataobject.py +420 -0
  9. devopsdriver-0.1.53/devopsdriver/dataobject.py +0 -63
  10. devopsdriver-0.1.53/tests/test_dataobject.py +0 -29
  11. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/LICENSE +0 -0
  12. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/__init__.py +0 -0
  13. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/azureobject.py +0 -0
  14. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/builds/__init__.py +0 -0
  15. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/builds/build.py +0 -0
  16. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/builds/client.py +0 -0
  17. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/clients.py +0 -0
  18. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/pipeline/__init__.py +0 -0
  19. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/pipeline/client.py +0 -0
  20. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/pipeline/log.py +0 -0
  21. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/pipeline/pipeline.py +0 -0
  22. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/pipeline/run.py +0 -0
  23. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/timestamp.py +0 -0
  24. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/workitem/__init__.py +0 -0
  25. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/workitem/client.py +0 -0
  26. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/azdo/workitem/wiql.py +0 -0
  27. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/github/__init__.py +0 -0
  28. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/github/client.py +0 -0
  29. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/manage_settings.py +0 -0
  30. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/sendmail.py +0 -0
  31. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/settings.py +0 -0
  32. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/template.py +0 -0
  33. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver/templates/manage_settings.txt.mako +0 -0
  34. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver.egg-info/SOURCES.txt +0 -0
  35. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver.egg-info/dependency_links.txt +0 -0
  36. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver.egg-info/entry_points.txt +0 -0
  37. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/devopsdriver.egg-info/top_level.txt +0 -0
  38. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/setup.cfg +0 -0
  39. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_azureobject.py +0 -0
  40. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_build.py +0 -0
  41. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_build_client.py +0 -0
  42. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_clients.py +0 -0
  43. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_pipeline.py +0 -0
  44. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_pipeline_client.py +0 -0
  45. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_pipeline_run.py +0 -0
  46. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_timestamp.py +0 -0
  47. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_workitem_client.py +0 -0
  48. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_azure_workitem_wiql.py +0 -0
  49. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_manage_settings.py +0 -0
  50. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_sendmail.py +0 -0
  51. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_settings.py +0 -0
  52. {devopsdriver-0.1.53 → devopsdriver-0.1.55}/tests/test_template.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devopsdriver
3
- Version: 0.1.53
3
+ Version: 0.1.55
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.
@@ -50,7 +50,7 @@ Requires-Dist: PyYAML==6.0.3
50
50
  Requires-Dist: keyring==25.7.0
51
51
  Requires-Dist: setuptools==82.0.1
52
52
  Requires-Dist: azure-devops==7.1.0b4
53
- Requires-Dist: Mako==1.3.11
53
+ Requires-Dist: Mako==1.3.12
54
54
  Requires-Dist: PyGithub==2.9.1
55
55
  Provides-Extra: dev
56
56
  Requires-Dist: black>=24.3.0; extra == "dev"
@@ -64,7 +64,7 @@ Dynamic: license-file
64
64
  # devops-driver
65
65
 
66
66
  ![status sheild](https://img.shields.io/static/v1?label=status&message=beta&color=blue&style=plastic)
67
- [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.53&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.53/)
67
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.55&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.55/)
68
68
  [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
69
69
  [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
70
70
  [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
@@ -1,7 +1,7 @@
1
1
  # devops-driver
2
2
 
3
3
  ![status sheild](https://img.shields.io/static/v1?label=status&message=beta&color=blue&style=plastic)
4
- [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.53&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.53/)
4
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.55&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.55/)
5
5
  [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
6
6
  [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
7
7
  [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
@@ -6,6 +6,6 @@ from .template import Template
6
6
  from .azdo.clients import Azure
7
7
  from .github.client import Github
8
8
 
9
- __version__ = "0.1.53"
9
+ __version__ = "0.1.55"
10
10
  __author__ = "Marc Page"
11
11
  __credits__ = ""
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Data Objects"""
4
+
5
+ from json import dumps
6
+ from re import fullmatch
7
+ from typing import Any
8
+
9
+
10
+ class DataObject: # pylint: disable=too-few-public-methods
11
+ """dict like object with fuzzy field matching"""
12
+
13
+ def __init__(self, data: dict):
14
+ self.data = data
15
+
16
+ def _matches_field(self, 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
+ def _parse_value(self, data: Any) -> Any:
32
+ if isinstance(data, dict):
33
+ return DataObject._Dict(self, data)
34
+
35
+ if isinstance(data, list):
36
+ return [self._parse_value(d) for d in data]
37
+
38
+ return data
39
+
40
+ def _get_field(self, name: str, data: dict) -> Any:
41
+ assert name and data, f"name = {name} data = {data}"
42
+ found = [f for f in data if self._matches_field(name, f)]
43
+ assert len(found) in {0, 1}, found
44
+
45
+ if len(found) == 1:
46
+ return self._parse_value(data[found[0]])
47
+
48
+ return None
49
+
50
+ def __getattr__(self, name: str) -> Any:
51
+ return self._get_field(name, self.data)
52
+
53
+ def __str__(self) -> str:
54
+ return dumps(self.data, indent=2)
55
+
56
+ def __repr__(self) -> str:
57
+ return dumps(self.data, indent=2)
58
+
59
+ def lookup(self, path: str, default: Any = None) -> Any:
60
+ """
61
+ Resolves a custom path expression against the underlying data.
62
+
63
+ Supported syntax:
64
+ /key/subkey
65
+ .first
66
+ .last
67
+ .split(delimiter)
68
+ /(path=value)
69
+
70
+ Examples:
71
+ /id
72
+ /fields/System.Title
73
+ /relations/(/attributes/name=Parent).first/url.split(/).last
74
+ /relations/(/attributes/name=Child)/url.split(/).last
75
+ """
76
+
77
+ tokens = self._tokenize(path)
78
+ current = self.data
79
+
80
+ try:
81
+ for token in tokens:
82
+ current = self._apply_token(current, token)
83
+
84
+ except KeyError:
85
+ return default
86
+
87
+ return current
88
+
89
+ def _tokenize(self, path: str) -> list[str]:
90
+ """Splits a path into tokens while respecting parentheses."""
91
+
92
+ path = path.strip("/")
93
+
94
+ tokens: list[str] = []
95
+ current: list[str] = []
96
+ depth = 0
97
+
98
+ for char in path:
99
+ if char == "/" and depth == 0:
100
+ if current:
101
+ tokens.append("".join(current))
102
+ current = []
103
+
104
+ continue
105
+
106
+ if char == "(":
107
+ depth += 1
108
+ elif char == ")":
109
+ depth -= 1
110
+
111
+ current.append(char)
112
+
113
+ if current:
114
+ tokens.append("".join(current))
115
+
116
+ return tokens
117
+
118
+ def _apply_token(self, value: Any, token: str) -> Any:
119
+ """
120
+ Applies a token to the current value.
121
+
122
+ Supports chained expressions like:
123
+ (...).first
124
+ url.split(/).last.int
125
+ But maintains tokens like:
126
+ System.Title
127
+ """
128
+ segments = token.split(".")
129
+ parts = []
130
+
131
+ while segments[-1] in ("first", "last", "int") or segments[-1].startswith(
132
+ "split"
133
+ ):
134
+ parts.insert(0, segments.pop())
135
+
136
+ parts.insert(0, ".".join(segments))
137
+ current = value
138
+ group = False
139
+
140
+ for part in parts:
141
+ current, group = self._apply_part(current, part, group)
142
+
143
+ return current
144
+
145
+ def _apply_part( # pylint: disable=too-many-return-statements
146
+ self, value: Any, part: str, group: bool = False
147
+ ) -> tuple[Any, bool]:
148
+ """Applies a single operation."""
149
+ if part.startswith("(") and part.endswith(")"): # List filter
150
+ return self._filter_list(value, part[1:-1]), group
151
+
152
+ if part == "first": # first
153
+ if group:
154
+ return [v[0] for v in value], group
155
+
156
+ return value[0], False
157
+
158
+ if part == "last": # last
159
+ if group:
160
+ return [v[-1] for v in value], group
161
+
162
+ return value[-1], False
163
+
164
+ if part == "int": # int
165
+ if group:
166
+ return [int(v) for v in value], group
167
+
168
+ return int(value), group
169
+
170
+ # split(delimiter)
171
+ split_only_match = fullmatch(r"split\((.*?)\)", part)
172
+
173
+ if split_only_match:
174
+ delimiter = split_only_match.group(1)
175
+
176
+ if isinstance(value, list):
177
+ return [item.split(delimiter) for item in value], group
178
+
179
+ return value.split(delimiter), group
180
+
181
+ if isinstance(value, dict): # Dictionary lookup
182
+ return value[part], group
183
+
184
+ if isinstance(value, list): # Apply lookup to all items in a list
185
+ return [self._apply_part(item, part)[0] for item in value], True
186
+
187
+ raise ValueError(f"Cannot apply '{part}' to value: {value}")
188
+
189
+ def _filter_list(self, items: list[dict], expression: str) -> list[Any]:
190
+ """
191
+ Filters a list using an expression.
192
+
193
+ Expression format:
194
+ /path=value
195
+
196
+ Example:
197
+ /attributes/name=Parent
198
+ """
199
+
200
+ path_expr, expected = expression.split("=", 1)
201
+
202
+ result = []
203
+
204
+ for item in items:
205
+ actual = DataObject(item).lookup(path_expr)
206
+
207
+ if str(actual) == expected:
208
+ result.append(item)
209
+
210
+ return result
211
+
212
+ class _Dict(dict):
213
+ def __init__(self, dataobject, data: dict):
214
+ self.dataobject = dataobject
215
+ super().__init__(data)
216
+
217
+ def __getattr__(self, name: str) -> Any:
218
+ return self.dataobject._get_field(name, self)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devopsdriver
3
- Version: 0.1.53
3
+ Version: 0.1.55
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.
@@ -50,7 +50,7 @@ Requires-Dist: PyYAML==6.0.3
50
50
  Requires-Dist: keyring==25.7.0
51
51
  Requires-Dist: setuptools==82.0.1
52
52
  Requires-Dist: azure-devops==7.1.0b4
53
- Requires-Dist: Mako==1.3.11
53
+ Requires-Dist: Mako==1.3.12
54
54
  Requires-Dist: PyGithub==2.9.1
55
55
  Provides-Extra: dev
56
56
  Requires-Dist: black>=24.3.0; extra == "dev"
@@ -64,7 +64,7 @@ Dynamic: license-file
64
64
  # devops-driver
65
65
 
66
66
  ![status sheild](https://img.shields.io/static/v1?label=status&message=beta&color=blue&style=plastic)
67
- [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.53&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.53/)
67
+ [![status sheild](https://img.shields.io/static/v1?label=released&message=v0.1.55&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.55/)
68
68
  [![GitHub](https://img.shields.io/github/license/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
69
69
  [![GitHub contributors](https://img.shields.io/github/contributors/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/graphs/contributors)
70
70
  [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
@@ -2,7 +2,7 @@ PyYAML==6.0.3
2
2
  keyring==25.7.0
3
3
  setuptools==82.0.1
4
4
  azure-devops==7.1.0b4
5
- Mako==1.3.11
5
+ Mako==1.3.12
6
6
  PyGithub==2.9.1
7
7
 
8
8
  [dev]
@@ -10,7 +10,7 @@ dependencies = [
10
10
  "keyring==25.7.0",
11
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.11",
13
+ "Mako==1.3.12",
14
14
  "PyGithub==2.9.1",
15
15
  ]
16
16
  keywords = [
@@ -0,0 +1,420 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Testing dataobject"""
4
+
5
+ from devopsdriver.dataobject import DataObject
6
+
7
+
8
+ def test_dataobject_basic() -> None:
9
+ """tests the basic dataobject"""
10
+ data = DataObject(
11
+ {
12
+ "Test": 5,
13
+ "system.Go": 12,
14
+ "fields": {"harry": 16, "people": [{"name": "john"}, 5, "test"]},
15
+ }
16
+ )
17
+ assert data.test == 5, data.test
18
+ assert data.go == 12, data.go
19
+ assert data.system_go == 12, data.system_go
20
+ assert data.fields.Harry == 16, data.fields.Harry
21
+ assert data.Harry is None, data.Harry
22
+ assert data.fields.people[0].Name == "john", data.fields.people[0].Name
23
+ assert data.fields.people[1] == 5, data.fields.people[1]
24
+ assert data.fields.people[2] == "test", data.fields.people[2]
25
+
26
+
27
+ def test_lookup() -> None:
28
+ """Tests lookup"""
29
+ data = DataObject(
30
+ {
31
+ "url": "https://dev.azure.com/ni/94b22d7b-ad7b-4f5e-88f0-867910f91c94/"
32
+ + "_apis/wit/workItems/956619/revisions/200",
33
+ "fields": {
34
+ "System.WorkItemType": "Initiative",
35
+ "System.State": "Active",
36
+ "System.Reason": "Moved to state Active",
37
+ "System.AssignedTo": {
38
+ "displayName": "Samant, Abhay",
39
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
40
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/"
41
+ + "_apis/Identities/838da6d7-6336-68fa-9b48-053771a4d16f",
42
+ "_links": {
43
+ "avatar": {
44
+ "href": "https://dev.azure.com/ni/"
45
+ + "_apis/GraphProfile/MemberAvatars/"
46
+ + "aad.M2NkOWZmNWQtMDMwZC03NDNlLTk5OTUtZTc2MGFhZDRhYmM1"
47
+ }
48
+ },
49
+ "id": "838da6d7-6336-68fa-9b48-053771a4d16f",
50
+ "uniqueName": "abhay.samant@emerson.com",
51
+ "imageUrl": "https://dev.azure.com/ni/"
52
+ + "_apis/GraphProfile/MemberAvatars/"
53
+ + "aad.M2NkOWZmNWQtMDMwZC03NDNlLTk5OTUtZTc2MGFhZDRhYmM1",
54
+ "descriptor": "aad.M2NkOWZmNWQtMDMwZC03NDNlLTk5OTUtZTc2MGFhZDRhYmM1",
55
+ },
56
+ "System.CreatedDate": "2020-01-08T22:59:16.223Z",
57
+ "System.CreatedBy": {
58
+ "displayName": "Tillerson, Michael",
59
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
60
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/"
61
+ + "_apis/Identities/dc42cd0e-ab09-4553-8516-9efe930b92a2",
62
+ "_links": {
63
+ "avatar": {
64
+ "href": "https://dev.azure.com/ni/"
65
+ + "_apis/GraphProfile/MemberAvatars/"
66
+ + "aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2"
67
+ }
68
+ },
69
+ "id": "dc42cd0e-ab09-4553-8516-9efe930b92a2",
70
+ "uniqueName": "michael.tillerson@emerson.com",
71
+ "imageUrl": "https://dev.azure.com/ni/"
72
+ + "_apis/GraphProfile/MemberAvatars/"
73
+ + "aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2",
74
+ "descriptor": "aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2",
75
+ },
76
+ "System.ChangedDate": "2021-02-25T16:50:38.367Z",
77
+ "System.ChangedBy": {
78
+ "displayName": "SVC, tmrdazdoservice1",
79
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
80
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/"
81
+ + "_apis/Identities/760b4082-c90f-6f25-986a-2e22b876ccd7",
82
+ "_links": {
83
+ "avatar": {
84
+ "href": "https://dev.azure.com/ni/"
85
+ + "_apis/GraphProfile/MemberAvatars/"
86
+ + "aad.ODg2NDQ0NGMtNWJlNy03ZWRkLTljOTEtZmJiNTE4MzFlZjFj"
87
+ }
88
+ },
89
+ "id": "760b4082-c90f-6f25-986a-2e22b876ccd7",
90
+ "uniqueName": "tm-rd-azdo-service-1@emerson.com",
91
+ "imageUrl": "https://dev.azure.com/ni/"
92
+ + "_apis/GraphProfile/MemberAvatars/"
93
+ + "aad.ODg2NDQ0NGMtNWJlNy03ZWRkLTljOTEtZmJiNTE4MzFlZjFj",
94
+ "descriptor": "aad.ODg2NDQ0NGMtNWJlNy03ZWRkLTljOTEtZmJiNTE4MzFlZjFj",
95
+ },
96
+ "System.CommentCount": 0,
97
+ "System.TeamProject": "DevCentral",
98
+ "System.AreaPath": "DevCentral\\Business Units\\ADG\\ElectroMag\\MMIC",
99
+ "System.IterationPath": "DevCentral",
100
+ "Microsoft.VSTS.Common.ActivatedDate": "2021-01-13T17:33:29.453Z",
101
+ "Microsoft.VSTS.Common.ActivatedBy": {
102
+ "displayName": "Samant, Abhay",
103
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
104
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/"
105
+ + "_apis/Identities/838da6d7-6336-68fa-9b48-053771a4d16f",
106
+ "_links": {
107
+ "avatar": {
108
+ "href": "https://dev.azure.com/ni/"
109
+ + "_apis/GraphProfile/MemberAvatars/"
110
+ + "aad.M2NkOWZmNWQtMDMwZC03NDNlLTk5OTUtZTc2MGFhZDRhYmM1"
111
+ }
112
+ },
113
+ "id": "838da6d7-6336-68fa-9b48-053771a4d16f",
114
+ "uniqueName": "abhay.samant@emerson.com",
115
+ "imageUrl": "https://dev.azure.com/ni/"
116
+ + "_apis/GraphProfile/MemberAvatars/"
117
+ + "aad.M2NkOWZmNWQtMDMwZC03NDNlLTk5OTUtZTc2MGFhZDRhYmM1",
118
+ "descriptor": "aad.M2NkOWZmNWQtMDMwZC03NDNlLTk5OTUtZTc2MGFhZDRhYmM1",
119
+ },
120
+ "Microsoft.VSTS.Scheduling.TargetDate": "2021-09-30T05:00:00Z",
121
+ "Microsoft.VSTS.Scheduling.StartDate": "2021-01-13T17:21:25.887Z",
122
+ "Microsoft.VSTS.Common.StackRank": 36894149.0,
123
+ "WEF_C397CD3F9AD34F4CB8D9481E56BBB71C_Kanban.Column.Done": False,
124
+ "WEF_5ADB076CBE2147FC82A22EC1842CD4E9_Kanban.Column.Done": False,
125
+ "Custom.ExpectedStartDate": "2021-01-13T17:21:25.887Z",
126
+ "Custom.ExpectedEndDate": "2021-09-30T05:00:00Z",
127
+ "Custom.AssignedToDate": "2021-01-13T17:33:29Z",
128
+ "Custom.DaysUntilAssigned": 370.8,
129
+ "Custom.AllocationLevel": "Level 1",
130
+ "Custom.OfferingAffinity": "Cognitive RF Sensor Prototyping Testbed ",
131
+ "Custom.AeroDefGovAllocation": 100,
132
+ "WEF_E2F7AF0C84D84051A09966884CE567A9_Kanban.Column.Done": False,
133
+ "Custom.InitiativeType": "P&T Managed",
134
+ "Custom.IncludeonPoR": False,
135
+ "Custom.ScopeorQualityRisk": False,
136
+ "Custom.ScheduleRisk": False,
137
+ "Custom.DaysUntilActive": 370.8,
138
+ "System.Description": '<div><img src="https://dev.azure.com/ni/'
139
+ + '94b22d7b-ad7b-4f5e-88f0-867910f91c94/"+"_apis/wit/attachments/'
140
+ + '64027421-bbca-408e-a742-209aa15366a3?fileName=image.png" alt=Image><br></div>',
141
+ "Microsoft.VSTS.Common.AcceptanceCriteria": "<span>Measurement IP library"
142
+ + "<br></span>"
143
+ + "<div>Interactive Examples<br></div><div>For interactive use-case and demos"
144
+ + "<br></div>"
145
+ + "<div>Programming Examples<br></div><div>Demonstrate how to use the MMIC VIs"
146
+ + "<br></div>"
147
+ + "<div>Datasheet with Performance Metrics<br></div>"
148
+ + "<span>Documentation of the system and recommendation about the "
149
+ + "third-party components "
150
+ + "(couplers, switches, cables, adapters and cal-kit)</span>",
151
+ "Custom.DeprecatedFields": "The Iterative planning team is currently "
152
+ + "refactoring Initiatives and Epics. Expect changes to these two work items "
153
+ + "over the next few months.The below &quot;Legacy Fields&quot; are under "
154
+ + "consideration for removal. If these fields are needed, contact Green Rives.",
155
+ "WEF_5ADB076CBE2147FC82A22EC1842CD4E9_Kanban.Column": "Active",
156
+ "WEF_C397CD3F9AD34F4CB8D9481E56BBB71C_Kanban.Column": "Active",
157
+ "WEF_E2F7AF0C84D84051A09966884CE567A9_Kanban.Column": "Active",
158
+ "Microsoft.VSTS.Common.StateChangeDate": "2021-01-13T17:33:29Z",
159
+ "System.BoardColumn": "Active",
160
+ "System.BoardColumnDone": False,
161
+ "Custom.ParentID": 1081371,
162
+ "Custom.ParentTitle": "MMIC: SDP Dev Planning",
163
+ "System.Title": "MMIC Reference Architecture for PA/TRM Test Using 5841/5831",
164
+ "Microsoft.VSTS.Scheduling.StoryPoints": 390.6,
165
+ "Custom.CompletedStoryPoints": 180.0,
166
+ "System.Tags": "ADG Offering",
167
+ "System.Parent": 1081371,
168
+ },
169
+ "id": 956619,
170
+ "relations": [
171
+ {
172
+ "attributes": {"isLocked": False, "name": "Child"},
173
+ "rel": "System.LinkTypes.Hierarchy-Forward",
174
+ "url": "https://dev.azure.com/ni/94b22d7b-ad7b-4f5e-88f0-867910f91c94/"
175
+ + "_apis/wit/workItems/1094328",
176
+ },
177
+ {
178
+ "attributes": {"isLocked": False, "name": "Child"},
179
+ "rel": "System.LinkTypes.Hierarchy-Forward",
180
+ "url": "https://dev.azure.com/ni/94b22d7b-ad7b-4f5e-88f0-867910f91c94/"
181
+ + "_apis/wit/workItems/956628",
182
+ },
183
+ {
184
+ "attributes": {"isLocked": False, "name": "Child"},
185
+ "rel": "System.LinkTypes.Hierarchy-Forward",
186
+ "url": "https://dev.azure.com/ni/94b22d7b-ad7b-4f5e-88f0-867910f91c94/"
187
+ + "_apis/wit/workItems/1198202",
188
+ },
189
+ {
190
+ "attributes": {"isLocked": False, "name": "Parent"},
191
+ "rel": "System.LinkTypes.Hierarchy-Reverse",
192
+ "url": "https://dev.azure.com/ni/94b22d7b-ad7b-4f5e-88f0-867910f91c94/"
193
+ + "_apis/wit/workItems/1081371",
194
+ },
195
+ ],
196
+ "rev": 200,
197
+ }
198
+ )
199
+
200
+ assert data.lookup("/id") == 956619, data.lookup("/id")
201
+ assert (
202
+ data.lookup("/fields/System.Title")
203
+ == "MMIC Reference Architecture for PA/TRM Test Using 5841/5831"
204
+ ), data.lookup("/fields/System.Title")
205
+ assert (
206
+ data.lookup("/relations/(/attributes/name=Parent).first/url.split(/).last.int")
207
+ == 1081371
208
+ ), [data.lookup("/relations/(/attributes/name=Parent).first/url.split(/).last.int")]
209
+ assert data.lookup("/relations/(/attributes/name=Child)/url.split(/).last.int") == [
210
+ 1094328,
211
+ 956628,
212
+ 1198202,
213
+ ], [data.lookup("/relations/(/attributes/name=Child)/url.split(/).last.int")]
214
+ assert data.lookup("/fields/System.State") == "Active", data.lookup(
215
+ "/fields/System.State"
216
+ )
217
+ assert data.lookup("/fields/System.WorkItemType") == "Initiative", data.lookup(
218
+ "/fields/System.WorkItemType"
219
+ )
220
+ assert (
221
+ data.lookup("/fields/System.AreaPath")
222
+ == "DevCentral\\Business Units\\ADG\\ElectroMag\\MMIC"
223
+ ), data.lookup("/fields/System.AreaPath")
224
+ assert data.lookup("/fields/System.IterationPath") == "DevCentral", data.lookup(
225
+ "/fields/System.IterationPath"
226
+ )
227
+ assert data.lookup("/fields/System.Tags") == "ADG Offering", data.lookup(
228
+ "/fields/System.Tags"
229
+ )
230
+ assert int(
231
+ 10 * data.lookup("/fields/Microsoft.VSTS.Scheduling.StoryPoints")
232
+ ) == int(10 * 390.6), data.lookup("/fields/Microsoft.VSTS.Scheduling.StoryPoints")
233
+
234
+ assert data.lookup("/relations/(/attributes/name=Child)/url.split(/).first") == [
235
+ "https:",
236
+ "https:",
237
+ "https:",
238
+ ], [data.lookup("/relations/(/attributes/name=Child)/url.split(/).first")]
239
+
240
+ try:
241
+ data.lookup("/fields/not there")
242
+ except KeyError:
243
+ pass
244
+
245
+ try:
246
+ data.lookup("/fields/System.Title/not there")
247
+ except ValueError:
248
+ pass
249
+
250
+
251
+ def test_lookup_missing() -> None:
252
+ """Tests lookup when a key is missing"""
253
+ data = DataObject(
254
+ {
255
+ "url": "https://dev.azure.com/ni/94b22d7b-ad7b-4f5e-88f0-867910f91c94/_apis/"
256
+ + "wit/workItems/949367/revisions/54",
257
+ "fields": {
258
+ "System.AreaPath": "DevCentral\\Business Units\\ADG\\SharedRepo\\MMSsc",
259
+ "System.TeamProject": "DevCentral",
260
+ "System.IterationPath": "DevCentral\\zzzArchive\\Cycles\\Cycle 17\\i77 "
261
+ + "(ends 2020-02-28)",
262
+ "System.WorkItemType": "Research",
263
+ "System.State": "Closed",
264
+ "System.Reason": "Moved to state Closed",
265
+ "System.AssignedTo": {
266
+ "displayName": "Brown, Andy",
267
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
268
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/_apis/Identities/"
269
+ + "d91becd5-72ba-6967-a84a-6a6708550e20",
270
+ "_links": {
271
+ "avatar": {
272
+ "href": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
273
+ + "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl"
274
+ }
275
+ },
276
+ "id": "d91becd5-72ba-6967-a84a-6a6708550e20",
277
+ "uniqueName": "andy.c.brown@emerson.com",
278
+ "imageUrl": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
279
+ + "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl",
280
+ "descriptor": "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl",
281
+ },
282
+ "System.CreatedDate": "2019-12-10T20:33:08.743Z",
283
+ "System.CreatedBy": {
284
+ "displayName": "Tillerson, Michael",
285
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
286
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/_apis/Identities/"
287
+ + "dc42cd0e-ab09-4553-8516-9efe930b92a2",
288
+ "_links": {
289
+ "avatar": {
290
+ "href": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
291
+ + "aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2"
292
+ }
293
+ },
294
+ "id": "dc42cd0e-ab09-4553-8516-9efe930b92a2",
295
+ "uniqueName": "michael.tillerson@emerson.com",
296
+ "imageUrl": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
297
+ + "aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2",
298
+ "descriptor": "aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2",
299
+ },
300
+ "System.ChangedDate": "2026-05-07T16:20:55.96Z",
301
+ "System.ChangedBy": {
302
+ "displayName": "Tillerson, Michael",
303
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
304
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/_apis/Identities/"
305
+ + "dc42cd0e-ab09-4553-8516-9efe930b92a2",
306
+ "_links": {
307
+ "avatar": {
308
+ "href": "https://dev.azure.com/ni/_apis/GraphProfile/"
309
+ + "MemberAvatars/aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2"
310
+ }
311
+ },
312
+ "id": "dc42cd0e-ab09-4553-8516-9efe930b92a2",
313
+ "uniqueName": "michael.tillerson@emerson.com",
314
+ "imageUrl": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
315
+ + "aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2",
316
+ "descriptor": "aad.YWZkMzg2MTItMTAwMy03YmM1LWI1ODQtMTY1YTcwMmM1MmE2",
317
+ },
318
+ "System.CommentCount": 7,
319
+ "System.Title": "PA: RFSA: Test Bench Architecture (time boxed)",
320
+ "System.BoardColumn": "Closed",
321
+ "System.BoardColumnDone": False,
322
+ "Microsoft.VSTS.Common.StateChangeDate": "2019-12-10T20:33:08.743Z",
323
+ "Microsoft.VSTS.Common.ClosedDate": "2020-02-14T18:17:33.233Z",
324
+ "Microsoft.VSTS.Common.ValueArea": "Business",
325
+ "Microsoft.VSTS.Common.ClosedBy": {
326
+ "displayName": "Brown, Andy",
327
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
328
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/_apis/Identities/"
329
+ + "d91becd5-72ba-6967-a84a-6a6708550e20",
330
+ "_links": {
331
+ "avatar": {
332
+ "href": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
333
+ + "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl"
334
+ }
335
+ },
336
+ "id": "d91becd5-72ba-6967-a84a-6a6708550e20",
337
+ "uniqueName": "andy.c.brown@emerson.com",
338
+ "imageUrl": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
339
+ + "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl",
340
+ "descriptor": "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl",
341
+ },
342
+ "Microsoft.VSTS.Common.ResolvedDate": "2020-02-06T03:21:28.327Z",
343
+ "Microsoft.VSTS.Common.ResolvedBy": {
344
+ "displayName": "Brown, Andy",
345
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
346
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/_apis/Identities/"
347
+ + "d91becd5-72ba-6967-a84a-6a6708550e20",
348
+ "_links": {
349
+ "avatar": {
350
+ "href": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
351
+ + "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl"
352
+ }
353
+ },
354
+ "id": "d91becd5-72ba-6967-a84a-6a6708550e20",
355
+ "uniqueName": "andy.c.brown@emerson.com",
356
+ "imageUrl": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
357
+ + "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl",
358
+ "descriptor": "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl",
359
+ },
360
+ "Microsoft.VSTS.Common.StackRank": 388652161.0,
361
+ "Microsoft.VSTS.Scheduling.StoryPoints": 5.0,
362
+ "Custom.PendingValidationBy": {
363
+ "displayName": "Brown, Andy",
364
+ "url": "https://spsprodeus24.vssps.visualstudio.com/"
365
+ + "Afae0922c-6afd-42ea-b8cb-e21a431ec8dd/_apis/Identities/"
366
+ + "d91becd5-72ba-6967-a84a-6a6708550e20",
367
+ "_links": {
368
+ "avatar": {
369
+ "href": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
370
+ + "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl"
371
+ }
372
+ },
373
+ "id": "d91becd5-72ba-6967-a84a-6a6708550e20",
374
+ "uniqueName": "andy.c.brown@emerson.com",
375
+ "imageUrl": "https://dev.azure.com/ni/_apis/GraphProfile/MemberAvatars/"
376
+ + "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl",
377
+ "descriptor": "aad.ZDIxNmMzY2EtMDdiMi03MmQ1LWFjMWQtNmJhMThhZDlhN2Zl",
378
+ },
379
+ "Custom.PendingValidationDate": "2020-02-06T03:21:28.327Z",
380
+ "WEF_D8499809B15042DAABC202ECF51A67D8_Kanban.Column": "Closed",
381
+ "WEF_D8499809B15042DAABC202ECF51A67D8_Kanban.Column.Done": False,
382
+ "WEF_C7071942F5474A15BD600E74B97D7C7A_Kanban.Column": "Closed",
383
+ "WEF_C7071942F5474A15BD600E74B97D7C7A_Kanban.Column.Done": False,
384
+ "WEF_42B49516479B4AEEA0D80D89E8893C45_Kanban.Column": "Closed",
385
+ "WEF_42B49516479B4AEEA0D80D89E8893C45_Kanban.Column.Done": False,
386
+ "WEF_3E05701FE5EA4460876365252800813A_Kanban.Column": "Closed",
387
+ "WEF_3E05701FE5EA4460876365252800813A_Kanban.Column.Done": False,
388
+ "System.Description": "<div>As a developer, I need a testbench to measure and "
389
+ + "validate these features and assess performance</div>",
390
+ "Microsoft.VSTS.Common.AcceptanceCriteria": "<ul><li>Expect documentation "
391
+ + 'similar to <a href="https://dev.azure.com/ni/DevCentral/_wiki/wikis/'
392
+ + 'AppCentral.wiki/8216/EV-Battery-Test-System">this EV Battery Test System '
393
+ + "documentation</a></li><li>Purchase HW&nbsp;</li><ul><li>controller, chassis, "
394
+ + "modules, cables, host computer</li><li><br></li></ul><li>Able to deploy SW to "
395
+ + "DUT</li><li>Able to execute tests on DUT</li>"
396
+ + "<li>Able to record test results</li>"
397
+ + "<li><br></li><li>Eventually use DevOps Services</li>"
398
+ + "<ul><li>Deployment Framework</li>"
399
+ + "<li>Test Execution Framework</li>"
400
+ + "<li><span>Test Reporting Framework</span></li></ul></ul>",
401
+ },
402
+ "id": 949367,
403
+ "rev": 54,
404
+ }
405
+ )
406
+
407
+ assert (
408
+ data.lookup("/relations/(/attributes/name=Parent).first/url.split(/).last.int")
409
+ is None
410
+ ), [data.lookup("/relations/(/attributes/name=Parent).first/url.split(/).last.int")]
411
+ assert (
412
+ data.lookup("/relations/(/attributes/name=Child)/url.split(/).last.int", [])
413
+ == []
414
+ ), [data.lookup("/relations/(/attributes/name=Child)/url.split(/).last.int", [])]
415
+
416
+
417
+ if __name__ == "__main__":
418
+ test_lookup_missing()
419
+ test_lookup()
420
+ test_dataobject_basic()
@@ -1,63 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- """Data Objects"""
4
-
5
- from json import dumps
6
-
7
-
8
- class DataObject: # pylint: disable=too-few-public-methods
9
- """dict like object with fuzzy field matching"""
10
-
11
- def __init__(self, data: dict):
12
- self.data = data
13
-
14
- def _matches_field(self, name: str, field: str) -> bool:
15
- name = name.lower()
16
- field = field.lower()
17
-
18
- if name == field:
19
- return True
20
-
21
- if name == field.replace(".", "_"):
22
- return True
23
-
24
- if name == field.split(".")[-1]:
25
- return True
26
-
27
- return False
28
-
29
- def _parse_value(self, data: any) -> any:
30
- if isinstance(data, dict):
31
- return DataObject._Dict(self, data)
32
-
33
- if isinstance(data, list):
34
- return [self._parse_value(d) for d in data]
35
-
36
- return data
37
-
38
- def _get_field(self, name: str, data: dict) -> any:
39
- assert name and data, f"name = {name} data = {data}"
40
- found = [f for f in data if self._matches_field(name, f)]
41
- assert len(found) in {0, 1}, found
42
-
43
- if len(found) == 1:
44
- return self._parse_value(data[found[0]])
45
-
46
- return None
47
-
48
- def __getattr__(self, name: str) -> any:
49
- return self._get_field(name, self.data)
50
-
51
- def __str__(self) -> str:
52
- return dumps(self.data, indent=2)
53
-
54
- def __repr__(self) -> str:
55
- return dumps(self.data, indent=2)
56
-
57
- class _Dict(dict):
58
- def __init__(self, dataobject, data: dict):
59
- self.dataobject = dataobject
60
- super().__init__(data)
61
-
62
- def __getattr__(self, name: str) -> any:
63
- return self.dataobject._get_field(name, self)
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- """ Testing dataobject """
4
-
5
-
6
- from devopsdriver.dataobject import DataObject
7
-
8
-
9
- def test_dataobject_basic() -> None:
10
- """tests the basic dataobject"""
11
- data = DataObject(
12
- {
13
- "Test": 5,
14
- "system.Go": 12,
15
- "fields": {"harry": 16, "people": [{"name": "john"}, 5, "test"]},
16
- }
17
- )
18
- assert data.test == 5, data.test
19
- assert data.go == 12, data.go
20
- assert data.system_go == 12, data.system_go
21
- assert data.fields.Harry == 16, data.fields.Harry
22
- assert data.Harry is None, data.Harry
23
- assert data.fields.people[0].Name == "john", data.fields.people[0].Name
24
- assert data.fields.people[1] == 5, data.fields.people[1]
25
- assert data.fields.people[2] == "test", data.fields.people[2]
26
-
27
-
28
- if __name__ == "__main__":
29
- test_dataobject_basic()
File without changes
File without changes