devopsdriver 0.1.52__tar.gz → 0.1.53__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/PKG-INFO +7 -7
  2. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/README.md +2 -2
  3. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/__init__.py +1 -2
  4. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/__init__.py +1 -1
  5. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/azureobject.py +0 -1
  6. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/client.py +0 -1
  7. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/clients.py +0 -2
  8. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/client.py +0 -1
  9. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/log.py +1 -3
  10. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/pipeline.py +0 -1
  11. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/run.py +0 -1
  12. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/timestamp.py +0 -1
  13. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/client.py +0 -1
  14. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/wiql.py +10 -2
  15. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/dataobject.py +0 -1
  16. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/github/client.py +0 -1
  17. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/manage_settings.py +3 -4
  18. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/sendmail.py +0 -2
  19. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/settings.py +99 -8
  20. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/template.py +0 -1
  21. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/PKG-INFO +7 -7
  22. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/requires.txt +4 -4
  23. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/pyproject.toml +4 -4
  24. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_workitem_wiql.py +43 -1
  25. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_settings.py +242 -7
  26. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/LICENSE +0 -0
  27. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/__init__.py +0 -0
  28. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/builds/build.py +0 -0
  29. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/pipeline/__init__.py +0 -0
  30. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/azdo/workitem/__init__.py +0 -0
  31. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/github/__init__.py +0 -0
  32. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver/templates/manage_settings.txt.mako +0 -0
  33. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/SOURCES.txt +0 -0
  34. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/dependency_links.txt +0 -0
  35. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/entry_points.txt +0 -0
  36. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/devopsdriver.egg-info/top_level.txt +0 -0
  37. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/setup.cfg +0 -0
  38. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_azureobject.py +0 -0
  39. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_build.py +0 -0
  40. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_build_client.py +0 -0
  41. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_clients.py +0 -0
  42. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_pipeline.py +0 -0
  43. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_pipeline_client.py +0 -0
  44. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_pipeline_run.py +0 -0
  45. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_timestamp.py +0 -0
  46. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_azure_workitem_client.py +0 -0
  47. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_dataobject.py +0 -0
  48. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_manage_settings.py +0 -0
  49. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_sendmail.py +0 -0
  50. {devopsdriver-0.1.52 → devopsdriver-0.1.53}/tests/test_template.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devopsdriver
3
- Version: 0.1.52
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.8.1
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.52&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.52/)
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.52&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.52/)
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.52"
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
@@ -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
@@ -19,7 +19,6 @@ ORDER BY [System.ChangedDate] DESC
19
19
  ASOF '02-11-2020'
20
20
  """
21
21
 
22
-
23
22
  from datetime import datetime, date
24
23
 
25
24
 
@@ -145,6 +144,13 @@ class Compare: # pylint: disable=too-few-public-methods
145
144
  return f"{str(self.left)} {self.operator} {str(self.right)}"
146
145
 
147
146
 
147
+ class Under(Compare): # pylint: disable=too-few-public-methods
148
+ """checks for under, like area path or iteration path"""
149
+
150
+ def __init__(self, field_name: Field | str, value: Value | str):
151
+ super().__init__(field_name, value, "UNDER")
152
+
153
+
148
154
  class In(Compare): # pylint: disable=too-few-public-methods
149
155
  """checks for field in a list of values"""
150
156
 
@@ -294,7 +300,9 @@ class Wiql:
294
300
  return self
295
301
 
296
302
  def mode(self, results_mode: str):
297
- """Sets the mode for link queries"""
303
+ """Sets the mode for link queries
304
+ results_mode may be one of: MustContain, MayContain, DoesNotContain, Recursive, None
305
+ """
298
306
  self.mode_type = results_mode
299
307
  return self
300
308
 
@@ -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
@@ -75,7 +75,8 @@ api:
75
75
  johndoe and janedoe share the same password, so you just need to update the `user`
76
76
  """
77
77
 
