devopsdriver 0.1.37__tar.gz → 0.1.39__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.
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/PKG-INFO +27 -8
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/README.md +19 -7
- devopsdriver-0.1.39/devopsdriver/__init__.py +5 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver/azdo/__init__.py +2 -1
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver/azdo/clients.py +3 -0
- devopsdriver-0.1.39/devopsdriver/azdo/timestamp.py +80 -0
- devopsdriver-0.1.39/devopsdriver/azdo/workitem/__init__.py +1 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver/azdo/workitem/client.py +3 -1
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver/azdo/workitem/wiql.py +1 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver/azdo/workitem/workitem.py +18 -3
- devopsdriver-0.1.39/devopsdriver/manage_settings.py +55 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver/sendmail.py +4 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver/settings.py +3 -36
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver/template.py +2 -0
- devopsdriver-0.1.39/devopsdriver/templates/manage_settings.txt.mako +14 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver.egg-info/PKG-INFO +27 -8
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver.egg-info/SOURCES.txt +6 -0
- devopsdriver-0.1.39/devopsdriver.egg-info/entry_points.txt +2 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver.egg-info/requires.txt +10 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/pyproject.toml +17 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/tests/test_azure_clients.py +1 -1
- devopsdriver-0.1.39/tests/test_azure_timestamp.py +247 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/tests/test_azure_workitem.py +18 -9
- devopsdriver-0.1.39/tests/test_manage_settings.py +80 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/tests/test_sendmail.py +1 -1
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/tests/test_settings.py +1 -44
- devopsdriver-0.1.37/devopsdriver/__init__.py +0 -10
- devopsdriver-0.1.37/devopsdriver/azdo/workitem/__init__.py +0 -4
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/LICENSE +0 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver.egg-info/dependency_links.txt +0 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/devopsdriver.egg-info/top_level.txt +0 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/setup.cfg +0 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/tests/test_azure_workitem_client.py +0 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/tests/test_azure_workitem_wiql.py +0 -0
- {devopsdriver-0.1.37 → devopsdriver-0.1.39}/tests/test_template.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: devopsdriver
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.39
|
|
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.
|
|
@@ -51,9 +51,16 @@ Requires-Dist: keyring==25.0.0
|
|
|
51
51
|
Requires-Dist: setuptools==69.0.2
|
|
52
52
|
Requires-Dist: azure-devops==7.1.0b4
|
|
53
53
|
Requires-Dist: Mako==1.3.2
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: black>=24.3.0; extra == "dev"
|
|
56
|
+
Requires-Dist: pylint>=3.1.0; extra == "dev"
|
|
57
|
+
Provides-Extra: test
|
|
58
|
+
Requires-Dist: pytest>=8.1.1; extra == "test"
|
|
59
|
+
Requires-Dist: coverage>=7.4.4; extra == "test"
|
|
60
|
+
Provides-Extra: doc
|
|
54
61
|
|
|
55
62
|

