devopsdriver 0.1.51__tar.gz → 0.1.53__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/PKG-INFO +7 -7
  2. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/README.md +2 -2
  3. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/__init__.py +1 -2
  4. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/__init__.py +1 -1
  5. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/azureobject.py +2 -3
  6. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/client.py +0 -1
  7. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/clients.py +0 -2
  8. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/client.py +0 -1
  9. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/log.py +1 -3
  10. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/pipeline.py +0 -1
  11. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/run.py +0 -1
  12. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/timestamp.py +0 -1
  13. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/client.py +10 -11
  14. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/wiql.py +29 -8
  15. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/dataobject.py +0 -1
  16. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/github/client.py +0 -1
  17. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/manage_settings.py +3 -4
  18. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/sendmail.py +12 -6
  19. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/settings.py +112 -19
  20. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/template.py +0 -1
  21. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/PKG-INFO +7 -7
  22. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/requires.txt +4 -4
  23. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/pyproject.toml +4 -4
  24. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_workitem_wiql.py +47 -5
  25. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_settings.py +242 -7
  26. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/LICENSE +0 -0
  27. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/__init__.py +0 -0
  28. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/build.py +0 -0
  29. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/__init__.py +0 -0
  30. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/__init__.py +0 -0
  31. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/github/__init__.py +0 -0
  32. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver/templates/manage_settings.txt.mako +0 -0
  33. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/SOURCES.txt +0 -0
  34. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/dependency_links.txt +0 -0
  35. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/entry_points.txt +0 -0
  36. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/devopsdriver.egg-info/top_level.txt +0 -0
  37. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/setup.cfg +0 -0
  38. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_azureobject.py +0 -0
  39. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_build.py +0 -0
  40. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_build_client.py +0 -0
  41. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_clients.py +0 -0
  42. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_pipeline.py +0 -0
  43. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_pipeline_client.py +0 -0
  44. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_pipeline_run.py +0 -0
  45. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_timestamp.py +0 -0
  46. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_azure_workitem_client.py +0 -0
  47. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_dataobject.py +0 -0
  48. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_manage_settings.py +0 -0
  49. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_sendmail.py +0 -0
  50. {devopsdriver-0.1.51 → devopsdriver-0.1.53}/tests/test_template.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devopsdriver
3
- Version: 0.1.51
3
+ Version: 0.1.53
4
4
  Summary: DevOps tools
5
5
  Author-email: Marc Page <marcallenpage@gmail.com>
6
6
  License: This is free and unencumbered software released into the public domain.
@@ -47,11 +47,11 @@ Requires-Python: >=3.10
47
47
  Description-Content-Type: text/markdown
48
48
  License-File: LICENSE
49
49
  Requires-Dist: PyYAML==6.0.3
50
- Requires-Dist: keyring==25.6.0
51
- Requires-Dist: setuptools==80.9.0
50
+ Requires-Dist: keyring==25.7.0
51
+ Requires-Dist: setuptools==82.0.1
52
52
  Requires-Dist: azure-devops==7.1.0b4
53
- Requires-Dist: Mako==1.3.10
54
- Requires-Dist: PyGithub==2.5.0
53
+ Requires-Dist: Mako==1.3.11
54
+ Requires-Dist: PyGithub==2.9.1
55
55
  Provides-Extra: dev
56
56
  Requires-Dist: black>=24.3.0; extra == "dev"
57
57
  Requires-Dist: pylint>=3.1.0; extra == "dev"
@@ -64,7 +64,7 @@ Dynamic: license-file
64
64
  # devops-driver
65
65
 