78
-
78
+ from __future__ import annotations
79
+ from dataclasses import MISSING, fields, is_dataclass
79
80
  from json import load
80
81
  from os.path import dirname, basename, splitext, join
81
82
  from os import environ as os_environ, makedirs as os_makedirs
@@ -83,22 +84,81 @@ from re import compile as regex
83
84
  from platform import system as os_system
84
85
  from sys import argv as sys_argv
85
86
  from getpass import getpass as os_getpass
87
+ from typing import Any, TypeVar, Union, cast, get_args, get_origin, get_type_hints
86
88
 
87
89
  from yaml import safe_load
88
90
  from keyring import get_password, set_password
89
91
  from keyring.backends import fail
90
92
 
91
-
92
93
  # for testing
93
94
  ENVIRON = os_environ
94
95
  ARGV = sys_argv
95
- SYSTEM = os_system
96
- MAKEDIRS = os_makedirs
96
+ SYSTEM = os_system # pylint: disable=invalid-name
97
+ MAKEDIRS = os_makedirs # pylint: disable=invalid-name
97
98
  SHARED = "devopsdriver"
98
- PRINT = print
99
- GET_PASSWORD = get_password
100
- SET_PASSWORD = set_password
101
- GET_PASS = os_getpass
99
+ PRINT = print # pylint: disable=invalid-name
100
+ GET_PASSWORD = get_password # pylint: disable=invalid-name
101
+ SET_PASSWORD = set_password # pylint: disable=invalid-name
102
+ GET_PASS = os_getpass # pylint: disable=invalid-name
103
+ T = TypeVar("T")
104
+
105
+
106
+ def _is_instance_of_type( # pylint: disable=too-many-branches, too-many-return-statements
107
+ value: Any, expected: Any
108
+ ) -> bool:
109
+ """Check if a value matches an expected type, including support for common typing constructs."""
110
+ if expected is Any:
111
+ return True
112
+
113
+ origin = get_origin(expected)
114
+ args = get_args(expected)
115
+
116
+ if origin is Union: # Optional[T] / Union[T1, T2, ...]
117
+ return any(_is_instance_of_type(value, t) for t in args)
118
+
119
+ if origin is list: # list[T]
120
+ if not isinstance(value, list):
121
+ return False
122
+
123
+ if not args:
124
+ return True
125
+
126
+ item_type = args[0]
127
+ return all(_is_instance_of_type(v, item_type) for v in value)
128
+
129
+ if origin is dict: # dict[K, V]
130
+ if not isinstance(value, dict):
131
+ return False
132
+
133
+ if len(args) != 2:
134
+ return True
135
+
136
+ key_type, val_type = args
137
+ return all(
138
+ _is_instance_of_type(k, key_type) and _is_instance_of_type(v, val_type)
139
+ for k, v in value.items()
140
+ )
141
+
142
+ if origin is tuple: # tuple[T1, T2] or tuple[T, ...]
143
+ if not isinstance(value, tuple):
144
+ return False
145
+
146
+ if not args:
147
+ return True
148
+
149
+ if len(args) == 2 and args[1] is Ellipsis:
150
+ return all(_is_instance_of_type(v, args[0]) for v in value)
151
+
152
+ if len(value) != len(args):
153
+ return False
154
+
155
+ return all(_is_instance_of_type(v, t) for v, t in zip(value, args))
156
+
157
+ try: # Fallback to normal isinstance
158
+ return isinstance(value, expected)
159
+
160
+ except TypeError:
161
+ return False # Covers unsupported runtime checks for some typing constructs
102
162
 
103
163
 
104
164
  def load_json(path: str) -> dict:
