devopsdriver 0.1.53__tar.gz → 0.1.54__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.54}/PKG-INFO +2 -2
  2. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/README.md +1 -1
  3. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/__init__.py +1 -1
  4. devopsdriver-0.1.54/devopsdriver/dataobject.py +214 -0
  5. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver.egg-info/PKG-INFO +2 -2
  6. devopsdriver-0.1.54/tests/test_dataobject.py +253 -0
  7. devopsdriver-0.1.53/devopsdriver/dataobject.py +0 -63
  8. devopsdriver-0.1.53/tests/test_dataobject.py +0 -29
  9. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/LICENSE +0 -0
  10. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/__init__.py +0 -0
  11. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/azureobject.py +0 -0
  12. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/builds/__init__.py +0 -0
  13. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/builds/build.py +0 -0
  14. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/builds/client.py +0 -0
  15. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/clients.py +0 -0
  16. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/pipeline/__init__.py +0 -0
  17. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/pipeline/client.py +0 -0
  18. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/pipeline/log.py +0 -0
  19. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/pipeline/pipeline.py +0 -0
  20. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/pipeline/run.py +0 -0
  21. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/timestamp.py +0 -0
  22. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/workitem/__init__.py +0 -0
  23. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/workitem/client.py +0 -0
  24. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/azdo/workitem/wiql.py +0 -0
  25. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/github/__init__.py +0 -0
  26. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/github/client.py +0 -0
  27. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/manage_settings.py +0 -0
  28. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/sendmail.py +0 -0
  29. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/settings.py +0 -0
  30. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/template.py +0 -0
  31. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver/templates/manage_settings.txt.mako +0 -0
  32. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver.egg-info/SOURCES.txt +0 -0
  33. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver.egg-info/dependency_links.txt +0 -0
  34. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver.egg-info/entry_points.txt +0 -0
  35. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver.egg-info/requires.txt +0 -0
  36. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/devopsdriver.egg-info/top_level.txt +0 -0
  37. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/pyproject.toml +0 -0
  38. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/setup.cfg +0 -0
  39. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_azureobject.py +0 -0
  40. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_build.py +0 -0
  41. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_build_client.py +0 -0
  42. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_clients.py +0 -0
  43. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_pipeline.py +0 -0
  44. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_pipeline_client.py +0 -0
  45. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_pipeline_run.py +0 -0
  46. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_timestamp.py +0 -0
  47. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_workitem_client.py +0 -0
  48. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_azure_workitem_wiql.py +0 -0
  49. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_manage_settings.py +0 -0
  50. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_sendmail.py +0 -0
  51. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/tests/test_settings.py +0 -0
  52. {devopsdriver-0.1.53 → devopsdriver-0.1.54}/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.54
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.
@@ -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.54&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.54/)
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.54&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.54/)
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.54"
10
10
  __author__ = "Marc Page"
11
11
  __credits__ = ""
@@ -0,0 +1,214 @@
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) -> 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
+ for token in tokens:
81
+ current = self._apply_token(current, token)
82
+
83
+ return current
84
+
85
+ def _tokenize(self, path: str) -> list[str]:
86
+ """Splits a path into tokens while respecting parentheses."""
87
+
88
+ path = path.strip("/")
89
+
90
+ tokens: list[str] = []
91
+ current: list[str] = []
92
+ depth = 0
93
+
94
+ for char in path:
95
+ if char == "/" and depth == 0:
96
+ if current:
97
+ tokens.append("".join(current))
98
+ current = []
99
+
100
+ continue
101
+
102
+ if char == "(":
103
+ depth += 1
104
+ elif char == ")":
105
+ depth -= 1
106
+
107
+ current.append(char)
108
+
109
+ if current:
110
+ tokens.append("".join(current))
111
+
112
+ return tokens
113
+
114
+ def _apply_token(self, value: Any, token: str) -> Any:
115
+ """
116
+ Applies a token to the current value.
117
+
118
+ Supports chained expressions like:
119
+ (...).first
120
+ url.split(/).last.int
121
+ But maintains tokens like:
122
+ System.Title
123
+ """
124
+ segments = token.split(".")
125
+ parts = []
126
+
127
+ while segments[-1] in ("first", "last", "int") or segments[-1].startswith(
128
+ "split"
129
+ ):
130
+ parts.insert(0, segments.pop())
131
+
132
+ parts.insert(0, ".".join(segments))
133
+ current = value
134
+ group = False
135
+
136
+ for part in parts:
137
+ current, group = self._apply_part(current, part, group)
138
+
139
+ return current
140
+
141
+ def _apply_part( # pylint: disable=too-many-return-statements
142
+ self, value: Any, part: str, group: bool = False
143
+ ) -> tuple[Any, bool]:
144
+ """Applies a single operation."""
145
+ if part.startswith("(") and part.endswith(")"): # List filter
146
+ return self._filter_list(value, part[1:-1]), group
147
+
148
+ if part == "first": # first
149
+ if group:
150
+ return [v[0] for v in value], group
151
+
152
+ return value[0], False
153
+
154
+ if part == "last": # last
155
+ if group:
156
+ return [v[-1] for v in value], group
157
+
158
+ return value[-1], False
159
+
160
+ if part == "int": # int
161
+ if group:
162
+ return [int(v) for v in value], group
163
+
164
+ return int(value), group
165
+
166
+ # split(delimiter)
167
+ split_only_match = fullmatch(r"split\((.*?)\)", part)
168
+
169
+ if split_only_match:
170
+ delimiter = split_only_match.group(1)
171
+
172
+ if isinstance(value, list):
173
+ return [item.split(delimiter) for item in value], group
174
+
175
+ return value.split(delimiter), group
176
+
177
+ if isinstance(value, dict): # Dictionary lookup
178
+ return value[part], group
179
+
180
+ if isinstance(value, list): # Apply lookup to all items in a list
181
+ return [self._apply_part(item, part)[0] for item in value], True
182
+
183
+ raise ValueError(f"Cannot apply '{part}' to value: {value}")
184
+
185
+ def _filter_list(self, items: list[dict], expression: str) -> list[Any]:
186
+ """
187
+ Filters a list using an expression.
188
+
189
+ Expression format:
190
+ /path=value
191
+
192
+ Example:
193
+ /attributes/name=Parent
194
+ """
195
+
196
+ path_expr, expected = expression.split("=", 1)
197
+
198
+ result = []
199
+
200
+ for item in items:
201
+ actual = DataObject(item).lookup(path_expr)
202
+
203
+ if str(actual) == expected:
204
+ result.append(item)
205
+
206
+ return result
207
+
208
+ class _Dict(dict):
209
+ def __init__(self, dataobject, data: dict):
210
+ self.dataobject = dataobject
211
+ super().__init__(data)
212
+
213
+ def __getattr__(self, name: str) -> Any:
214
+ 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.54
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.
@@ -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.54&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.54/)
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)
@@ -0,0 +1,253 @@
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
+ if __name__ == "__main__":
252
+ test_lookup()
253
+ 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