|
|
56
|
-
[](https://pypi.org/project/devopsdriver/0.1.39/)
|
|
57
64
|
[](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
|
|
58
65
|
[](https://github.com/marcpage/devops-driver/graphs/contributors)
|
|
59
66
|
[](http://makeapullrequest.com)
|
|
@@ -84,7 +91,16 @@ OS:
|
|
|
84
91
|
|
|
85
92
|
Devops-driver is a collection of tools to help streamline developer's experience and gain insights into various processes.
|
|
86
93
|
|
|
87
|
-
##
|
|
94
|
+
## Tools
|
|
95
|
+
|
|
96
|
+
devopsdriver is a toolbox that helps to quickly put together reports. Some of the items in the toolbox are:
|
|
97
|
+
|
|
98
|
+
- **Settings**: store data, constants, settings, keys, tokens, etc. both in and out of the repository
|
|
99
|
+
- **send_email**: send emails over SMTP (including SSL), using `Settings` to store credentials
|
|
100
|
+
- **Template**: Simplify generating reports using `.mako` templates
|
|
101
|
+
- **Azure.workitem**: Search for and inspect Azure DevOps work items
|
|
102
|
+
|
|
103
|
+
## Example use-case
|
|
88
104
|
|
|
89
105
|
To allow seamless work in both pipelines as well as in the development environment, the `Settings` object gives you access to common settings among multiple scripts, secrets, and configuration constants in a way the helps keep secrets out of the repository but runs just as well in the pipeline as your machine.
|
|
90
106
|
|
|
@@ -101,7 +117,6 @@ secrets:
|
|
|
101
117
|
```
|
|
102
118
|
|
|
103
119
|
This file is in a global place (location varies by OS) and stores information that you may not want in your repository or is specific to development.
|
|
104
|
-
The `secrets` are extra sensative and are stored in the OS keychain.
|
|
105
120
|
|
|
106
121
|
| Platform | Global Directory |
|
|
107
122
|
|----------|------------------------|
|
|
@@ -109,26 +124,28 @@ The `secrets` are extra sensative and are stored in the OS keychain.
|
|
|
109
124
|
| Linux | ~/.devopsdriver/ |
|
|
110
125
|
| macOS | ~/Library/Preferences/ |
|
|
111
126
|
|
|
127
|
+
The `secrets` are extra sensative and are stored in the OS keychain.
|
|
128
|
+
|
|
112
129
|
### Set secrets in the keychain
|
|
113
130
|
```bash
|
|
114
131
|
$ python3 -m venv .venv
|
|
115
132
|
$ source .venv/bin/activate
|
|
116
133
|
$ pip install devopsdriver
|
|
117
|
-
$
|
|
134
|
+
$ settings --secrets
|
|
118
135
|
secret: smtp.password key: smtp/password
|
|
119
136
|
smtp.password (smtp/password): ****
|
|
120
137
|
secret: azure.token key: azure/token
|
|
121
138
|
smtp.password (azure/token): ****
|
|
122
|
-
$
|
|
139
|
+
$ settings --secrets
|
|
123
140
|
secret: azure.token key: azure/token
|
|
124
141
|
Value set
|
|
125
142
|
secret: smtp.password key: smtp/password
|
|
126
143
|
Value set
|
|
127
144
|
$
|
|
128
145
|
```
|
|
129
|
-
The first call to `
|
|
146
|
+
The first call to `settings` will look for every secret and check if they are already set in the keychain.
|
|
130
147
|
For any secret that has not been set in the keychain, you will be prompted to enter the password to store.
|
|
131
|
-
The second call to `
|
|
148
|
+
The second call to `settings` will verify that all the values have been set in the keychain.
|
|
132
149
|
|
|
133
150
|
### devopsdriver.yml
|
|
134
151
|
```yaml
|
|
@@ -174,6 +191,8 @@ These are values that you want to use in your script but have them here for easy
|
|
|
174
191
|
</ul>
|
|
175
192
|
```
|
|
176
193
|
|
|
194
|
+
This file is the template for the email body.
|
|
195
|
+
|
|
177
196
|
### new_stories.py
|
|
178
197
|
```python
|
|
179
198
|
from datetime import date, timedelta
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|

|
|
2
|
-
[](https://pypi.org/project/devopsdriver/0.1.39/)
|
|
3
3
|
[](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
|
|
4
4
|
[](https://github.com/marcpage/devops-driver/graphs/contributors)
|
|
5
5
|
[](http://makeapullrequest.com)
|
|
@@ -30,7 +30,16 @@ OS:
|
|
|
30
30
|
|
|
31
31
|
Devops-driver is a collection of tools to help streamline developer's experience and gain insights into various processes.
|
|
32
32
|
|
|
33
|
-
##
|
|
33
|
+
## Tools
|
|
34
|
+
|
|
35
|
+
devopsdriver is a toolbox that helps to quickly put together reports. Some of the items in the toolbox are:
|
|
36
|
+
|
|
37
|
+
- **Settings**: store data, constants, settings, keys, tokens, etc. both in and out of the repository
|
|
38
|
+
- **send_email**: send emails over SMTP (including SSL), using `Settings` to store credentials
|
|
39
|
+
- **Template**: Simplify generating reports using `.mako` templates
|
|
40
|
+
- **Azure.workitem**: Search for and inspect Azure DevOps work items
|
|
41
|
+
|
|
42
|
+
## Example use-case
|
|
34
43
|
|
|
35
44
|
To allow seamless work in both pipelines as well as in the development environment, the `Settings` object gives you access to common settings among multiple scripts, secrets, and configuration constants in a way the helps keep secrets out of the repository but runs just as well in the pipeline as your machine.
|
|
36
45
|
|
|
@@ -47,7 +56,6 @@ secrets:
|
|
|
47
56
|
```
|
|
48
57
|
|
|
49
58
|
This file is in a global place (location varies by OS) and stores information that you may not want in your repository or is specific to development.
|
|
50
|
-
The `secrets` are extra sensative and are stored in the OS keychain.
|
|
51
59
|
|
|
52
60
|
| Platform | Global Directory |
|
|
53
61
|
|----------|------------------------|
|
|
@@ -55,26 +63,28 @@ The `secrets` are extra sensative and are stored in the OS keychain.
|
|
|
55
63
|
| Linux | ~/.devopsdriver/ |
|
|
56
64
|
| macOS | ~/Library/Preferences/ |
|
|
57
65
|
|
|
66
|
+
The `secrets` are extra sensative and are stored in the OS keychain.
|
|
67
|
+
|
|
58
68
|
### Set secrets in the keychain
|
|
59
69
|
```bash
|
|
60
70
|
$ python3 -m venv .venv
|
|
61
71
|
$ source .venv/bin/activate
|
|
62
72
|
$ pip install devopsdriver
|
|
63
|
-
$
|
|
73
|
+
$ settings --secrets
|
|
64
74
|
secret: smtp.password key: smtp/password
|
|
65
75
|
smtp.password (smtp/password): ****
|
|
66
76
|
secret: azure.token key: azure/token
|
|
67
77
|
smtp.password (azure/token): ****
|
|
68
|
-
$
|
|
78
|
+
$ settings --secrets
|
|
69
79
|
secret: azure.token key: azure/token
|
|
70
80
|
Value set
|
|
71
81
|
secret: smtp.password key: smtp/password
|
|
72
82
|
Value set
|
|
73
83
|
$
|
|
74
84
|
```
|
|
75
|
-
The first call to `
|
|
85
|
+
The first call to `settings` will look for every secret and check if they are already set in the keychain.
|
|
76
86
|
For any secret that has not been set in the keychain, you will be prompted to enter the password to store.
|
|
77
|
-
The second call to `
|
|
87
|
+
The second call to `settings` will verify that all the values have been set in the keychain.
|
|
78
88
|
|
|
79
89
|
### devopsdriver.yml
|
|
80
90
|
```yaml
|
|
@@ -120,6 +130,8 @@ These are values that you want to use in your script but have them here for easy
|
|
|
120
130
|
</ul>
|
|
121
131
|
```
|
|
122
132
|
|
|
133
|
+
This file is the template for the email body.
|
|
134
|
+
|
|
123
135
|
### new_stories.py
|
|
124
136
|
```python
|
|
125
137
|
from datetime import date, timedelta
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# re export symbols for easier use
|
|
4
4
|
from .clients import Azure
|
|
5
|
+
from .timestamp import Timestamp
|
|
5
6
|
|
|
6
|
-
from .workitem import WorkItem
|
|
7
|
+
from .workitem.workitem import WorkItem
|
|
7
8
|
from .workitem.wiql import Wiql, Value, Field
|
|
8
9
|
from .workitem.wiql import Ascending, Descending, And, Or
|
|
9
10
|
from .workitem.wiql import Equal, NotEqual, LessThanOrEqual, GreaterThanOrEqual
|
|
@@ -7,12 +7,15 @@ API Documented here:
|
|
|
7
7
|
https://github.com/microsoft/azure-devops-python-api
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
from azure.devops.connection import Connection as AzureConnection
|
|
11
12
|
from msrest.authentication import BasicAuthentication as MSBasicAuthentication
|
|
12
13
|
|
|
13
14
|
from devopsdriver.settings import Settings
|
|
14
15
|
from devopsdriver.azdo.workitem.client import Client as WIClient
|
|
15
16
|
|
|
17
|
+
|
|
18
|
+
# for testing
|
|
16
19
|
CONNECTION = AzureConnection
|
|
17
20
|
AUTHENTICATION = MSBasicAuthentication
|
|
18
21
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
""" Tools that help when working with Azure """
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Timestamp:
|
|
11
|
+
"""An Azure timestamp"""
|
|
12
|
+
|
|
13
|
+
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
14
|
+
US_PER_MS = 1000 # microseconds per millisecond
|
|
15
|
+
US_PER_SEC = 1000 * US_PER_MS # microseconds per second
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def is_timestamp(value: any) -> bool:
|
|
19
|
+
"""Determines if the value is a timestamp string
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
value (any): The value to check
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
bool: True if the value is an Azure timestamp
|
|
26
|
+
"""
|
|
27
|
+
if not isinstance(value, str):
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
if not value.endswith("Z"):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
Timestamp.__parse_string(value)
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
except ValueError:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
def __init__(self, value: datetime | str | float | int):
|
|
41
|
+
if isinstance(value, datetime):
|
|
42
|
+
self.value = value
|
|
43
|
+
|
|
44
|
+
elif isinstance(value, str):
|
|
45
|
+
self.value = Timestamp.__parse_string(value)
|
|
46
|
+
|
|
47
|
+
elif isinstance(value, (int, float)):
|
|
48
|
+
self.value = datetime.fromtimestamp(value, tz=timezone.utc)
|
|
49
|
+
|
|
50
|
+
def __str__(self) -> str:
|
|
51
|
+
return self.to_string()
|
|
52
|
+
|
|
53
|
+
def to_string(self) -> str:
|
|
54
|
+
"""Returns the Azure formatted timestamp
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
str: The correctly formatted string
|
|
58
|
+
"""
|
|
59
|
+
milliseconds = f"{self.value.microsecond / Timestamp.US_PER_MS:03.0f}".rstrip(
|
|
60
|
+
"0"
|
|
61
|
+
)
|
|
62
|
+
return f"{self.value.strftime(Timestamp.DATE_FORMAT)}.{milliseconds}Z"
|
|
63
|
+
|
|
64
|
+
def to_timestamp(self) -> float:
|
|
65
|
+
"""Converts to a number to use with time.time()
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
float: The number of seconds since the epoch
|
|
69
|
+
"""
|
|
70
|
+
return datetime.timestamp(self.value)
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def __parse_string(timestamp: str) -> datetime:
|
|
74
|
+
assert timestamp.endswith("Z"), timestamp
|
|
75
|
+
whole, fractional = timestamp.rsplit(".", 1)
|
|
76
|
+
result = datetime.strptime(whole, Timestamp.DATE_FORMAT)
|
|
77
|
+
fractional_seconds = float(f"0.{fractional[:-1].ljust(3, '0')}")
|
|
78
|
+
return result.replace(
|
|
79
|
+
microsecond=int(fractional_seconds * Timestamp.US_PER_SEC)
|
|
80
|
+
).replace(tzinfo=timezone.utc)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
""" init workitem module """
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
""" Azure WorkItem Client """
|
|
4
5
|
|
|
6
|
+
|
|
5
7
|
from azure.devops.v7_1.work_item_tracking.models import Wiql as AzureWiql
|
|
6
8
|
from azure.devops.v7_1.work_item_tracking.models import WorkItem as AzureWorkItem
|
|
7
9
|
from azure.devops.v7_1.work_item_tracking.models import TeamContext
|
|
8
10
|
from azure.devops.v7_1.work_item_tracking.models import WorkItemQueryResult
|
|
9
|
-
from devopsdriver.azdo.workitem import WorkItem
|
|
11
|
+
from devopsdriver.azdo.workitem.workitem import WorkItem
|
|
10
12
|
from devopsdriver.azdo.workitem.wiql import Wiql
|
|
11
13
|
|
|
12
14
|
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
""" An Azure Devops WorkItem """
|
|
4
5
|
|
|
6
|
+
|
|
5
7
|
from typing import Any
|
|
8
|
+
|
|
6
9
|
from azure.devops.v7_1.work_item_tracking.models import WorkItem as AzureWorkItem
|
|
7
10
|
|
|
11
|
+
from devopsdriver.azdo.timestamp import Timestamp
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
class WorkItem: # pylint: disable=too-few-public-methods
|
|
10
15
|
"""Azure WorkItem"""
|
|
@@ -43,11 +48,21 @@ class WorkItem: # pylint: disable=too-few-public-methods
|
|
|
43
48
|
if "fields" in data:
|
|
44
49
|
return WorkItem._parse_field(name, data["fields"])
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
return None
|
|
47
52
|
|
|
48
53
|
class _Dict(dict):
|
|
49
54
|
def __getattr__(self, name: str) -> Any:
|
|
50
|
-
|
|
55
|
+
value = WorkItem._parse_field(name, self)
|
|
56
|
+
|
|
57
|
+
if Timestamp.is_timestamp(value):
|
|
58
|
+
return Timestamp(value)
|
|
59
|
+
|
|
60
|
+
return value
|
|
51
61
|
|
|
52
62
|
def __getattr__(self, name: str) -> Any:
|
|
53
|
-
|
|
63
|
+
value = WorkItem._parse_field(name, self.raw.as_dict())
|
|
64
|
+
|
|
65
|
+
if Timestamp.is_timestamp(value):
|
|
66
|
+
return Timestamp(value)
|
|
67
|
+
|
|
68
|
+
return value
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
""" Module Doc """
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from os.path import dirname, join
|
|
8
|
+
from sys import argv as sys_argv
|
|
9
|
+
from getpass import getpass as os_getpass
|
|
10
|
+
|
|
11
|
+
from keyring import set_password
|
|
12
|
+
|
|
13
|
+
from .settings import Settings
|
|
14
|
+
from .template import Template
|
|
15
|
+
|
|
16
|
+
ARGV = sys_argv
|
|
17
|
+
PRINT = print
|
|
18
|
+
SET_PASSWORD = set_password
|
|
19
|
+
GET_PASS = os_getpass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
"""Get settings values"""
|
|
24
|
+
args = list(ARGV[1:])
|
|
25
|
+
|
|
26
|
+
if not args or "--help" in args or "-h" in args:
|
|
27
|
+
PRINT(
|
|
28
|
+
Template(
|
|
29
|
+
__file__, join(dirname(__file__), "templates"), extension=".txt.mako"
|
|
30
|
+
).render()
|
|
31
|
+
)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
settings = Settings(__file__, dirname(dirname(__file__))).key("secrets")
|
|
35
|
+
|
|
36
|
+
if "--secrets" in args:
|
|
37
|
+
args.remove("--secrets")
|
|
38
|
+
|
|
39
|
+
for secret, key in settings.secrets.items():
|
|
40
|
+
PRINT(f"secret: {secret} key: {key}")
|
|
41
|
+
|
|
42
|
+
if not settings.has(secret):
|
|
43
|
+
value = GET_PASS(f"{secret} ({key}): ")
|
|
44
|
+
|
|
45
|
+
if value:
|
|
46
|
+
SET_PASSWORD(*Settings.split_key(key), value)
|
|
47
|
+
else:
|
|
48
|
+
PRINT("\tValue set")
|
|
49
|
+
|
|
50
|
+
for arg in args:
|
|
51
|
+
PRINT(f"{settings.get(arg)}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
""" Ability to send emails with embedded images """
|
|
5
|
+
|
|
6
|
+
|
|
4
7
|
from smtplib import SMTP as OS_SMTP, SMTP_SSL as OS_SMTP_SSL
|
|
5
8
|
from email.mime.multipart import MIMEMultipart as OS_MIMEMultipart
|
|
6
9
|
from email.mime.text import MIMEText as OS_MIMEText
|
|
@@ -8,6 +11,7 @@ from email.mime.image import MIMEImage as OS_MIMEImage
|
|
|
8
11
|
|
|
9
12
|
from devopsdriver.settings import Settings
|
|
10
13
|
|
|
14
|
+
|
|
11
15
|
IMAGE_HEADERS = {".png": b"\x89PNG\r\n\x1a\n", ".jpg": b"\xff\xd8\xff"}
|
|
12
16
|
|
|
13
17
|
# for testing
|
|
@@ -86,6 +86,8 @@ from getpass import getpass as os_getpass
|
|
|
86
86
|
|
|
87
87
|
from yaml import safe_load
|
|
88
88
|
from keyring import get_password, set_password
|
|
89
|
+
from keyring.backends import fail
|
|
90
|
+
|
|
89
91
|
|
|
90
92
|
# for testing
|
|
91
93
|
ENVIRON = os_environ
|
|
@@ -266,7 +268,7 @@ class Settings:
|
|
|
266
268
|
if key in self.secrets:
|
|
267
269
|
value = GET_PASSWORD(*Settings.split_key(self.secrets[key]))
|
|
268
270
|
|
|
269
|
-
if value is not None:
|
|
271
|
+
if value is not None and not isinstance(value, fail.Keyring):
|
|
270
272
|
return True if check else value
|
|
271
273
|
|
|
272
274
|
# Last check the files for settings
|
|
@@ -357,38 +359,3 @@ class Settings:
|
|
|
357
359
|
Settings.__merge(settings, contents)
|
|
358
360
|
|
|
359
361
|
return settings
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
def main() -> None:
|
|
363
|
-
"""Get settings values"""
|
|
364
|
-
args = list(ARGV[1:])
|
|
365
|
-
|
|
366
|
-
if not args or "--help" in args or "-h" in args:
|
|
367
|
-
PRINT("pass in settings to see if they are set and to what value")
|
|
368
|
-
PRINT("You can pass dotted names to get inner values, like smpt.server")
|
|
369
|
-
PRINT("You can pass --secrets to set keychain values that have not been set")
|
|
370
|
-
PRINT("You can also pass --help or -h to get this message")
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
settings = Settings(__file__, dirname(dirname(__file__))).key("secrets")
|
|
374
|
-
|
|
375
|
-
if "--secrets" in args:
|
|
376
|
-
args.remove("--secrets")
|
|
377
|
-
|
|
378
|
-
for secret, key in settings.secrets.items():
|
|
379
|
-
PRINT(f"secret: {secret} key: {key}")
|
|
380
|
-
|
|
381
|
-
if not settings.has(secret):
|
|
382
|
-
value = GET_PASS(f"{secret} ({key}): ")
|
|
383
|
-
|
|
384
|
-
if value:
|
|
385
|
-
SET_PASSWORD(*Settings.split_key(key), value)
|
|
386
|
-
else:
|
|
387
|
-
PRINT("\tValue set")
|
|
388
|
-
|
|
389
|
-
for arg in args:
|
|
390
|
-
PRINT(f"{settings.get(arg)}")
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if __name__ == "__main__":
|
|
394
|
-
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Usage:
|
|
2
|
+
|
|
3
|
+
settings [-h | --help]
|
|
4
|
+
settings --secrets
|
|
5
|
+
settings <value>...
|
|
6
|
+
|
|
7
|
+
-h --help Display this help information
|
|
8
|
+
|
|
9
|
+
--secrets Prompt for any secrets that have not been set in the keychain
|
|
10
|
+
For secrets that are set, confirms that the value is set
|
|
11
|
+
|
|
12
|
+
<value> A value to display the value for. For instance:
|
|
13
|
+
"smtp" will display all the smtp values (but not secrets in smtp, like password)
|
|
14
|
+
"smtp.server" will display just the smtp server
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: devopsdriver
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.39
|
|
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.
|
|
@@ -51,9 +51,16 @@ Requires-Dist: keyring==25.0.0
|
|
|
51
51
|
Requires-Dist: setuptools==69.0.2
|
|
52
52
|
Requires-Dist: azure-devops==7.1.0b4
|
|
53
53
|
Requires-Dist: Mako==1.3.2
|
|
54
|
+
Provides-Extra: dev
|
|
55
|
+
Requires-Dist: black>=24.3.0; extra == "dev"
|
|
56
|
+
Requires-Dist: pylint>=3.1.0; extra == "dev"
|
|
57
|
+
Provides-Extra: test
|
|
58
|
+
Requires-Dist: pytest>=8.1.1; extra == "test"
|
|
59
|
+
Requires-Dist: coverage>=7.4.4; extra == "test"
|
|
60
|
+
Provides-Extra: doc
|
|
54
61
|
|
|
55
62
|

|
|
56
|
-
[](https://pypi.org/project/devopsdriver/0.1.39/)
|
|
57
64
|
[](https://github.com/marcpage/devops-driver?tab=Unlicense-1-ov-file#readme)
|
|
58
65
|
[](https://github.com/marcpage/devops-driver/graphs/contributors)
|
|
59
66
|
[](http://makeapullrequest.com)
|
|
@@ -84,7 +91,16 @@ OS:
|
|
|
84
91
|
|
|
85
92
|
Devops-driver is a collection of tools to help streamline developer's experience and gain insights into various processes.
|
|
86
93
|
|
|
87
|
-
##
|
|
94
|
+
## Tools
|
|
95
|
+
|
|
96
|
+
devopsdriver is a toolbox that helps to quickly put together reports. Some of the items in the toolbox are:
|
|
97
|
+
|
|
98
|
+
- **Settings**: store data, constants, settings, keys, tokens, etc. both in and out of the repository
|
|
99
|
+
- **send_email**: send emails over SMTP (including SSL), using `Settings` to store credentials
|
|
100
|
+
- **Template**: Simplify generating reports using `.mako` templates
|
|
101
|
+
- **Azure.workitem**: Search for and inspect Azure DevOps work items
|
|
102
|
+
|
|
103
|
+
## Example use-case
|
|
88
104
|
|
|
89
105
|
To allow seamless work in both pipelines as well as in the development environment, the `Settings` object gives you access to common settings among multiple scripts, secrets, and configuration constants in a way the helps keep secrets out of the repository but runs just as well in the pipeline as your machine.
|
|
90
106
|
|
|
@@ -101,7 +117,6 @@ secrets:
|
|
|
101
117
|
```
|
|
102
118
|
|
|
103
119
|
This file is in a global place (location varies by OS) and stores information that you may not want in your repository or is specific to development.
|
|
104
|
-
The `secrets` are extra sensative and are stored in the OS keychain.
|
|
105
120
|
|
|
106
121
|
| Platform | Global Directory |
|
|
107
122
|
|----------|------------------------|
|
|
@@ -109,26 +124,28 @@ The `secrets` are extra sensative and are stored in the OS keychain.
|
|
|
109
124
|
| Linux | ~/.devopsdriver/ |
|
|
110
125
|
| macOS | ~/Library/Preferences/ |
|
|
111
126
|
|
|
127
|
+
The `secrets` are extra sensative and are stored in the OS keychain.
|
|
128
|
+
|
|
112
129
|
### Set secrets in the keychain
|
|
113
130
|
```bash
|
|
114
131
|
$ python3 -m venv .venv
|
|
115
132
|
$ source .venv/bin/activate
|
|
116
133
|
$ pip install devopsdriver
|
|
117
|
-
$
|
|
134
|
+
$ settings --secrets
|
|
118
135
|
secret: smtp.password key: smtp/password
|
|
119
136
|
smtp.password (smtp/password): ****
|
|
120
137
|
secret: azure.token key: azure/token
|
|
121
138
|
smtp.password (azure/token): ****
|
|
122
|
-
$
|
|
139
|
+
$ settings --secrets
|
|
123
140
|
secret: azure.token key: azure/token
|
|
124
141
|
Value set
|
|
125
142
|
secret: smtp.password key: smtp/password
|
|
126
143
|
Value set
|
|
127
144
|
$
|
|
128
145
|
```
|
|
129
|
-
The first call to `
|
|
146
|
+
The first call to `settings` will look for every secret and check if they are already set in the keychain.
|
|
130
147
|
For any secret that has not been set in the keychain, you will be prompted to enter the password to store.
|
|
131
|
-
The second call to `
|
|
148
|
+
The second call to `settings` will verify that all the values have been set in the keychain.
|
|
132
149
|
|
|
133
150
|
### devopsdriver.yml
|
|
134
151
|
```yaml
|
|
@@ -174,6 +191,8 @@ These are values that you want to use in your script but have them here for easy
|
|
|
174
191
|
</ul>
|
|
175
192
|
```
|
|
176
193
|
|
|
194
|
+
This file is the template for the email body.
|
|
195
|
+
|
|
177
196
|
### new_stories.py
|
|
178
197
|
```python
|
|
179
198
|
from datetime import date, timedelta
|
|
@@ -2,24 +2,30 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
devopsdriver/__init__.py
|
|
5
|
+
devopsdriver/manage_settings.py
|
|
5
6
|
devopsdriver/sendmail.py
|
|
6
7
|
devopsdriver/settings.py
|
|
7
8
|
devopsdriver/template.py
|
|
8
9
|
devopsdriver.egg-info/PKG-INFO
|
|
9
10
|
devopsdriver.egg-info/SOURCES.txt
|
|
10
11
|
devopsdriver.egg-info/dependency_links.txt
|
|
12
|
+
devopsdriver.egg-info/entry_points.txt
|
|
11
13
|
devopsdriver.egg-info/requires.txt
|
|
12
14
|
devopsdriver.egg-info/top_level.txt
|
|
13
15
|
devopsdriver/azdo/__init__.py
|
|
14
16
|
devopsdriver/azdo/clients.py
|
|
17
|
+
devopsdriver/azdo/timestamp.py
|
|
15
18
|
devopsdriver/azdo/workitem/__init__.py
|
|
16
19
|
devopsdriver/azdo/workitem/client.py
|
|
17
20
|
devopsdriver/azdo/workitem/wiql.py
|
|
18
21
|
devopsdriver/azdo/workitem/workitem.py
|
|
22
|
+
devopsdriver/templates/manage_settings.txt.mako
|
|
19
23
|
tests/test_azure_clients.py
|
|
24
|
+
tests/test_azure_timestamp.py
|
|
20
25
|
tests/test_azure_workitem.py
|
|
21
26
|
tests/test_azure_workitem_client.py
|
|
22
27
|
tests/test_azure_workitem_wiql.py
|
|
28
|
+
tests/test_manage_settings.py
|
|
23
29
|
tests/test_sendmail.py
|
|
24
30
|
tests/test_settings.py
|
|
25
31
|
tests/test_template.py
|
|
@@ -28,6 +28,23 @@ authors = [
|
|
|
28
28
|
{name = "Marc Page", email = "marcallenpage@gmail.com"},
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
+
[project.scripts]
|
|
32
|
+
settings = "devopsdriver.manage_settings:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.package-data]
|
|
35
|
+
"*" = ["*.mako"]
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"black>=24.3.0",
|
|
40
|
+
"pylint>=3.1.0",
|
|
41
|
+
]
|
|
42
|
+
test = [
|
|
43
|
+
"pytest>=8.1.1",
|
|
44
|
+
"coverage>=7.4.4",
|
|
45
|
+
]
|
|
46
|
+
doc = []
|
|
47
|
+
|
|
31
48
|
[project.urls]
|
|
32
49
|
Homepage = "https://github.com/marcpage/devops-driver"
|
|
33
50
|
Documentation = "https://github.com/marcpage/devops-driver"
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
""" Test Azure Timestamp """
|
|
4
|
+
|
|
5
|
+
from devopsdriver.azdo import Timestamp
|
|
6
|
+
|
|
7
|
+
TEST_TIMESTAMPS = [
|
|
8
|
+
"2023-11-16T03:24:40.36Z",
|
|
9
|
+
"2023-11-16T03:17:01.413Z",
|
|
10
|
+
"2023-11-16T05:09:11.62Z",
|
|
11
|
+
"2023-11-16T16:21:29.393Z",
|
|
12
|
+
"2023-11-16T16:21:48.88Z",
|
|
13
|
+
"2023-11-16T17:49:38.527Z",
|
|
14
|
+
"2023-11-16T22:47:07.257Z",
|
|
15
|
+
"2023-11-17T03:54:07.34Z",
|
|
16
|
+
"2023-11-18T22:31:38.557Z",
|
|
17
|
+
"2023-11-21T03:54:54.98Z",
|
|
18
|
+
"2023-11-21T17:12:58.44Z",
|
|
19
|
+
"2023-11-21T17:17:13.987Z",
|
|
20
|
+
"2023-11-21T17:35:06.58Z",
|
|
21
|
+
"2023-11-24T16:17:49.49Z",
|
|
22
|
+
"2023-11-24T16:18:09.48Z",
|
|
23
|
+
"2023-11-24T16:24:42.23Z",
|
|
24
|
+
"2023-11-24T19:00:29.217Z",
|
|
25
|
+
"2023-11-24T19:01:07.813Z",
|
|
26
|
+
"2023-11-29T03:18:24.333Z",
|
|
27
|
+
"2023-11-29T03:21:16.353Z",
|
|
28
|
+
"2023-11-29T03:21:20.96Z",
|
|
29
|
+
"2023-11-29T03:25:54.02Z",
|
|
30
|
+
"2023-11-29T03:28:59.403Z",
|
|
31
|
+
"2023-11-29T03:29:55.51Z",
|
|
32
|
+
"2023-11-29T03:44:19.653Z",
|
|
33
|
+
"2023-11-29T03:48:15.777Z",
|
|
34
|
+
"2023-11-29T15:49:26.967Z",
|
|
35
|
+
"2023-11-29T15:49:33.457Z",
|
|
36
|
+
"2023-11-29T15:54:59.17Z",
|
|
37
|
+
"2023-12-05T01:53:52.67Z",
|
|
38
|
+
"2023-12-05T01:54:03.417Z",
|
|
39
|
+
"2023-12-09T02:49:15.053Z",
|
|
40
|
+
"2023-12-09T02:53:44.95Z",
|
|
41
|
+
"2023-12-16T02:16:58.697Z",
|
|
42
|
+
"2024-01-15T15:10:41.83Z",
|
|
43
|
+
"2024-01-15T16:27:31.43Z",
|
|
44
|
+
"2024-01-15T19:16:11.38Z",
|
|
45
|
+
"2024-01-15T19:16:16.017Z",
|
|
46
|
+
"2024-01-16T21:21:48.23Z",
|
|
47
|
+
"2024-01-16T21:21:48.87Z",
|
|
48
|
+
"2024-01-16T21:21:54.127Z",
|
|
49
|
+
"2024-01-16T21:21:55.277Z",
|
|
50
|
+
"2024-01-16T21:21:57.223Z",
|
|
51
|
+
"2024-01-16T21:31:02.743Z",
|
|
52
|
+
"2024-02-01T15:12:12.277Z",
|
|
53
|
+
"2024-02-02T17:30:43.253Z",
|
|
54
|
+
"2024-02-02T17:31:18.313Z",
|
|
55
|
+
"2024-02-02T17:49:10.37Z",
|
|
56
|
+
"2024-02-02T17:51:32.537Z",
|
|
57
|
+
"2024-02-02T17:55:17.257Z",
|
|
58
|
+
"2024-02-05T03:13:24.16Z",
|
|
59
|
+
"2024-02-05T03:13:24.66Z",
|
|
60
|
+
"2024-02-05T03:15:24.9Z",
|
|
61
|
+
"2024-02-05T14:46:44.583Z",
|
|
62
|
+
"2024-02-05T14:47:54.803Z",
|
|
63
|
+
"2024-02-12T17:01:23.693Z",
|
|
64
|
+
"2024-02-12T17:04:20.5Z",
|
|
65
|
+
"2024-02-13T00:30:14.31Z",
|
|
66
|
+
"2024-02-13T00:55:01.35Z",
|
|
67
|
+
"2024-02-13T16:08:30.643Z",
|
|
68
|
+
"2024-02-13T16:46:34.79Z",
|
|
69
|
+
"2024-02-13T16:46:35.35Z",
|
|
70
|
+
"2024-02-15T20:41:26.767Z",
|
|
71
|
+
"2024-02-20T17:18:06.95Z",
|
|
72
|
+
"2024-02-20T17:18:07.49Z",
|
|
73
|
+
"2024-02-20T17:18:09.537Z",
|
|
74
|
+
"2024-02-21T15:11:34.86Z",
|
|
75
|
+
"2024-02-21T15:30:12.723Z",
|
|
76
|
+
"2024-02-21T15:30:28.887Z",
|
|
77
|
+
"2024-02-21T15:30:57.373Z",
|
|
78
|
+
"2024-02-21T15:36:03.137Z",
|
|
79
|
+
"2024-02-21T15:37:26.463Z",
|
|
80
|
+
"2024-02-21T16:53:53.29Z",
|
|
81
|
+
"2024-02-21T16:53:57.943Z",
|
|
82
|
+
"2024-02-21T17:55:39.323Z",
|
|
83
|
+
"2024-02-22T16:01:50.477Z",
|
|
84
|
+
"2024-02-22T16:02:19.69Z",
|
|
85
|
+
"2024-02-22T17:22:02.963Z",
|
|
86
|
+
"2024-02-22T17:22:05.7Z",
|
|
87
|
+
"2024-02-27T17:11:56.247Z",
|
|
88
|
+
"2024-03-03T21:11:41.707Z",
|
|
89
|
+
"2024-03-03T21:11:42.257Z",
|
|
90
|
+
"2024-03-03T21:17:57.317Z",
|
|
91
|
+
"2024-03-03T21:18:19.83Z",
|
|
92
|
+
"2024-03-03T21:19:16.68Z",
|
|
93
|
+
"2024-03-03T21:23:35.33Z",
|
|
94
|
+
"2024-03-08T20:26:21.927Z",
|
|
95
|
+
"2024-03-08T20:27:36.023Z",
|
|
96
|
+
"2024-03-08T21:46:49.617Z",
|
|
97
|
+
"2024-03-10T18:28:05.983Z",
|
|
98
|
+
"2024-03-10T20:18:28.353Z",
|
|
99
|
+
"2024-03-10T20:27:13.34Z",
|
|
100
|
+
"2024-03-10T20:28:19.893Z",
|
|
101
|
+
"2024-03-10T22:22:23.163Z",
|
|
102
|
+
"2024-03-11T00:00:10.72Z",
|
|
103
|
+
"2024-03-11T01:15:51.407Z",
|
|
104
|
+
"2024-03-11T01:15:57.817Z",
|
|
105
|
+
"2024-03-11T03:39:43.92Z",
|
|
106
|
+
"2024-03-11T15:46:50.733Z",
|
|
107
|
+
"2024-03-11T15:48:01.667Z",
|
|
108
|
+
"2024-03-11T15:58:51.747Z",
|
|
109
|
+
"2024-03-11T18:37:30.577Z",
|
|
110
|
+
"2024-03-11T18:50:51.663Z",
|
|
111
|
+
"2024-03-11T18:57:36.687Z",
|
|
112
|
+
"2024-03-11T19:14:03.663Z",
|
|
113
|
+
"2024-03-11T19:15:28.78Z",
|
|
114
|
+
"2024-03-11T22:52:00.85Z",
|
|
115
|
+
"2024-03-12T00:44:38.52Z",
|
|
116
|
+
"2024-03-12T00:45:25.517Z",
|
|
117
|
+
"2024-03-12T00:45:31.18Z",
|
|
118
|
+
"2024-03-12T00:46:11.873Z",
|
|
119
|
+
"2024-03-14T19:11:36.84Z",
|
|
120
|
+
"2024-03-14T21:21:55.313Z",
|
|
121
|
+
"2024-03-14T21:23:05.68Z",
|
|
122
|
+
"2024-03-14T21:23:06.013Z",
|
|
123
|
+
"2024-03-14T21:23:13.693Z",
|
|
124
|
+
"2024-03-14T21:24:06.643Z",
|
|
125
|
+
"2024-03-15T20:46:03.337Z",
|
|
126
|
+
"2024-03-15T20:46:07.973Z",
|
|
127
|
+
"2024-03-15T20:52:28.92Z",
|
|
128
|
+
"2024-03-15T21:01:55.407Z",
|
|
129
|
+
"2024-03-18T20:13:03.047Z",
|
|
130
|
+
"2024-03-19T02:46:45.227Z",
|
|
131
|
+
"2024-03-19T02:53:31.133Z",
|
|
132
|
+
"2024-03-19T02:58:25.87Z",
|
|
133
|
+
"2024-03-19T02:59:42.95Z",
|
|
134
|
+
"2024-03-19T03:00:17.017Z",
|
|
135
|
+
"2024-03-19T19:21:11.957Z",
|
|
136
|
+
"2024-03-19T21:13:49.46Z",
|
|
137
|
+
"2024-03-19T22:29:45.097Z",
|
|
138
|
+
"2024-03-20T14:52:04.92Z",
|
|
139
|
+
"2024-03-21T15:04:26.963Z",
|
|
140
|
+
"2024-03-21T15:04:59.003Z",
|
|
141
|
+
"2024-03-21T15:05:08.52Z",
|
|
142
|
+
"2024-03-21T15:26:09.337Z",
|
|
143
|
+
"2024-03-21T15:29:49.457Z",
|
|
144
|
+
"2024-03-21T15:30:33.85Z",
|
|
145
|
+
"2024-03-21T17:29:38.59Z",
|
|
146
|
+
"2024-03-21T18:07:55.047Z",
|
|
147
|
+
"2024-03-21T18:08:22.883Z",
|
|
148
|
+
"2024-03-22T17:23:07.13Z",
|
|
149
|
+
"2024-03-22T17:31:21.85Z",
|
|
150
|
+
"2024-03-22T21:55:56.293Z",
|
|
151
|
+
"2024-03-22T21:55:57.267Z",
|
|
152
|
+
"2024-03-23T16:04:49.38Z",
|
|
153
|
+
"2024-03-25T23:37:29.403Z",
|
|
154
|
+
"2024-03-25T23:37:45.44Z",
|
|
155
|
+
"2024-03-25T23:38:14.967Z",
|
|
156
|
+
"2024-03-26T00:01:26.593Z",
|
|
157
|
+
"2024-03-26T00:01:36.127Z",
|
|
158
|
+
"2024-03-26T15:55:04.947Z",
|
|
159
|
+
"2024-03-26T16:10:08.82Z",
|
|
160
|
+
"2024-03-26T16:24:48.657Z",
|
|
161
|
+
"2024-03-26T16:24:49.053Z",
|
|
162
|
+
"2024-03-26T16:24:53.573Z",
|
|
163
|
+
"2024-03-27T13:21:04.567Z",
|
|
164
|
+
"2024-03-27T13:32:11.08Z",
|
|
165
|
+
"2024-03-27T13:32:12.76Z",
|
|
166
|
+
"2024-03-27T13:33:08.953Z",
|
|
167
|
+
"2024-03-27T17:29:49.157Z",
|
|
168
|
+
"2024-03-27T17:37:58.327Z",
|
|
169
|
+
"2024-03-27T17:39:51.85Z",
|
|
170
|
+
"2024-03-27T17:40:09.95Z",
|
|
171
|
+
"2024-03-27T17:40:21.84Z",
|
|
172
|
+
"2024-03-28T15:15:34.357Z",
|
|
173
|
+
"2024-03-28T16:09:35.753Z",
|
|
174
|
+
"2024-03-28T16:27:05.56Z",
|
|
175
|
+
"2024-03-28T16:33:28.717Z",
|
|
176
|
+
"2024-03-28T16:33:35.71Z",
|
|
177
|
+
"2024-03-28T16:33:56.49Z",
|
|
178
|
+
"2024-03-28T16:35:01.127Z",
|
|
179
|
+
"2024-03-28T16:35:03.513Z",
|
|
180
|
+
"2024-03-28T16:35:11.627Z",
|
|
181
|
+
"2024-03-28T16:35:48.313Z",
|
|
182
|
+
"2024-03-28T16:36:11.28Z",
|
|
183
|
+
"2024-03-28T16:37:02.943Z",
|
|
184
|
+
"2024-03-28T16:37:30.85Z",
|
|
185
|
+
"2024-03-28T16:37:32.873Z",
|
|
186
|
+
"2024-03-28T16:42:45.65Z",
|
|
187
|
+
"2024-03-28T16:42:48.86Z",
|
|
188
|
+
"2024-03-28T16:42:51.15Z",
|
|
189
|
+
"2024-04-01T16:27:02.117Z",
|
|
190
|
+
"2024-04-02T01:31:12.047Z",
|
|
191
|
+
"2024-04-02T01:31:36.603Z",
|
|
192
|
+
"2024-04-02T01:34:48.52Z",
|
|
193
|
+
"2024-04-02T01:34:57.02Z",
|
|
194
|
+
"2024-04-02T01:35:44.64Z",
|
|
195
|
+
"2024-04-02T16:12:35.537Z",
|
|
196
|
+
"2024-04-02T16:12:44.213Z",
|
|
197
|
+
"2024-04-02T16:15:18.68Z",
|
|
198
|
+
"2024-04-02T16:21:59.76Z",
|
|
199
|
+
"2024-04-02T16:24:44.087Z",
|
|
200
|
+
"2024-04-02T16:24:52.65Z",
|
|
201
|
+
"2024-04-02T16:25:09.547Z",
|
|
202
|
+
"2024-04-02T16:25:55.57Z",
|
|
203
|
+
"2024-04-02T16:27:56.313Z",
|
|
204
|
+
"2024-04-02T16:28:00.403Z",
|
|
205
|
+
"2024-04-02T16:29:39.293Z",
|
|
206
|
+
"2024-04-02T16:32:20.54Z",
|
|
207
|
+
"2024-04-02T16:32:41.527Z",
|
|
208
|
+
"2024-04-02T16:42:20.413Z",
|
|
209
|
+
"2024-04-02T20:38:32.47Z",
|
|
210
|
+
"2024-04-02T20:39:42.337Z",
|
|
211
|
+
"2024-04-03T01:26:25.087Z",
|
|
212
|
+
"2024-04-04T16:04:50.917Z",
|
|
213
|
+
"2024-04-04T16:05:11.873Z",
|
|
214
|
+
"2024-04-04T16:05:17.573Z",
|
|
215
|
+
"2024-04-04T16:06:35.577Z",
|
|
216
|
+
"2024-04-04T16:07:22.947Z",
|
|
217
|
+
"2024-04-04T16:09:58.193Z",
|
|
218
|
+
"2024-04-04T16:10:06.467Z",
|
|
219
|
+
"2024-04-04T16:10:11.853Z",
|
|
220
|
+
"2024-04-04T16:10:28.763Z",
|
|
221
|
+
"2024-04-04T16:10:31.533Z",
|
|
222
|
+
"2024-04-04T16:15:19.473Z",
|
|
223
|
+
"2024-04-04T16:15:23.39Z",
|
|
224
|
+
"2024-04-04T16:16:09.86Z",
|
|
225
|
+
"2024-04-04T18:09:35.133Z",
|
|
226
|
+
"2024-04-04T23:12:30.663Z",
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_basic() -> None:
|
|
231
|
+
"""Test basic timestamp functionality"""
|
|
232
|
+
for timestamp_string in TEST_TIMESTAMPS:
|
|
233
|
+
value_under_test = Timestamp(timestamp_string)
|
|
234
|
+
assert (
|
|
235
|
+
str(value_under_test) == timestamp_string
|
|
236
|
+
), f"{str(value_under_test)} != {timestamp_string}"
|
|
237
|
+
timestamp = value_under_test.to_timestamp()
|
|
238
|
+
assert (
|
|
239
|
+
abs(timestamp - Timestamp(timestamp).to_timestamp()) < 0.001
|
|
240
|
+
), f"{timestamp} != {Timestamp(timestamp).to_timestamp()}"
|
|
241
|
+
assert (
|
|
242
|
+
abs(timestamp - Timestamp(value_under_test.value).to_timestamp()) < 0.001
|
|
243
|
+
), f"{timestamp} != {Timestamp(value_under_test.value).to_timestamp()}"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
if __name__ == "__main__":
|
|
247
|
+
test_basic()
|
|
@@ -21,7 +21,7 @@ class MockAzureWorkItem:
|
|
|
21
21
|
"System.Reason": "New",
|
|
22
22
|
"System.CreatedDate": "2023-11-16T03:12:32.94Z",
|
|
23
23
|
"System.CreatedBy": {
|
|
24
|
-
"displayName": "Edna Johnson",
|
|
24
|
+
"displayName": "Edna Johnson Z",
|
|
25
25
|
"url": "https://spsprodcus5.vssps.visualstudio.com/"
|
|
26
26
|
+ "A3eb27a26-75f2-40f9-87dc-cc10e8e565e4/_apis/Identities"
|
|
27
27
|
+ "/45fcf770-0670-69d4-8e48-3ae6e0bf9b5c",
|
|
@@ -40,7 +40,8 @@ class MockAzureWorkItem:
|
|
|
40
40
|
},
|
|
41
41
|
"System.ChangedDate": "2023-11-16T03:12:32.94Z",
|
|
42
42
|
"System.ChangedBy": {
|
|
43
|
-
"displayName": "Edna Johnson",
|
|
43
|
+
"displayName": "Edna Johnson .Z",
|
|
44
|
+
"changedOn": "2023-11-16T03:12:32.94Z",
|
|
44
45
|
"url": "https://spsprodcus5.vssps.visualstudio.com/"
|
|
45
46
|
+ "A3eb27a26-75f2-40f9-87dc-cc10e8e565e4/_apis/Identities/"
|
|
46
47
|
+ "45fcf770-0670-69d4-8e48-3ae6e0bf9b5c",
|
|
@@ -60,7 +61,7 @@ class MockAzureWorkItem:
|
|
|
60
61
|
"System.CommentCount": 0,
|
|
61
62
|
"System.TeamProject": "Creative",
|
|
62
63
|
"System.AreaPath": "Creative",
|
|
63
|
-
"System.IterationPath": "
|
|
64
|
+
"System.IterationPath": "2023-11-16T03:12:32.alphaZ",
|
|
64
65
|
"System.Title": "test",
|
|
65
66
|
"Microsoft.VSTS.Common.Priority": 2,
|
|
66
67
|
"Microsoft.VSTS.Common.ValueArea": "Business",
|
|
@@ -80,16 +81,24 @@ def test_workitem() -> None:
|
|
|
80
81
|
assert wi.ID == 5, wi.ID
|
|
81
82
|
assert wi.workitemtype == "User Story", wi.workitemtype
|
|
82
83
|
assert wi.system_workitemtype == "User Story", wi.system_workitemtype
|
|
83
|
-
assert wi.ChangedBy["displayName"] == "Edna Johnson", wi.changedBy
|
|
84
|
-
assert wi.changedby.displayname == "Edna Johnson", wi.changedby.displayname
|
|
85
84
|
assert wi.microsoft_vsts_common_priority == 2, wi.microsoft_vsts_common_priority
|
|
85
|
+
assert wi.not_a_field is None, wi.not_a_field
|
|
86
86
|
|
|
87
|
-
try:
|
|
88
|
-
_ = wi.not_a_field
|
|
89
87
|
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
def test_timestamp() -> None:
|
|
89
|
+
"""test timestamps"""
|
|
90
|
+
wi = WorkItem(MockAzureWorkItem())
|
|
91
|
+
assert wi.StateChangeDate.to_string() == "2023-11-16T03:12:32.94Z"
|
|
92
|
+
assert wi.CreatedDate.to_string() == "2023-11-16T03:12:32.94Z"
|
|
93
|
+
assert wi.ChangedDate.to_string() == "2023-11-16T03:12:32.94Z"
|
|
94
|
+
assert wi.IterationPath == "2023-11-16T03:12:32.alphaZ", wi.IterationPath
|
|
95
|
+
assert wi.ChangedBy["displayName"] == "Edna Johnson .Z", wi.changedBy["displayName"]
|
|
96
|
+
assert wi.createdBy.displayName == "Edna Johnson Z", wi.createdBy.displayName
|
|
97
|
+
assert (
|
|
98
|
+
wi.changedBy.changedOn.to_string() == "2023-11-16T03:12:32.94Z"
|
|
99
|
+
), wi.changedBy.changedOn
|
|
92
100
|
|
|
93
101
|
|
|
94
102
|
if __name__ == "__main__":
|
|
103
|
+
test_timestamp()
|
|
95
104
|
test_workitem()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
""" Test the manage_settings tool """
|
|
4
|
+
|
|
5
|
+
from os.path import join
|
|
6
|
+
from tempfile import TemporaryDirectory
|
|
7
|
+
from helpers import setup_settings, write
|
|
8
|
+
|
|
9
|
+
from devopsdriver import settings
|
|
10
|
+
from devopsdriver import manage_settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_main():
|
|
14
|
+
"""test the main entry point"""
|
|
15
|
+
with TemporaryDirectory() as working_dir:
|
|
16
|
+
setup_settings(shared="test", Linux=join(working_dir, "Linux"))
|
|
17
|
+
settings.ARGV = ["ignore", "test"]
|
|
18
|
+
manage_settings.ARGV = ["ignore", "test"]
|
|
19
|
+
storage = {}
|
|
20
|
+
|
|
21
|
+
def mock_print(message: str) -> None:
|
|
22
|
+
storage["print"] = message
|
|
23
|
+
|
|
24
|
+
settings.PRINT = mock_print
|
|
25
|
+
manage_settings.PRINT = mock_print
|
|
26
|
+
write(join(working_dir, "Linux", "test.yml"), test=3)
|
|
27
|
+
manage_settings.main()
|
|
28
|
+
assert storage["print"] == "3", storage
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_main_help():
|
|
32
|
+
"""test the main entry point"""
|
|
33
|
+
with TemporaryDirectory() as working_dir:
|
|
34
|
+
setup_settings(shared="test", Linux=join(working_dir, "Linux"))
|
|
35
|
+
manage_settings.PRINT = settings.PRINT
|
|
36
|
+
settings.ARGV = ["ignore", "--help"]
|
|
37
|
+
manage_settings.ARGV = settings.ARGV
|
|
38
|
+
storage = {}
|
|
39
|
+
|
|
40
|
+
def mock_print(message: str) -> None:
|
|
41
|
+
storage["print"] = message
|
|
42
|
+
|
|
43
|
+
settings.PRINT = mock_print
|
|
44
|
+
manage_settings.PRINT = mock_print
|
|
45
|
+
write(join(working_dir, "Linux", "test.yml"), test=3)
|
|
46
|
+
manage_settings.main()
|
|
47
|
+
assert storage["print"].startswith("Usage:"), storage
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_main_set_secret():
|
|
51
|
+
"""test the main entry point when settings keychain secrets"""
|
|
52
|
+
|
|
53
|
+
def set_password(s, n, p):
|
|
54
|
+
assert (
|
|
55
|
+
s in ("azure", "jira") and n == "token" and p == "setec astronomy"
|
|
56
|
+
), f"{s} {n} {p}"
|
|
57
|
+
|
|
58
|
+
with TemporaryDirectory() as working_dir:
|
|
59
|
+
setup_settings(shared="test", Linux=join(working_dir, "Linux"))
|
|
60
|
+
settings.ARGV = ["ignore", "--secrets"]
|
|
61
|
+
manage_settings.ARGV = settings.ARGV
|
|
62
|
+
settings.GET_PASSWORD = lambda s, n: (
|
|
63
|
+
"password" if f"{s}/{n}" == "azure/token" else None
|
|
64
|
+
)
|
|
65
|
+
settings.GET_PASS = lambda p: "setec astronomy"
|
|
66
|
+
manage_settings.GET_PASS = settings.GET_PASS
|
|
67
|
+
manage_settings.PRINT = settings.PRINT
|
|
68
|
+
settings.SET_PASSWORD = set_password
|
|
69
|
+
manage_settings.SET_PASSWORD = settings.SET_PASSWORD
|
|
70
|
+
write(
|
|
71
|
+
join(working_dir, "Linux", "test.yml"),
|
|
72
|
+
secrets={"azure.token": "azure/token", "jira.token": "jira/token"},
|
|
73
|
+
)
|
|
74
|
+
manage_settings.main()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
test_main()
|
|
79
|
+
test_main_help()
|
|
80
|
+
test_main_set_secret()
|
|
@@ -10,7 +10,7 @@ from itertools import product
|
|
|
10
10
|
from helpers import setup_settings, ensure, write
|
|
11
11
|
|
|
12
12
|
from devopsdriver import settings # debug access
|
|
13
|
-
from devopsdriver import Settings
|
|
13
|
+
from devopsdriver.settings import Settings
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def __setup_files(directory: str, dir1: str, dir2: str) -> None:
|
|
@@ -238,24 +238,6 @@ def test_environ_values():
|
|
|
238
238
|
assert opts["value"] == "testing ${noenv} for noenv", opts["value"]
|
|
239
239
|
|
|
240
240
|
|
|
241
|
-
def test_main():
|
|
242
|
-
"""test the main entry point"""
|
|
243
|
-
with TemporaryDirectory() as working_dir:
|
|
244
|
-
setup_settings(shared="test", Linux=join(working_dir, "Linux"))
|
|
245
|
-
settings.ARGV = ["ignore", "test"]
|
|
246
|
-
write(join(working_dir, "Linux", "test.yml"), test=3)
|
|
247
|
-
settings.main()
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def test_main_help():
|
|
251
|
-
"""test the main entry point"""
|
|
252
|
-
with TemporaryDirectory() as working_dir:
|
|
253
|
-
setup_settings(shared="test", Linux=join(working_dir, "Linux"))
|
|
254
|
-
settings.ARGV = ["ignore", "--help"]
|
|
255
|
-
write(join(working_dir, "Linux", "test.yml"), test=3)
|
|
256
|
-
settings.main()
|
|
257
|
-
|
|
258
|
-
|
|
259
241
|
def test_secret():
|
|
260
242
|
"""test os secret storage"""
|
|
261
243
|
with TemporaryDirectory() as working_dir:
|
|
@@ -274,33 +256,8 @@ def test_secret():
|
|
|
274
256
|
assert opts["password"] == "setec astronomy", opts["password"]
|
|
275
257
|
|
|
276
258
|
|
|
277
|
-
def test_main_set_secret():
|
|
278
|
-
"""test the main entry point when settings keychain secrets"""
|
|
279
|
-
|
|
280
|
-
def set_password(s, n, p):
|
|
281
|
-
assert (
|
|
282
|
-
s in ("azure", "jira") and n == "token" and p == "setec astronomy"
|
|
283
|
-
), f"{s} {n} {p}"
|
|
284
|
-
|
|
285
|
-
with TemporaryDirectory() as working_dir:
|
|
286
|
-
setup_settings(shared="test", Linux=join(working_dir, "Linux"))
|
|
287
|
-
settings.ARGV = ["ignore", "--secrets"]
|
|
288
|
-
settings.GET_PASSWORD = lambda s, n: (
|
|
289
|
-
"password" if f"{s}/{n}" == "azure/token" else None
|
|
290
|
-
)
|
|
291
|
-
settings.GET_PASS = lambda p: "setec astronomy"
|
|
292
|
-
settings.SET_PASSWORD = set_password
|
|
293
|
-
write(
|
|
294
|
-
join(working_dir, "Linux", "test.yml"),
|
|
295
|
-
secrets={"azure.token": "azure/token", "jira.token": "jira/token"},
|
|
296
|
-
)
|
|
297
|
-
settings.main()
|
|
298
|
-
|
|
299
|
-
|
|
300
259
|
if __name__ == "__main__":
|
|
301
|
-
test_main_set_secret()
|
|
302
260
|
test_secret()
|
|
303
|
-
test_main()
|
|
304
261
|
test_environ_values()
|
|
305
262
|
test_cli_env_in_yaml()
|
|
306
263
|
test_basic()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|