66
66
  ![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.51&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.51/)
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/)
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)
@@ -75,7 +75,7 @@ Dynamic: license-file
75
75
  [![size sheild](https://img.shields.io/github/languages/code-size/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
76
76
 
77
77
  [![example workflow](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml/badge.svg)](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
78
- [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=98%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
78
+ [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=97%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
79
79
  [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
80
80
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
81
81
 
@@ -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.51&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.51/)
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/)
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)
@@ -12,7 +12,7 @@
12
12
  [![size sheild](https://img.shields.io/github/languages/code-size/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
13
13
 
14
14
  [![example workflow](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml/badge.svg)](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
15
- [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=98%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
15
+ [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=97%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
16
16
  [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
17
17
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
18
18
 
@@ -6,7 +6,6 @@ from .template import Template
6
6
  from .azdo.clients import Azure
7
7
  from .github.client import Github
8
8
 
9
-
10
- __version__ = "0.1.51"
9
+ __version__ = "0.1.53"
11
10
  __author__ = "Marc Page"
12
11
  __credits__ = ""
@@ -6,6 +6,6 @@ from .clients import Azure
6
6
  from .timestamp import Timestamp
7
7
 
8
8
  from .workitem.wiql import Wiql, Value, Field
9
- from .workitem.wiql import Ascending, Descending, And, Or, In, NotIn
9
+ from .workitem.wiql import Ascending, Descending, And, Or, In, NotIn, Under
10
10
  from .workitem.wiql import Equal, NotEqual, LessThanOrEqual, GreaterThanOrEqual
11
11
  from .workitem.wiql import IsEmpty, IsNotEmpty, LessThan, GreaterThan
@@ -3,7 +3,6 @@
3
3
 
4
4
  """An Azure Devops WorkItem"""
5
5
 
6
-
7
6
  from msrest.serialization import Model
8
7
 
9
8
  from devopsdriver.azdo.timestamp import Timestamp
@@ -17,13 +16,13 @@ class AzureObject(DataObject): # pylint: disable=too-few-public-methods
17
16
  self.raw = azure_object
18
17
  super().__init__(self.raw.as_dict())
19
18
 
20
- def _parse_value(self, data: any) -> any:
19
+ def _parse_value(self, data):
21
20
  if isinstance(data, str) and Timestamp.is_timestamp(data):
22
21
  return Timestamp(data)
23
22
 
24
23
  return super()._parse_value(data)
25
24
 
26
- def _get_field(self, name: str, data: dict) -> any:
25
+ def _get_field(self, name: str, data: dict):
27
26
  value = super()._get_field(name, data)
28
27
 
29
28
  if value is None and "fields" in data:
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Azure Build Client"""
5
5
 
6
-
7
6
  from datetime import datetime
8
7
  from azure.devops.v7_1.build import BuildClient
9
8
 
@@ -7,7 +7,6 @@ API Documented here:
7
7
  https://github.com/microsoft/azure-devops-python-api
8
8
  """
9
9
 
10
-
11
10
  from azure.devops.connection import Connection as AzureConnection
12
11
  from msrest.authentication import BasicAuthentication as MSBasicAuthentication
13
12
 
@@ -16,7 +15,6 @@ from devopsdriver.azdo.workitem.client import Client as WIClient
16
15
  from devopsdriver.azdo.pipeline.client import Client as PLClient
17
16
  from devopsdriver.azdo.builds.client import Client as BClient
18
17
 
19
-
20
18
  # for testing
21
19
  CONNECTION = AzureConnection
22
20
  AUTHENTICATION = MSBasicAuthentication
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Pipeline client"""
5
5
 
6
-
7
6
  from azure.devops.v7_1.pipelines import PipelinesClient
8
7
 
9
8
  from .pipeline import Pipeline
@@ -3,15 +3,13 @@
3
3
 
4
4
  """Azure Pipeline Run Log"""
5
5
 
6
-
7
6
  from azure.devops.v7_1.pipelines.models import Log as AzureLog
8
7
 
9
8
  from requests import get as get_url
10
9
 
11
10
  from devopsdriver.azdo.azureobject import AzureObject
12
11
 
13
-
14
- GET_URL = get_url
12
+ GET_URL = get_url # pylint: disable=invalid-name
15
13
 
16
14
 
17
15
  class Log(AzureObject): # pylint: disable=too-few-public-methods
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Azure Pipeline"""
5
5
 
6
-
7
6
  from azure.devops.v7_1.pipelines.models import Pipeline as AzurePipeline
8
7
  from azure.devops.v7_1.pipelines import PipelinesClient
9
8
 
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Pipeline Run"""
5
5
 
6
-
7
6
  from azure.devops.v7_1.pipelines.models import Pipeline
8
7
  from azure.devops.v7_1.pipelines.models import Run as AzureRun
9
8
  from azure.devops.v7_1.pipelines import PipelinesClient
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Tools that help when working with Azure"""
5
5
 
6
-
7
6
  from datetime import datetime, timezone, timedelta
8
7
  from functools import total_ordering
9
8
 
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Azure WorkItem Client"""
5
5
 
6
-
7
6
  from azure.devops.v7_1.work_item_tracking.models import Wiql as AzureWiql
8
7
  from azure.devops.v7_1.work_item_tracking.models import WorkItem as AzureWorkItem
9
8
  from azure.devops.v7_1.work_item_tracking.models import TeamContext
@@ -21,9 +20,9 @@ class Client:
21
20
  def query(
22
21
  self,
23
22
  wiql: Wiql | str,
24
- team_context: TeamContext = None,
25
- time_precision: bool = None,
26
- top: int = None,
23
+ team_context: TeamContext | None = None,
24
+ time_precision: bool | None = None,
25
+ top: int | None = None,
27
26
  ) -> WorkItemQueryResult:
28
27
  """Perform a wiql query
29
28
 
@@ -46,15 +45,15 @@ class Client:
46
45
  def get_history( # pylint: disable=too-many-positional-arguments,too-many-arguments
47
46
  self,
48
47
  wi_id: int,
49
- project: str = None,
50
- top: int = None,
51
- skip: int = None,
52
- expand: str = None,
48
+ project: str | None = None,
49
+ top: int | None = None,
50
+ skip: int | None = None,
51
+ expand: str | None = None,
53
52
  ) -> list[AzureWorkItem]:
54
53
  """Simple wrapper around get_revisions"""
55
54
  return self.client.get_revisions(wi_id, project, top, skip, expand)
56
55
 
57
- def find_ids(self, wiql: Wiql | str, top: int = None) -> list[int]:
56
+ def find_ids(self, wiql: Wiql | str, top: int | None = None) -> list[int]:
58
57
  """Given a query, find the work item ids
59
58
 
60
59
  Args:
@@ -73,9 +72,9 @@ class Client:
73
72
  # query_results_type: workItem
74
73
  # query_type: flat
75
74
  # columns: list of name, reference_name, url
76
- return [i.id for i in found.work_items]
75
+ return [i.id for i in found.work_items] if found.work_items else []
77
76
 
78
- def find(self, wiql: Wiql | str, top: int = None) -> list[list[AzureObject]]:
77
+ def find(self, wiql: Wiql | str, top: int | None = None) -> list[list[AzureObject]]:
79
78
  """Gets the full history of items found in a WIQL search
80
79
 
81
80
  Args:
@@ -19,7 +19,6 @@ ORDER BY [System.ChangedDate] DESC
19
19
  ASOF '02-11-2020'
20
20
  """
21
21
 
22
-
23
22
  from datetime import datetime, date
24
23
 
25
24
 
@@ -145,13 +144,20 @@ class Compare: # pylint: disable=too-few-public-methods
145
144
  return f"{str(self.left)} {self.operator} {str(self.right)}"
146
145
 
147
146
 
147
+ class Under(Compare): # pylint: disable=too-few-public-methods
148
+ """checks for under, like area path or iteration path"""
149
+
150
+ def __init__(self, field_name: Field | str, value: Value | str):
151
+ super().__init__(field_name, value, "UNDER")
152
+
153
+
148
154
  class In(Compare): # pylint: disable=too-few-public-methods
149
155
  """checks for field in a list of values"""
150
156
 
151
157
  def __init__(
152
158
  self,
153
159
  field: Field | str,
154
- *values: list[Value | str | date | datetime | int | float],
160
+ *values: Value | str | date | datetime | int | float,
155
161
  ):
156
162
  super().__init__(
157
163
  field,
@@ -167,7 +173,7 @@ class NotIn(Compare): # pylint: disable=too-few-public-methods
167
173
  def __init__(
168
174
  self,
169
175
  field: Field | str,
170
- *values: list[Value | str | date | datetime | int | float],
176
+ *values: Value | str | date | datetime | int | float,
171
177
  ):
172
178
  super().__init__(
173
179
  field,
@@ -234,7 +240,7 @@ class GreaterThanOrEqual(Compare): # pylint: disable=too-few-public-methods
234
240
  class Expression: # pylint: disable=too-few-public-methods
235
241
  """Join several compares"""
236
242
 
237
- def __init__(self, operator: str, *compares: list[Compare]):
243
+ def __init__(self, operator: str, *compares):
238
244
  self.operator = operator
239
245
  self.expressions = compares
240
246
 
@@ -245,14 +251,14 @@ class Expression: # pylint: disable=too-few-public-methods
245
251
  class And(Expression): # pylint: disable=too-few-public-methods
246
252
  """Join compares via AND"""
247
253
 
248
- def __init__(self, *compares: list[Compare | Expression]):
254
+ def __init__(self, *compares: Compare | Expression):
249
255
  super().__init__("AND", *compares)
250
256
 
251
257
 
252
258
  class Or(Expression): # pylint: disable=too-few-public-methods
253
259
  """join compares via OR"""
254
260
 
255
- def __init__(self, *compares: list[Compare | Expression]):
261
+ def __init__(self, *compares: Compare | Expression):
256
262
  super().__init__("OR", *compares)
257
263
 
258
264
 
@@ -264,8 +270,10 @@ class Wiql:
264
270
  self.search = None
265
271
  self.order = []
266
272
  self.snapshot = None
273
+ self.source = "WorkItems"
274
+ self.mode_type = None
267
275
 
268
- def select(self, *fields: list[Field | str]):
276
+ def select(self, *fields: Field | str):
269
277
  """The fields to select
270
278
 
271
279
  Returns:
@@ -286,6 +294,18 @@ class Wiql:
286
294
  self.search = expression
287
295
  return self
288
296
 
297
+ def from_source(self, source: str):
298
+ """Sets the FROM field"""
299
+ self.source = source
300
+ return self
301
+
302
+ def mode(self, results_mode: str):
303
+ """Sets the mode for link queries
304
+ results_mode may be one of: MustContain, MayContain, DoesNotContain, Recursive, None
305
+ """
306
+ self.mode_type = results_mode
307
+ return self
308
+
289
309
  def order_by(self, *orders):
290
310
  """Set the fields to order the results by
291
311
 
@@ -315,4 +335,5 @@ class Wiql:
315
335
  f" ORDER BY {', '.join(str(o) for o in self.order)}" if self.order else ""
316
336
  )
317
337
  asof = f" ASOF {str(self.snapshot)}" if self.snapshot else ""
318
- return f"SELECT {select} FROM workitems{where}{order}{asof}"
338
+ mode = f" MODE({self.mode_type})" if self.mode_type else ""
339
+ return f"SELECT {select} FROM {self.source}{where}{order}{asof}{mode}"
@@ -2,7 +2,6 @@
2
2
 
3
3
  """Data Objects"""
4
4
 
5
-
6
5
  from json import dumps
7
6
 
8
7
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  """Manages GitHub connection"""
4
4
 
5
-
6
5
  from github import Github as Github_connection, Auth
7
6
 
8
7
  from devopsdriver.settings import Settings
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Module Doc"""
5
5
 
6
-
7
6
  from os.path import dirname, join, abspath
8
7
  from sys import argv as sys_argv
9
8
  from getpass import getpass as os_getpass
@@ -14,9 +13,9 @@ from .settings import Settings
14
13
  from .template import Template
15
14
 
16
15
  ARGV = sys_argv
17
- PRINT = print
18
- SET_PASSWORD = set_password
19
- GET_PASS = os_getpass
16
+ PRINT = print # pylint: disable=invalid-name
17
+ SET_PASSWORD = set_password # pylint: disable=invalid-name
18
+ GET_PASS = os_getpass # pylint: disable=invalid-name
20
19
 
21
20
 
22
21
  def main() -> None:
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Ability to send emails with embedded images"""
5
5
 
6
-
7
6
  from smtplib import SMTP as OS_SMTP, SMTP_SSL as OS_SMTP_SSL
8
7
  from email.mime.multipart import MIMEMultipart as OS_MIMEMultipart
9
8
  from email.mime.text import MIMEText as OS_MIMEText
@@ -11,7 +10,6 @@ from email.mime.image import MIMEImage as OS_MIMEImage
11
10
 
12
11
  from devopsdriver.settings import Settings
13
12
 
14
-
15
13
  IMAGE_HEADERS = {".png": b"\x89PNG\r\n\x1a\n", ".jpg": b"\xff\xd8\xff"}
16
14
 
17
15
  # for testing
@@ -41,11 +39,11 @@ def image_extension(data: bytes) -> str:
41
39
  raise AttributeError("Image not a known format: " + ",".join(IMAGE_HEADERS))
42
40
 
43
41
 
44
- def send_email(
42
+ def send_email( # pylint: disable=too-many-locals
45
43
  recipients: str | list[str],
46
44
  subject: str,
47
45
  html_body: str,
48
- settings: Settings = None,
46
+ settings: Settings | None = None,
49
47
  **image_data,
50
48
  ):
51
49
  """Sends an email with embedded images
@@ -65,7 +63,9 @@ def send_email(
65
63
  ", ".join(missing) + " not found in:\n" + "\n".join(settings.search_files)
66
64
  )
67
65
  sender = settings["smtp.sender"]
66
+ assert isinstance(sender, str), sender
68
67
  username = settings.get("smtp.username", sender)
68
+ assert isinstance(username, str), username
69
69
  message = MIMEMULTIPART()
70
70
  message["Subject"] = subject
71
71
  message["From"] = sender
@@ -85,8 +85,14 @@ def send_email(
85
85
  )
86
86
  message.attach(image)
87
87
 
88
- with connection_type(settings["smtp.server"], settings["smtp.port"]) as smtp:
88
+ server = settings["smtp.server"]
89
+ assert isinstance(server, str), server
90
+ port = settings["smtp.port"]
91
+ assert isinstance(port, int), port
92
+ password = settings["smtp.password"]
93
+ assert isinstance(password, str), password
94
+ with connection_type(server, port) as smtp:
89
95
  smtp.set_debuglevel(False)
90
- smtp.login(username, settings["smtp.password"])
96
+ smtp.login(username, password)
91
97
  smtp.sendmail(sender, recipients, message.as_string())
92
98
  smtp.quit()
@@ -75,7 +75,8 @@ api:
75
75
  johndoe and janedoe share the same password, so you just need to update the `user`
76
76
  """
77
77
 
78
-
78
+ from __future__ import annotations
79
+ from dataclasses import MISSING, fields, is_dataclass
79
80
  from json import load
80
81
  from os.path import dirname, basename, splitext, join
81
82
  from os import environ as os_environ, makedirs as os_makedirs
@@ -83,22 +84,81 @@ from re import compile as regex
83
84
  from platform import system as os_system
84
85
  from sys import argv as sys_argv
85
86
  from getpass import getpass as os_getpass
87
+ from typing import Any, TypeVar, Union, cast, get_args, get_origin, get_type_hints
86
88
 
87
89
  from yaml import safe_load
88
90
  from keyring import get_password, set_password
89
91
  from keyring.backends import fail
90
92
 
91
-
92
93
  # for testing
93
94
  ENVIRON = os_environ
94
95
  ARGV = sys_argv
95
- SYSTEM = os_system
96
- MAKEDIRS = os_makedirs
96
+ SYSTEM = os_system # pylint: disable=invalid-name
97
+ MAKEDIRS = os_makedirs # pylint: disable=invalid-name
97
98
  SHARED = "devopsdriver"
98
- PRINT = print
99
- GET_PASSWORD = get_password
100
- SET_PASSWORD = set_password
101
- GET_PASS = os_getpass
99
+ PRINT = print # pylint: disable=invalid-name
100
+ GET_PASSWORD = get_password # pylint: disable=invalid-name
101
+ SET_PASSWORD = set_password # pylint: disable=invalid-name
102
+ GET_PASS = os_getpass # pylint: disable=invalid-name
103
+ T = TypeVar("T")
104
+
105
+
106
+ def _is_instance_of_type( # pylint: disable=too-many-branches, too-many-return-statements
107
+ value: Any, expected: Any
108
+ ) -> bool:
109
+ """Check if a value matches an expected type, including support for common typing constructs."""
110
+ if expected is Any:
111
+ return True
112
+
113
+ origin = get_origin(expected)
114
+ args = get_args(expected)
115
+
116
+ if origin is Union: # Optional[T] / Union[T1, T2, ...]
117
+ return any(_is_instance_of_type(value, t) for t in args)
118
+
119
+ if origin is list: # list[T]
120
+ if not isinstance(value, list):
121
+ return False
122
+
123
+ if not args:
124
+ return True
125
+
126
+ item_type = args[0]
127
+ return all(_is_instance_of_type(v, item_type) for v in value)
128
+
129
+ if origin is dict: # dict[K, V]
130
+ if not isinstance(value, dict):
131
+ return False
132
+
133
+ if len(args) != 2:
134
+ return True
135
+
136
+ key_type, val_type = args
137
+ return all(
138
+ _is_instance_of_type(k, key_type) and _is_instance_of_type(v, val_type)
139
+ for k, v in value.items()
140
+ )
141
+
142
+ if origin is tuple: # tuple[T1, T2] or tuple[T, ...]
143
+ if not isinstance(value, tuple):
144
+ return False
145
+
146
+ if not args:
147
+ return True
148
+
149
+ if len(args) == 2 and args[1] is Ellipsis:
150
+ return all(_is_instance_of_type(v, args[0]) for v in value)
151
+
152
+ if len(value) != len(args):
153
+ return False
154
+
155
+ return all(_is_instance_of_type(v, t) for v, t in zip(value, args))
156
+
157
+ try: # Fallback to normal isinstance
158
+ return isinstance(value, expected)
159
+
160
+ except TypeError:
161
+ return False # Covers unsupported runtime checks for some typing constructs
102
162
 
103
163
 
104
164
  def load_json(path: str) -> dict:
@@ -147,7 +207,9 @@ class Settings:
147
207
  }
148
208
  ENV_VAR_PATTERN = regex(r"\${(\S+)}")
149
209
 
150
- def __init__(self, file: str, *directories, shared_name: str = None, **settings):
210
+ def __init__(
211
+ self, file: str, *directories, shared_name: str | None = None, **settings
212
+ ):
151
213
  """Create a settings object using a file, directories to search, and settings overrides
152
214
 
153
215
  Args:
@@ -172,7 +234,7 @@ class Settings:
172
234
  self.environ = {}
173
235
  self.secrets = {}
174
236
 
175
- def __bypass(self, key: str, name: str, store: dict):
237
+ def __bypass(self, key: str, name: str | None, store: dict):
176
238
  if name is None:
177
239
  for setting_key, store_name in self.settings.get(key, {}).items():
178
240
  store[setting_key] = store_name
@@ -181,7 +243,7 @@ class Settings:
181
243
  store[key] = name
182
244
  return self
183
245
 
184
- def key(self, key: str, name: str = None):
246
+ def key(self, key: str, name: str | None = None):
185
247
  """Sets a keychain name to map to a settings value.
186
248
 
187
249
  Args:
@@ -195,7 +257,7 @@ class Settings:
195
257
  """
196
258
  return self.__bypass(key, name, self.secrets)
197
259
 
198
- def cli(self, key: str, name: str = None):
260
+ def cli(self, key: str, name: str | None = None):
199
261
  """Sets a command line switch to map to a settings value.
200
262
 
201
263
  Args:
@@ -209,7 +271,7 @@ class Settings:
209
271
  """
210
272
  return self.__bypass(key, name, self.opts)
211
273
 
212
- def env(self, key: str, name: str = None):
274
+ def env(self, key: str, name: str | None = None):
213
275
  """Sets an environment variable to map to a settings value.
214
276
 
215
277
  Args:
@@ -223,6 +285,37 @@ class Settings:
223
285
  """
224
286
  return self.__bypass(key, name, self.environ)
225
287
 
288
+ def parse(self, cls: type[T]) -> T:
289
+ """Extract settings into a dataclass for type safety"""
290
+ if not is_dataclass(cls):
291
+ raise TypeError(f"{cls} must be a dataclass type")
292
+
293
+ type_hints = get_type_hints(cls)
294
+ kwargs = {}
295
+
296
+ for f in fields(cls):
297
+ expected_type = type_hints.get(f.name, Any)
298
+
299
+ # Pull from settings if present, otherwise use dataclass default/default_factory
300
+ if self.has(f.name):
301
+ value = self.get(f.name)
302
+ elif f.default is not MISSING:
303
+ value = f.default
304
+ elif f.default_factory is not MISSING:
305
+ value = f.default_factory()
306
+ else:
307
+ raise KeyError(f"Missing required setting: {f.name}")
308
+
309
+ if not _is_instance_of_type(value, expected_type):
310
+ raise TypeError(
311
+ f"Invalid type for '{f.name}': expected {expected_type}, "
312
+ + f"got {type(value).__name__}"
313
+ )
314
+
315
+ kwargs[f.name] = value
316
+
317
+ return cast(T, cls(**kwargs))
318
+
226
319
  @staticmethod
227
320
  def __patch_instance(key: str) -> str:
228
321
  for env_key, value in ENVIRON.items():
@@ -232,7 +325,7 @@ class Settings:
232
325
  return "${" + key + "}"
233
326
 
234
327
  @staticmethod
235
- def __patch(value: any) -> any:
328
+ def __patch(value):
236
329
  if isinstance(value, str):
237
330
  return Settings.ENV_VAR_PATTERN.sub(
238
331
  lambda m: Settings.__patch_instance(m.group(1)), value
@@ -257,7 +350,7 @@ class Settings:
257
350
  secret_name = parts[1] if len(parts) == 2 else parts[0]
258
351
  return (service, secret_name)
259
352
 
260
- def __lookup(self, key: str, check: bool, default: any = None) -> any:
353
+ def __lookup(self, key: str, check: bool, default=None):
261
354
  # Settings passed in override everything
262
355
  if key in self.overrides:
263
356
  return True if check else self.overrides[key]
@@ -293,7 +386,7 @@ class Settings:
293
386
 
294
387
  return Settings.__patch(level.get(keys[-1], default))
295
388
 
296
- def get(self, key: str, default: any = None) -> any:
389
+ def get(self, key: str, default=None):
297
390
  """Dictionary-like get
298
391
 
299
392
  Args:
@@ -314,12 +407,12 @@ class Settings:
314
407
  Returns:
315
408
  bool: True if the key exists
316
409
  """
317
- return self.__lookup(key, check=True)
410
+ return bool(self.__lookup(key, check=True))
318
411
 
319
412
  def __contains__(self, key: str) -> bool:
320
413
  return self.has(key)
321
414
 
322
- def __getitem__(self, key: str) -> any:
415
+ def __getitem__(self, key: str):
323
416
  if not self.has(key):
324
417
  raise KeyError(key)
325
418
 
@@ -371,6 +464,6 @@ class Settings:
371
464
 
372
465
  for extension, name, directory, loader in search_info:
373
466
  contents = loader(join(directory, name + extension))
374
- Settings.__merge(settings, contents)
467
+ Settings.__merge(settings, contents if contents else {})
375
468
 
376
469
  return settings
@@ -3,7 +3,6 @@
3
3
 
4
4
  """Module Doc"""
5
5
 
6
-
7
6
  from os.path import dirname, basename, splitext
8
7
 
9
8
  from mako.lookup import TemplateLookup
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devopsdriver
3
- Version: 0.1.51
3
+ Version: 0.1.53
4
4
  Summary: DevOps tools
5
5
  Author-email: Marc Page <marcallenpage@gmail.com>
6
6
  License: This is free and unencumbered software released into the public domain.
@@ -47,11 +47,11 @@ Requires-Python: >=3.10
47
47
  Description-Content-Type: text/markdown
48
48
  License-File: LICENSE
49
49
  Requires-Dist: PyYAML==6.0.3
50
- Requires-Dist: keyring==25.6.0
51
- Requires-Dist: setuptools==80.9.0
50
+ Requires-Dist: keyring==25.7.0
51
+ Requires-Dist: setuptools==82.0.1
52
52
  Requires-Dist: azure-devops==7.1.0b4
53
- Requires-Dist: Mako==1.3.10
54
- Requires-Dist: PyGithub==2.5.0
53
+ Requires-Dist: Mako==1.3.11
54
+ Requires-Dist: PyGithub==2.9.1
55
55
  Provides-Extra: dev
56
56
  Requires-Dist: black>=24.3.0; extra == "dev"
57
57
  Requires-Dist: pylint>=3.1.0; extra == "dev"
@@ -64,7 +64,7 @@ Dynamic: license-file
64
64
  # devops-driver
65
65
 
66
66
  ![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.51&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.51/)
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/)
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)
@@ -75,7 +75,7 @@ Dynamic: license-file
75
75
  [![size sheild](https://img.shields.io/github/languages/code-size/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver)
76
76
 
77
77
  [![example workflow](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml/badge.svg)](https://github.com/marcpage/devops-driver/actions/workflows/pr.yml)
78
- [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=98%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
78
+ [![status sheild](https://img.shields.io/static/v1?label=test+coverage&message=97%&color=active&style=plastic)](https://github.com/marcpage/devops-driver/blob/main/Makefile#L4)
79
79
  [![issues sheild](https://img.shields.io/github/issues-raw/marcpage/devops-driver?style=plastic)](https://github.com/marcpage/devops-driver/issues)
80
80
  [![GitHub pull requests](https://img.shields.io/github/issues-pr/marcpage/devops-driver?style=flat)](https://github.com/marcpage/devops-driver/pulls)
81
81
 
@@ -1,9 +1,9 @@
1
1
  PyYAML==6.0.3
2
- keyring==25.6.0
3
- setuptools==80.9.0
2
+ keyring==25.7.0
3
+ setuptools==82.0.1
4
4
  azure-devops==7.1.0b4
5
- Mako==1.3.10
6
- PyGithub==2.5.0
5
+ Mako==1.3.11
6
+ PyGithub==2.9.1
7
7
 
8
8
  [dev]
9
9
  black>=24.3.0
@@ -7,11 +7,11 @@ dynamic = ["version"]
7
7
  requires-python = ">= 3.10"
8
8
  dependencies = [
9
9
  "PyYAML==6.0.3",
10
- "keyring==25.6.0",
11
- "setuptools==80.9.0", # neded for azure-devops to use 7.1 API
10
+ "keyring==25.7.0",
11
+ "setuptools==82.0.1", # neded for azure-devops to use 7.1 API
12
12
  "azure-devops==7.1.0b4",
13
- "Mako==1.3.10",
14
- "PyGithub==2.5.0",
13
+ "Mako==1.3.11",
14
+ "PyGithub==2.9.1",
15
15
  ]
16
16
  keywords = [
17
17
  "azure",
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- """ Test work item query language """
3
+ """Test work item query language"""
4
4
 
5
5
  from datetime import date, datetime
6
6
 
7
7
  from devopsdriver.azdo import Wiql
8
8
  from devopsdriver.azdo import Ascending, Descending, Value
9
- from devopsdriver.azdo import IsEmpty, IsNotEmpty, And, Or, In, NotIn
9
+ from devopsdriver.azdo import IsEmpty, IsNotEmpty, And, Or, In, NotIn, Under
10
10
  from devopsdriver.azdo import GreaterThan, LessThan, Equal, NotEqual
11
11
  from devopsdriver.azdo import GreaterThanOrEqual, LessThanOrEqual
12
12
 
13
13
 
14
14
  def test_no_params() -> None:
15
15
  """Test empty, default wiql"""
16
- assert str(Wiql()) == "SELECT [System.Id] FROM workitems", str(Wiql())
16
+ assert str(Wiql()) == "SELECT [System.Id] FROM WorkItems", str(Wiql())
17
17
 
18
18
 
19
19
  def test_expressions() -> None:
@@ -43,7 +43,7 @@ def test_expressions() -> None:
43
43
  .asof(start)
44
44
  )
45
45
  expected = (
46
- """SELECT [System.State], [System.Id] FROM workitems """
46
+ """SELECT [System.State], [System.Id] FROM WorkItems """
47
47
  + """WHERE [System.State] = "New" AND [System.Title] IS EMPTY """
48
48
  + """AND [Microsoft.VSTS.Common.Priority] IS NOT EMPTY """
49
49
  + """AND [System.CreatedDate] > "06/30/2024" """
@@ -71,14 +71,56 @@ def test_in_and_not_in() -> None:
71
71
  And(In("State", "New", "Ready for Development"), NotIn("Priority", 1, 2))
72
72
  )
73
73
  expected = (
74
- """SELECT [System.Id] FROM workitems WHERE [System.State] """
74
+ """SELECT [System.Id] FROM WorkItems WHERE [System.State] """
75
75
  + """IN ("New", "Ready for Development") AND [Microsoft.VSTS.Common.Priority] """
76
76
  + """NOT IN (1, 2)"""
77
77
  )
78
78
  assert str(builder) == expected, str(builder)
79
79
 
80
80
 
81
+ def test_under() -> None:
82
+ """Test under operator"""
83
+ builder = Wiql().where(Under("System.AreaPath", "/Project/Team/Product"))
84
+ expected = (
85
+ """SELECT [System.Id] FROM WorkItems WHERE [System.AreaPath] """
86
+ + """UNDER "/Project/Team/Product\""""
87
+ )
88
+ assert str(builder) == expected, str(builder)
89
+
90
+
91
+ def test_from() -> None:
92
+ """Test from"""
93
+ builder = (
94
+ Wiql()
95
+ .from_source("Wiki")
96
+ .where(Under("System.AreaPath", "/Project/Team/Product"))
97
+ )
98
+ expected = (
99
+ """SELECT [System.Id] FROM Wiki WHERE [System.AreaPath] """
100
+ + """UNDER "/Project/Team/Product\""""
101
+ )
102
+ assert str(builder) == expected, str(builder)
103
+
104
+
105
+ def test_mode() -> None:
106
+ """Test mode"""
107
+ builder = (
108
+ Wiql()
109
+ .mode("Recursive")
110
+ .where(Under("System.AreaPath", "/Project/Team/Product"))
111
+ )
112
+ expected = (
113
+ """SELECT [System.Id] FROM WorkItems WHERE [System.AreaPath] """
114
+ + """UNDER "/Project/Team/Product" """
115
+ + """MODE(Recursive)"""
116
+ )
117
+ assert str(builder) == expected, [str(builder), expected]
118
+
119
+
81
120
  if __name__ == "__main__":
121
+ test_under()
122
+ test_from()
123
+ test_mode()
82
124
  test_in_and_not_in()
83
125
  test_invalid_value_type()
84
126
  test_expressions()
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- """ Tests Settings class """
3
+ """Tests Settings class"""
4
4
 
5
- from tempfile import TemporaryDirectory
5
+ from dataclasses import dataclass, field
6
+ from itertools import product
6
7
  from os.path import join
7
8
  from string import ascii_lowercase
8
- from itertools import product
9
+ from tempfile import TemporaryDirectory
10
+ from typing import Any
9
11
 
10
12
  from helpers import setup_settings, ensure, write
11
13
 
@@ -13,6 +15,21 @@ from devopsdriver import settings # debug access
13
15
  from devopsdriver.settings import Settings
14
16
 
15
17
 
18
+ @dataclass
19
+ class MockSettings: # pylint: disable=too-many-instance-attributes
20
+ """Testing type-safe class"""
21
+
22
+ name: str
23
+ names: list[str]
24
+ age_names: dict[int, str]
25
+ age: int
26
+ stuff: list
27
+ info: dict
28
+ alive: bool = True
29
+ years: list[int] = field(default_factory=lambda: [2004, 2104])
30
+ random: Any = None
31
+
32
+
16
33
  def __setup_files(directory: str, dir1: str, dir2: str) -> None:
17
34
  """
18
35
  Priorities:
@@ -67,7 +84,7 @@ def __setup_files(directory: str, dir1: str, dir2: str) -> None:
67
84
  letters.pop()
68
85
 
69
86
 
70
- def test_basic():
87
+ def test_basic() -> None: # pylint: disable=too-many-statements
71
88
  """test the basic functionality"""
72
89
  with TemporaryDirectory() as working_dir:
73
90
  base_dir = join(working_dir, "base")
@@ -196,7 +213,7 @@ def test_basic():
196
213
  pass
197
214
 
198
215
 
199
- def test_cli_env_in_yaml():
216
+ def test_cli_env_in_yaml() -> None:
200
217
  """test setting cli and env lookups in the yaml itself"""
201
218
  with TemporaryDirectory() as working_dir:
202
219
  base_dir = join(working_dir, "base")
@@ -245,7 +262,7 @@ def test_cli_env_in_yaml():
245
262
  assert opts["zz"] == "environ zz", opts["zz"]
246
263
 
247
264
 
248
- def test_environ_values():
265
+ def test_environ_values() -> None:
249
266
  """test environment variable substitution"""
250
267
  with TemporaryDirectory() as working_dir:
251
268
  base_dir = join(working_dir, "base")
@@ -272,7 +289,7 @@ def test_environ_values():
272
289
  assert opts["value"] == "testing ${noenv} for noenv", opts["value"]
273
290
 
274
291
 
275
- def test_secret():
292
+ def test_secret() -> None:
276
293
  """test os secret storage"""
277
294
  with TemporaryDirectory() as working_dir:
278
295
  base_dir = join(working_dir, "base")
@@ -290,7 +307,225 @@ def test_secret():
290
307
  assert opts["password"] == "setec astronomy", opts["password"]
291
308
 
292
309
 
310
+ def test_parse(): # pylint: disable=too-many-statements
311
+ """Test parsing into type-safe type"""
312
+ with TemporaryDirectory() as working_dir:
313
+ base_dir = join(working_dir, "base")
314
+ settings_path = join(base_dir, "parse.yml")
315
+ setup_settings(
316
+ os="Unknown",
317
+ shared="test",
318
+ Linux=join(base_dir, "Linux"),
319
+ Darwin=join(base_dir, "macOS"),
320
+ Windows=join(base_dir, "Windows"),
321
+ )
322
+
323
+ write(
324
+ settings_path,
325
+ name="John Doe",
326
+ names=["John", "Doe"],
327
+ age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
328
+ age=50,
329
+ alive=True,
330
+ years=(1973, 2073),
331
+ stuff=[],
332
+ info={},
333
+ )
334
+ test_settings = Settings(settings_path).parse(MockSettings)
335
+
336
+ assert test_settings.name == "John Doe", test_settings.name
337
+ assert len(test_settings.names) == 2, test_settings.names
338
+ assert test_settings.names[0] == "John", test_settings.names
339
+ assert test_settings.names[1] == "Doe", test_settings.names
340
+ assert len(test_settings.age_names) == 3, test_settings.age_names
341
+ assert 0 in test_settings.age_names, test_settings.age_names
342
+ assert 50 in test_settings.age_names, test_settings.age_names
343
+ assert 100 in test_settings.age_names, test_settings.age_names
344
+ assert test_settings.age_names[0] == "Newborn", test_settings.age_names
345
+ assert test_settings.age_names[50] == "Middle age", test_settings.age_names
346
+ assert test_settings.age_names[100] == "Old", test_settings.age_names
347
+ assert test_settings.age == 50
348
+ assert test_settings.alive
349
+ assert len(test_settings.years) == 2, test_settings.years
350
+ assert test_settings.years[0] == 1973, test_settings.years
351
+ assert test_settings.years[1] == 2073, test_settings.years
352
+ assert test_settings.random is None, test_settings.random
353
+ assert len(test_settings.stuff) == 0, test_settings.stuff
354
+ assert len(test_settings.info) == 0, test_settings.info
355
+
356
+ write(
357
+ settings_path,
358
+ name="John Doe",
359
+ names=["John", "Doe"],
360
+ age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
361
+ age=50,
362
+ stuff=[],
363
+ info={},
364
+ )
365
+ test_settings = Settings(settings_path).parse(MockSettings)
366
+
367
+ assert test_settings.name == "John Doe", test_settings.name
368
+ assert len(test_settings.names) == 2, test_settings.names
369
+ assert test_settings.names[0] == "John", test_settings.names
370
+ assert test_settings.names[1] == "Doe", test_settings.names
371
+ assert len(test_settings.age_names) == 3, test_settings.age_names
372
+ assert 0 in test_settings.age_names, test_settings.age_names
373
+ assert 50 in test_settings.age_names, test_settings.age_names
374
+ assert 100 in test_settings.age_names, test_settings.age_names
375
+ assert test_settings.age_names[0] == "Newborn", test_settings.age_names
376
+ assert test_settings.age_names[50] == "Middle age", test_settings.age_names
377
+ assert test_settings.age_names[100] == "Old", test_settings.age_names
378
+ assert test_settings.age == 50
379
+ assert test_settings.alive
380
+ assert len(test_settings.years) == 2, test_settings.years
381
+ assert test_settings.years[0] == 2004, test_settings.years
382
+ assert test_settings.years[1] == 2104, test_settings.years
383
+ assert test_settings.random is None, test_settings.random
384
+ assert len(test_settings.stuff) == 0, test_settings.stuff
385
+ assert len(test_settings.info) == 0, test_settings.info
386
+
387
+ write(
388
+ settings_path,
389
+ name=None,
390
+ names=["John", "Doe"],
391
+ age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
392
+ age=50,
393
+ alive=True,
394
+ years=(1973, 2073),
395
+ stuff=[],
396
+ info={},
397
+ )
398
+ try:
399
+ test_settings = Settings(settings_path).parse(MockSettings)
400
+ assert False, "Expected TypeError Exception"
401
+ except TypeError:
402
+ pass
403
+
404
+ write(
405
+ settings_path,
406
+ name="John Doe",
407
+ names=[1, 2],
408
+ age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
409
+ age=50,
410
+ alive=True,
411
+ years=(1973, 2073),
412
+ stuff=[],
413
+ info={},
414
+ )
415
+ try:
416
+ test_settings = Settings(settings_path).parse(MockSettings)
417
+ assert False, "Expected TypeError Exception"
418
+ except TypeError:
419
+ pass
420
+
421
+ write(
422
+ settings_path,
423
+ name="John Doe",
424
+ names=None,
425
+ age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
426
+ age=50,
427
+ alive=True,
428
+ years=(1973, 2073),
429
+ stuff=[],
430
+ info={},
431
+ )
432
+ try:
433
+ test_settings = Settings(settings_path).parse(MockSettings)
434
+ assert False, "Expected TypeError Exception"
435
+ except TypeError:
436
+ pass
437
+
438
+ write(
439
+ settings_path,
440
+ name="John Doe",
441
+ names=["John", "Doe"],
442
+ age_names={"0": "Newborn", "50": "Middle age", "100": "Old"},
443
+ age=50,
444
+ alive=True,
445
+ years=(1973, 2073),
446
+ stuff=[],
447
+ info={},
448
+ )
449
+ try:
450
+ test_settings = Settings(settings_path).parse(MockSettings)
451
+ assert False, "Expected TypeError Exception"
452
+ except TypeError:
453
+ pass
454
+
455
+ write(
456
+ settings_path,
457
+ name="John Doe",
458
+ names=["John", "Doe"],
459
+ age_names=None,
460
+ age=50,
461
+ alive=True,
462
+ years=(1973, 2073),
463
+ stuff=[],
464
+ info={},
465
+ )
466
+ try:
467
+ test_settings = Settings(settings_path).parse(MockSettings)
468
+ assert False, "Expected TypeError Exception"
469
+ except TypeError:
470
+ pass
471
+
472
+ write(
473
+ settings_path,
474
+ name="John Doe",
475
+ names=["John", "Doe"],
476
+ age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
477
+ age=None,
478
+ alive=True,
479
+ years=(1973, 2073),
480
+ stuff=[],
481
+ info={},
482
+ )
483
+ try:
484
+ test_settings = Settings(settings_path).parse(MockSettings)
485
+ assert False, "Expected TypeError Exception"
486
+ except TypeError:
487
+ pass
488
+
489
+ write(
490
+ settings_path,
491
+ name="John Doe",
492
+ names=["John", "Doe"],
493
+ age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
494
+ age=50,
495
+ alive=None,
496
+ years=(1973, 2073),
497
+ stuff=[],
498
+ info={},
499
+ )
500
+ try:
501
+ test_settings = Settings(settings_path).parse(MockSettings)
502
+ assert False, "Expected TypeError Exception"
503
+ except TypeError:
504
+ pass
505
+
506
+ write(
507
+ settings_path,
508
+ name="John Doe",
509
+ names=["John", "Doe"],
510
+ age_names={0: "Newborn", 50: "Middle age", 100: "Old"},
511
+ stuff=[],
512
+ info={},
513
+ )
514
+ try:
515
+ test_settings = Settings(settings_path).parse(MockSettings)
516
+ assert False, "Expected KeyError Exception"
517
+ except KeyError:
518
+ pass
519
+
520
+ try:
521
+ test_settings = Settings(settings_path).parse(Settings)
522
+ assert False, "Expected TypeError Exception"
523
+ except TypeError:
524
+ pass
525
+
526
+
293
527
  if __name__ == "__main__":
528
+ test_parse()
294
529
  test_secret()
295
530
  test_environ_values()
296
531
  test_cli_env_in_yaml()
File without changes
File without changes