patcherctl 2.0__tar.gz → 2.0.2__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.
- {patcherctl-2.0 → patcherctl-2.0.2}/PKG-INFO +3 -2
- {patcherctl-2.0 → patcherctl-2.0.2}/README.md +2 -1
- {patcherctl-2.0 → patcherctl-2.0.2}/pyproject.toml +1 -1
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/__about__.py +1 -1
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/api_client.py +1 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/report_manager.py +8 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/patch.py +22 -4
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/animation.py +1 -1
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/data_manager.py +11 -5
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/PKG-INFO +3 -2
- patcherctl-2.0.2/src/patcherctl.egg-info/entry_points.txt +2 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_analyzer.py +9 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_data_manager.py +6 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_ios.py +6 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_ui.py +4 -4
- patcherctl-2.0/src/patcherctl.egg-info/entry_points.txt +0 -2
- {patcherctl-2.0 → patcherctl-2.0.2}/LICENSE.txt +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/setup.cfg +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/__init__.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/cli.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/__init__.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/analyze.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/config_manager.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/setup.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/token_manager.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/ui_manager.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/__init__.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/jamf_client.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/label.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/token.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/__init__.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/decorators.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/exceptions.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/installomator.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/logger.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/pdf_report.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/SOURCES.txt +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/dependency_links.txt +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/requires.txt +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/top_level.txt +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_api_fetching.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_base_api.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_config_manager.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_pdf.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_report.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_setup.py +0 -0
- {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_token_manager.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: patcherctl
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Fetch patch management data from Jamf Pro to generate comprehensive reports in both Excel and PDF formats
|
|
5
5
|
Author: Andrew Lerman, Chris Ball
|
|
6
6
|
Author-email: info@liquidzoo.io
|
|
@@ -268,7 +268,8 @@ Requires-Dist: sphinx-togglebutton; extra == "docs"
|
|
|
268
268
|
</a>
|
|
269
269
|
</p>
|
|
270
270
|
|
|
271
|
-
    
|
|
271
|
+
     
|
|
272
|
+
|
|
272
273
|
|
|
273
274
|
----
|
|
274
275
|
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
</a>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
|
-
    
|
|
7
|
+
     
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
----
|
|
10
11
|
|
|
@@ -45,7 +45,7 @@ Repository = "https://github.com/liquidz00/Patcher"
|
|
|
45
45
|
Issues = "https://github.com/liquidz00/Patcher/issues/new?assignees=&labels=bug&projects=&template=issue.md&title=%5BISSUE%5D+Your+Issue+Title"
|
|
46
46
|
|
|
47
47
|
[project.scripts]
|
|
48
|
-
patcherctl = "patcher.cli:
|
|
48
|
+
patcherctl = "patcher.cli:cli"
|
|
49
49
|
|
|
50
50
|
[project.optional-dependencies]
|
|
51
51
|
all = [
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__title__ = "patcher"
|
|
2
|
-
__version__ = "2.0"
|
|
2
|
+
__version__ = "2.0.2"
|
|
@@ -96,6 +96,7 @@ class ApiClient(BaseAPIClient):
|
|
|
96
96
|
patch_titles = [
|
|
97
97
|
PatchTitle(
|
|
98
98
|
title=summary.get("title"),
|
|
99
|
+
title_id=summary.get("softwareTitleId"),
|
|
99
100
|
released=self._convert_tz(summary.get("releaseDate")),
|
|
100
101
|
hosts_patched=summary.get("upToDate"),
|
|
101
102
|
missing_patch=summary.get("outOfDate"),
|
|
@@ -81,6 +81,7 @@ class ReportManager:
|
|
|
81
81
|
mapped = [
|
|
82
82
|
PatchTitle(
|
|
83
83
|
title=f"iOS {latest_versions_dict[version]['ProductVersion']}",
|
|
84
|
+
title_id="iOS",
|
|
84
85
|
released=latest_versions_dict[version]["ReleaseDate"],
|
|
85
86
|
hosts_patched=counts["count"],
|
|
86
87
|
missing_patch=counts["total"] - counts["count"],
|
|
@@ -149,6 +150,7 @@ class ReportManager:
|
|
|
149
150
|
output_path = self._validate_directory(path)
|
|
150
151
|
|
|
151
152
|
self.log.debug("Attempting to retrieve policy IDs.")
|
|
153
|
+
await animation.update_msg("Retrieving policy IDs from Jamf...")
|
|
152
154
|
try:
|
|
153
155
|
patch_ids = await self.api_client.get_policies()
|
|
154
156
|
self.log.info(f"Retrieved policy IDs for {len(patch_ids)} policies.")
|
|
@@ -159,6 +161,7 @@ class ReportManager:
|
|
|
159
161
|
)
|
|
160
162
|
|
|
161
163
|
self.log.debug("Attempting to retrieve patch summaries.")
|
|
164
|
+
await animation.update_msg("Retrieving patch summaries from Jamf...")
|
|
162
165
|
try:
|
|
163
166
|
patch_reports = await self.api_client.get_summaries(patch_ids)
|
|
164
167
|
self.log.info(f"Received policy summaries for {len(patch_reports)} policies.")
|
|
@@ -170,22 +173,27 @@ class ReportManager:
|
|
|
170
173
|
|
|
171
174
|
# (option) Sort
|
|
172
175
|
if sort:
|
|
176
|
+
await animation.update_msg("Sorting reports...")
|
|
173
177
|
patch_reports = await self._sort(patch_reports, sort)
|
|
174
178
|
|
|
175
179
|
# (option) Omit
|
|
176
180
|
if omit:
|
|
181
|
+
await animation.update_msg("Omitting recent releases...")
|
|
177
182
|
patch_reports = await self._omit(patch_reports)
|
|
178
183
|
|
|
179
184
|
# (option) iOS
|
|
180
185
|
if ios:
|
|
186
|
+
await animation.update_msg("Including iOS info...")
|
|
181
187
|
patch_reports = await self._ios(patch_reports)
|
|
182
188
|
|
|
183
189
|
# Generate reports
|
|
190
|
+
await animation.update_msg("Generating Excel file...")
|
|
184
191
|
excel_file = await self._generate_excel(
|
|
185
192
|
patch_reports=patch_reports, reports_dir=output_path
|
|
186
193
|
)
|
|
187
194
|
|
|
188
195
|
if pdf:
|
|
196
|
+
await animation.update_msg("Generating PDF report...")
|
|
189
197
|
await self._generate_pdf(excel_file=excel_file, date_format=date_format)
|
|
190
198
|
|
|
191
199
|
# Manually stop animation to show success message cleanly
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
from typing import List, Optional
|
|
1
|
+
from typing import List, Optional, Union
|
|
2
2
|
|
|
3
|
-
from pydantic import model_validator
|
|
3
|
+
from pydantic import field_validator, model_validator
|
|
4
4
|
|
|
5
5
|
from . import Model
|
|
6
6
|
from .label import Label
|
|
@@ -12,6 +12,8 @@ class PatchTitle(Model):
|
|
|
12
12
|
|
|
13
13
|
:ivar title: The name of the patch title.
|
|
14
14
|
:type title: :py:class:`str`
|
|
15
|
+
:ivar title_id: The ``softwareTitleId`` of the patch title from Jamf API response.
|
|
16
|
+
:type title_id: :py:class:`str`
|
|
15
17
|
:ivar released: The release date of the patch title.
|
|
16
18
|
:type released: :py:class:`str`
|
|
17
19
|
:ivar hosts_patched: The number of hosts that have applied the patch.
|
|
@@ -24,17 +26,33 @@ class PatchTitle(Model):
|
|
|
24
26
|
:type completion_percent: :py:class:`float`
|
|
25
27
|
:ivar total_hosts: The total number of hosts.
|
|
26
28
|
:type total_hosts: :py:class:`int`
|
|
27
|
-
:ivar install_label: The corresponding `Installomator <https://github.com/Installomator/Installomator>`_ label if available.
|
|
29
|
+
:ivar install_label: The corresponding `Installomator <https://github.com/Installomator/Installomator>`_ label(s) if available.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
32
|
title: str
|
|
33
|
+
title_id: str
|
|
31
34
|
released: str
|
|
32
35
|
hosts_patched: int
|
|
33
36
|
missing_patch: int
|
|
34
37
|
latest_version: str
|
|
35
38
|
completion_percent: float = 0.0
|
|
36
39
|
total_hosts: int = 0
|
|
37
|
-
install_label: Optional[List[Label]] =
|
|
40
|
+
install_label: Optional[List[Label]] = [] # account for variants (e.g., zulujdk8, zulujdk9)
|
|
41
|
+
|
|
42
|
+
def __str__(self):
|
|
43
|
+
return f"{self.title} ({self.latest_version})"
|
|
44
|
+
|
|
45
|
+
@field_validator("title_id")
|
|
46
|
+
def cast_as_string(cls, value: Union[int, str]) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Ensures the ``title_id`` property is always a string, regardless of type in API response payload.
|
|
49
|
+
|
|
50
|
+
:param value: The value of the ``title_id`` field.
|
|
51
|
+
:type value: :py:obj:`~typing.Union` [:py:class:`int` | :py:class:`str`]
|
|
52
|
+
:return: The value cast as a string.
|
|
53
|
+
:rtype: :py:class:`str`
|
|
54
|
+
"""
|
|
55
|
+
return str(value)
|
|
38
56
|
|
|
39
57
|
# Calculate completion percent via model validator
|
|
40
58
|
@model_validator(mode="after")
|
|
@@ -26,7 +26,7 @@ class Animation:
|
|
|
26
26
|
self.enable_animation = enable_animation
|
|
27
27
|
self.task = None
|
|
28
28
|
self.lock = asyncio.Lock()
|
|
29
|
-
self.spinner_chars = ["\
|
|
29
|
+
self.spinner_chars = ["\u25e4", "\u25e5", "\u25e2", "\u25e3"]
|
|
30
30
|
self.colors = ["cyan", "magenta", "bright_cyan", "bright_magenta"]
|
|
31
31
|
self.last_message_length = 0
|
|
32
32
|
|
|
@@ -15,7 +15,7 @@ from .logger import LogMe
|
|
|
15
15
|
|
|
16
16
|
class DataManager:
|
|
17
17
|
|
|
18
|
-
_IGNORED = ["install_label"]
|
|
18
|
+
_IGNORED = ["install_label", "title_id"]
|
|
19
19
|
|
|
20
20
|
def __init__(self, disable_cache: bool = False):
|
|
21
21
|
"""
|
|
@@ -139,7 +139,6 @@ class DataManager:
|
|
|
139
139
|
self.log.debug("Attempting to create DataFrame from PatchTitle objects.")
|
|
140
140
|
try:
|
|
141
141
|
df = pd.DataFrame([patch.model_dump() for patch in patch_titles])
|
|
142
|
-
df = df.drop(columns=DataManager._IGNORED, errors="ignore") # Drop excluded columns
|
|
143
142
|
df.columns = [column.replace("_", " ").title() for column in df.columns]
|
|
144
143
|
self.log.info(
|
|
145
144
|
f"Created DataFrame from {len(patch_titles)} PatchTitle objects successfully."
|
|
@@ -154,7 +153,7 @@ class DataManager:
|
|
|
154
153
|
skipped_rows = 0
|
|
155
154
|
patch_titles = []
|
|
156
155
|
|
|
157
|
-
for
|
|
156
|
+
for _, row in df.iterrows():
|
|
158
157
|
try:
|
|
159
158
|
patch = PatchTitle(
|
|
160
159
|
**{key.lower().replace(" ", "_"): value for key, value in row.items()}
|
|
@@ -163,7 +162,7 @@ class DataManager:
|
|
|
163
162
|
except (KeyError, ValueError, TypeError, ValidationError) as e:
|
|
164
163
|
exception_name = type(e).__name__
|
|
165
164
|
self.log.warning(
|
|
166
|
-
f"
|
|
165
|
+
f"Encountered {exception_name} during PatchTitle creation. Skipping row. Details: {e}."
|
|
167
166
|
)
|
|
168
167
|
skipped_rows += 1
|
|
169
168
|
|
|
@@ -191,6 +190,9 @@ class DataManager:
|
|
|
191
190
|
|
|
192
191
|
current_date = datetime.now().strftime("%m-%d-%y")
|
|
193
192
|
df = self._create_dataframe(patch_titles)
|
|
193
|
+
df = df.drop(
|
|
194
|
+
columns=[col.replace("_", " ").title() for col in DataManager._IGNORED], errors="ignore"
|
|
195
|
+
) # Drop excluded columns
|
|
194
196
|
|
|
195
197
|
self.log.debug("Attempting to export patch reports to Excel.")
|
|
196
198
|
try:
|
|
@@ -242,7 +244,11 @@ class DataManager:
|
|
|
242
244
|
file_path = export_dir / f"patch-analysis-{current_date}.html"
|
|
243
245
|
|
|
244
246
|
filtered_data = [
|
|
245
|
-
{
|
|
247
|
+
{
|
|
248
|
+
key: value
|
|
249
|
+
for key, value in patch.model_dump().items()
|
|
250
|
+
if key not in DataManager._IGNORED
|
|
251
|
+
}
|
|
246
252
|
for patch in titles
|
|
247
253
|
]
|
|
248
254
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: patcherctl
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Fetch patch management data from Jamf Pro to generate comprehensive reports in both Excel and PDF formats
|
|
5
5
|
Author: Andrew Lerman, Chris Ball
|
|
6
6
|
Author-email: info@liquidzoo.io
|
|
@@ -268,7 +268,8 @@ Requires-Dist: sphinx-togglebutton; extra == "docs"
|
|
|
268
268
|
</a>
|
|
269
269
|
</p>
|
|
270
270
|
|
|
271
|
-
    
|
|
271
|
+
     
|
|
272
|
+
|
|
272
273
|
|
|
273
274
|
----
|
|
274
275
|
|
|
@@ -8,6 +8,7 @@ from src.patcher.models.patch import PatchTitle
|
|
|
8
8
|
# Mock DataFrame for testing
|
|
9
9
|
mock_data = {
|
|
10
10
|
"Title": ["Patch A", "Patch B", "Patch C"],
|
|
11
|
+
"Title Id": ["0", "1", "2"],
|
|
11
12
|
"Released": ["2022-01-01", "2023-01-01", "2023-12-01"],
|
|
12
13
|
"Hosts Patched": [50, 30, 20],
|
|
13
14
|
"Missing Patch": [10, 20, 5],
|
|
@@ -30,6 +31,7 @@ def test_patch_title_calculation():
|
|
|
30
31
|
"""Test the PatchTitle model's completion percentage calculation."""
|
|
31
32
|
patch_title = PatchTitle(
|
|
32
33
|
title="Patch A",
|
|
34
|
+
title_id="0",
|
|
33
35
|
released="2022-01-01",
|
|
34
36
|
hosts_patched=50,
|
|
35
37
|
missing_patch=10,
|
|
@@ -41,6 +43,7 @@ def test_patch_title_calculation():
|
|
|
41
43
|
# Test with zero total hosts
|
|
42
44
|
patch_title_zero = PatchTitle(
|
|
43
45
|
title="Patch B",
|
|
46
|
+
title_id="1",
|
|
44
47
|
released="2023-01-01",
|
|
45
48
|
hosts_patched=0,
|
|
46
49
|
missing_patch=0,
|
|
@@ -86,6 +89,7 @@ def test_filter_titles(analyzer, mock_data_manager):
|
|
|
86
89
|
patch_titles = [
|
|
87
90
|
PatchTitle(
|
|
88
91
|
title="Patch A",
|
|
92
|
+
title_id="0",
|
|
89
93
|
released="2022-01-01",
|
|
90
94
|
hosts_patched=50,
|
|
91
95
|
missing_patch=10,
|
|
@@ -95,6 +99,7 @@ def test_filter_titles(analyzer, mock_data_manager):
|
|
|
95
99
|
),
|
|
96
100
|
PatchTitle(
|
|
97
101
|
title="Patch B",
|
|
102
|
+
title_id="1",
|
|
98
103
|
released="2023-01-01",
|
|
99
104
|
hosts_patched=30,
|
|
100
105
|
missing_patch=20,
|
|
@@ -104,6 +109,7 @@ def test_filter_titles(analyzer, mock_data_manager):
|
|
|
104
109
|
),
|
|
105
110
|
PatchTitle(
|
|
106
111
|
title="Patch C",
|
|
112
|
+
title_id="2",
|
|
107
113
|
released="2023-12-01",
|
|
108
114
|
hosts_patched=20,
|
|
109
115
|
missing_patch=5,
|
|
@@ -147,6 +153,7 @@ def test_filter_titles_parametrized(analyzer, criteria, expected_count, mock_dat
|
|
|
147
153
|
mock_data_manager.titles = [
|
|
148
154
|
PatchTitle(
|
|
149
155
|
title="Patch A",
|
|
156
|
+
title_id="0",
|
|
150
157
|
released="2022-01-01",
|
|
151
158
|
hosts_patched=50,
|
|
152
159
|
missing_patch=10,
|
|
@@ -154,6 +161,7 @@ def test_filter_titles_parametrized(analyzer, criteria, expected_count, mock_dat
|
|
|
154
161
|
),
|
|
155
162
|
PatchTitle(
|
|
156
163
|
title="Patch B",
|
|
164
|
+
title_id="1",
|
|
157
165
|
released="2023-01-01",
|
|
158
166
|
hosts_patched=30,
|
|
159
167
|
missing_patch=20,
|
|
@@ -161,6 +169,7 @@ def test_filter_titles_parametrized(analyzer, criteria, expected_count, mock_dat
|
|
|
161
169
|
),
|
|
162
170
|
PatchTitle(
|
|
163
171
|
title="Patch C",
|
|
172
|
+
title_id="2",
|
|
164
173
|
released="2023-12-01",
|
|
165
174
|
hosts_patched=20,
|
|
166
175
|
missing_patch=5,
|
|
@@ -24,6 +24,7 @@ def test_export_to_excel_success(sample_patch_reports, temp_output_dir):
|
|
|
24
24
|
assert not df.empty
|
|
25
25
|
assert list(df.columns) == [
|
|
26
26
|
"Title",
|
|
27
|
+
# "Title Id",
|
|
27
28
|
"Released",
|
|
28
29
|
"Hosts Patched",
|
|
29
30
|
"Missing Patch",
|
|
@@ -88,6 +89,7 @@ def test_titles_property_setter_valid():
|
|
|
88
89
|
patch_titles = [
|
|
89
90
|
PatchTitle(
|
|
90
91
|
title="Patch A",
|
|
92
|
+
title_id="0",
|
|
91
93
|
released="2022-01-01",
|
|
92
94
|
hosts_patched=50,
|
|
93
95
|
missing_patch=10,
|
|
@@ -95,6 +97,7 @@ def test_titles_property_setter_valid():
|
|
|
95
97
|
),
|
|
96
98
|
PatchTitle(
|
|
97
99
|
title="Patch B",
|
|
100
|
+
title_id="1",
|
|
98
101
|
released="2023-01-01",
|
|
99
102
|
hosts_patched=30,
|
|
100
103
|
missing_patch=20,
|
|
@@ -130,6 +133,7 @@ def test_export_to_excel_permission_error(temp_output_path):
|
|
|
130
133
|
mock_patches = [
|
|
131
134
|
PatchTitle(
|
|
132
135
|
title="Patch A",
|
|
136
|
+
title_id="0",
|
|
133
137
|
released="2022-01-01",
|
|
134
138
|
hosts_patched=50,
|
|
135
139
|
missing_patch=10,
|
|
@@ -139,6 +143,7 @@ def test_export_to_excel_permission_error(temp_output_path):
|
|
|
139
143
|
),
|
|
140
144
|
PatchTitle(
|
|
141
145
|
title="Patch B",
|
|
146
|
+
title_id="1",
|
|
142
147
|
released="2023-01-01",
|
|
143
148
|
hosts_patched=30,
|
|
144
149
|
missing_patch=20,
|
|
@@ -148,6 +153,7 @@ def test_export_to_excel_permission_error(temp_output_path):
|
|
|
148
153
|
),
|
|
149
154
|
PatchTitle(
|
|
150
155
|
title="Patch C",
|
|
156
|
+
title_id="2",
|
|
151
157
|
released="2023-12-01",
|
|
152
158
|
hosts_patched=20,
|
|
153
159
|
missing_patch=5,
|
|
@@ -93,6 +93,7 @@ async def test_calculate_ios_on_latest_success(patcher_instance):
|
|
|
93
93
|
expected_result = [
|
|
94
94
|
PatchTitle(
|
|
95
95
|
title="iOS 17.5.1",
|
|
96
|
+
title_id="iOS",
|
|
96
97
|
released="2024-05-20T00:00:00Z",
|
|
97
98
|
hosts_patched=2,
|
|
98
99
|
missing_patch=0,
|
|
@@ -102,6 +103,7 @@ async def test_calculate_ios_on_latest_success(patcher_instance):
|
|
|
102
103
|
),
|
|
103
104
|
PatchTitle(
|
|
104
105
|
title="iOS 16.7.8",
|
|
106
|
+
title_id="iOS",
|
|
105
107
|
released="2024-05-13T00:00:00Z",
|
|
106
108
|
hosts_patched=1,
|
|
107
109
|
missing_patch=0,
|
|
@@ -138,6 +140,7 @@ async def test_calculate_ios_on_latest_no_devices_on_latest(patcher_instance):
|
|
|
138
140
|
expected_result = [
|
|
139
141
|
PatchTitle(
|
|
140
142
|
title="iOS 17.5.1",
|
|
143
|
+
title_id="iOS",
|
|
141
144
|
released="2024-05-20T00:00:00Z",
|
|
142
145
|
hosts_patched=0,
|
|
143
146
|
missing_patch=1,
|
|
@@ -147,6 +150,7 @@ async def test_calculate_ios_on_latest_no_devices_on_latest(patcher_instance):
|
|
|
147
150
|
),
|
|
148
151
|
PatchTitle(
|
|
149
152
|
title="iOS 16.7.8",
|
|
153
|
+
title_id="iOS",
|
|
150
154
|
released="2024-05-13T00:00:00Z",
|
|
151
155
|
hosts_patched=0,
|
|
152
156
|
missing_patch=1,
|
|
@@ -179,6 +183,7 @@ async def test_calculate_ios_on_latest_all_devices_on_latest(patcher_instance):
|
|
|
179
183
|
expected_result = [
|
|
180
184
|
PatchTitle(
|
|
181
185
|
title="iOS 17.5.1",
|
|
186
|
+
title_id="iOS",
|
|
182
187
|
released="2024-05-20T00:00:00Z",
|
|
183
188
|
hosts_patched=2,
|
|
184
189
|
missing_patch=0,
|
|
@@ -211,6 +216,7 @@ async def test_calculate_ios_on_latest_some_devices_on_latest(patcher_instance):
|
|
|
211
216
|
expected_result = [
|
|
212
217
|
PatchTitle(
|
|
213
218
|
title="iOS 17.5.1",
|
|
219
|
+
title_id="iOS",
|
|
214
220
|
released="2024-05-20T00:00:00Z",
|
|
215
221
|
hosts_patched=1,
|
|
216
222
|
missing_patch=1,
|
|
@@ -76,13 +76,13 @@ def test_config_load_existing(ui_manager):
|
|
|
76
76
|
|
|
77
77
|
def test_config_load_default(ui_manager):
|
|
78
78
|
with (
|
|
79
|
-
patch.object(Path, "exists", side_effect=lambda: False),
|
|
80
|
-
patch.object(ui_manager, "_download_font", MagicMock()),
|
|
81
79
|
patch("plistlib.load", return_value={}),
|
|
80
|
+
patch.object(Path, "exists", return_value=False),
|
|
81
|
+
patch.object(ui_manager, "create_default_config") as mock_create_default,
|
|
82
82
|
):
|
|
83
83
|
config = ui_manager.config
|
|
84
|
-
|
|
85
|
-
assert config
|
|
84
|
+
mock_create_default.assert_called_once()
|
|
85
|
+
assert config == {}
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
def test_download_font_success(ui_manager):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|