@@ -225,6 +285,37 @@ class Settings:
225
285
  """
226
286
  return self.__bypass(key, name, self.environ)
227
287
 
288
+ def parse(self, cls: type[T]) -> T:
289
+ """Extract settings into a dataclass for type safety"""
290
+ if not is_dataclass(cls):
291
+ raise TypeError(f"{cls} must be a dataclass type")
292
+
293
+ type_hints = get_type_hints(cls)
294
+ kwargs = {}
295
+
296
+ for f in fields(cls):
297
+ expected_type = type_hints.get(f.name, Any)
298
+
299
+ # Pull from settings if present, otherwise use dataclass default/default_factory
300
+ if self.has(f.name):
301
+ value = self.get(f.name)
302
+ elif f.default is not MISSING:
303
+ value = f.default
304
+ elif f.default_factory is not MISSING:
305
+ value = f.default_factory()
306
+ else:
307
+ raise KeyError(f"Missing required setting: {f.name}")
308
+
309
+ if not _is_instance_of_type(value, expected_type):
310
+ raise TypeError(
311
+ f"Invalid type for '{f.name}': expected {expected_type}, "
312
+ + f"got {type(value).__name__}"
313
+ )
314
+
315
+ kwargs[f.name] = value
316
+
317
+ return cast(T, cls(**kwargs))
318
+
228
319
  @staticmethod
229
320
  def __patch_instance(key: str) -> str:
230
321
  for env_key, value in ENVIRON.items():
@@ -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.52
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.8.1
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.52&color=active&style=plastic)](https://pypi.org/project/devopsdriver/0.1.52/)
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.8.1
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.8.1",
13
+ "Mako==1.3.11",
14
+ "PyGithub==2.9.1",
15
15
  ]
16
16
  keywords = [
17
17
  "azure",
@@ -6,7 +6,7 @@ from datetime import date, datetime
6
6
 
7
7
  from devopsdriver.azdo import Wiql
8
8
  from devopsdriver.azdo import Ascending, Descending, Value
9
- from devopsdriver.azdo import IsEmpty, IsNotEmpty, And, Or, In, NotIn
9
+ from devopsdriver.azdo import IsEmpty, IsNotEmpty, And, Or, In, NotIn, Under
10
10
  from devopsdriver.azdo import GreaterThan, LessThan, Equal, NotEqual
11
11
  from devopsdriver.azdo import GreaterThanOrEqual, LessThanOrEqual
12
12
 
@@ -78,7 +78,49 @@ def test_in_and_not_in() -> None:
78
78
  assert str(builder) == expected, str(builder)
79
79
 
80
80
 
81
+ def test_under() -> None:
82
+ """Test under operator"""
83
+ builder = Wiql().where(Under("System.AreaPath", "/Project/Team/Product"))
84
+ expected = (
85
+ """SELECT [System.Id] FROM WorkItems WHERE [System.AreaPath] """
86
+ + """UNDER "/Project/Team/Product\""""
87
+ )
88
+ assert str(builder) == expected, str(builder)
89
+
90
+
91
+ def test_from() -> None:
92
+ """Test from"""
93
+ builder = (
94
+ Wiql()
95
+ .from_source("Wiki")
96
+ .where(Under("System.AreaPath", "/Project/Team/Product"))
97
+ )
98
+ expected = (
99
+ """SELECT [System.Id] FROM Wiki WHERE [System.AreaPath] """
100
+ + """UNDER "/Project/Team/Product\""""
101
+ )
102
+ assert str(builder) == expected, str(builder)
103
+
104
+
105
+ def test_mode() -> None:
106
+ """Test mode"""
107
+ builder = (
108
+ Wiql()
109
+ .mode("Recursive")
110
+ .where(Under("System.AreaPath", "/Project/Team/Product"))
111
+ )
112
+ expected = (
113
+ """SELECT [System.Id] FROM WorkItems WHERE [System.AreaPath] """
114
+ + """UNDER "/Project/Team/Product" """
115
+ + """MODE(Recursive)"""
116
+ )
117
+ assert str(builder) == expected, [str(builder), expected]
118
+
119
+
81
120
  if __name__ == "__main__":
121
+ test_under()
122
+ test_from()
123
+ test_mode()
82
124
  test_in_and_not_in()
83
125
  test_invalid_value_type()
84
126
  test_expressions()
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- """ 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