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.
Files changed (47) hide show
  1. {patcherctl-2.0 → patcherctl-2.0.2}/PKG-INFO +3 -2
  2. {patcherctl-2.0 → patcherctl-2.0.2}/README.md +2 -1
  3. {patcherctl-2.0 → patcherctl-2.0.2}/pyproject.toml +1 -1
  4. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/__about__.py +1 -1
  5. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/api_client.py +1 -0
  6. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/report_manager.py +8 -0
  7. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/patch.py +22 -4
  8. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/animation.py +1 -1
  9. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/data_manager.py +11 -5
  10. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/PKG-INFO +3 -2
  11. patcherctl-2.0.2/src/patcherctl.egg-info/entry_points.txt +2 -0
  12. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_analyzer.py +9 -0
  13. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_data_manager.py +6 -0
  14. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_ios.py +6 -0
  15. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_ui.py +4 -4
  16. patcherctl-2.0/src/patcherctl.egg-info/entry_points.txt +0 -2
  17. {patcherctl-2.0 → patcherctl-2.0.2}/LICENSE.txt +0 -0
  18. {patcherctl-2.0 → patcherctl-2.0.2}/setup.cfg +0 -0
  19. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/__init__.py +0 -0
  20. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/cli.py +0 -0
  21. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/__init__.py +0 -0
  22. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/analyze.py +0 -0
  23. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/config_manager.py +0 -0
  24. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/setup.py +0 -0
  25. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/token_manager.py +0 -0
  26. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/client/ui_manager.py +0 -0
  27. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/__init__.py +0 -0
  28. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/jamf_client.py +0 -0
  29. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/label.py +0 -0
  30. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/models/token.py +0 -0
  31. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/__init__.py +0 -0
  32. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/decorators.py +0 -0
  33. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/exceptions.py +0 -0
  34. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/installomator.py +0 -0
  35. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/logger.py +0 -0
  36. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcher/utils/pdf_report.py +0 -0
  37. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/SOURCES.txt +0 -0
  38. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/dependency_links.txt +0 -0
  39. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/requires.txt +0 -0
  40. {patcherctl-2.0 → patcherctl-2.0.2}/src/patcherctl.egg-info/top_level.txt +0 -0
  41. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_api_fetching.py +0 -0
  42. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_base_api.py +0 -0
  43. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_config_manager.py +0 -0
  44. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_pdf.py +0 -0
  45. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_report.py +0 -0
  46. {patcherctl-2.0 → patcherctl-2.0.2}/tests/test_setup.py +0 -0
  47. {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
- ![](https://img.shields.io/pypi/l/patcherctl)&nbsp;![](https://img.shields.io/badge/Python-3.10+-3776AB.svg?style=flat&logo=python&logoColor=white)&nbsp;![](https://img.shields.io/github/v/release/liquidz00/Patcher?color=orange)&nbsp;![](https://github.com/liquidz00/patcher/actions/workflows/pytest.yml/badge.svg)&nbsp;![](https://img.shields.io/pypi/v/patcherctl?color=yellow)
271
+ ![](https://img.shields.io/pypi/l/patcherctl)&nbsp;![](https://img.shields.io/badge/Python-3.10+-3776AB.svg?style=flat&logo=python&logoColor=white)&nbsp;![](https://img.shields.io/github/v/release/liquidz00/Patcher?color=orange)&nbsp;![](https://github.com/liquidz00/patcher/actions/workflows/pytest.yml/badge.svg)&nbsp;![](https://img.shields.io/pypi/v/patcherctl?color=yellow)&nbsp;![](https://img.shields.io/badge/macOS-10.13%2B-blueviolet?logo=apple&logoSize=auto)
272
+
272
273
 
273
274
  ----
274
275
 
@@ -4,7 +4,8 @@
4
4
  </a>
5
5
  </p>
6
6
 
7
- ![](https://img.shields.io/pypi/l/patcherctl)&nbsp;![](https://img.shields.io/badge/Python-3.10+-3776AB.svg?style=flat&logo=python&logoColor=white)&nbsp;![](https://img.shields.io/github/v/release/liquidz00/Patcher?color=orange)&nbsp;![](https://github.com/liquidz00/patcher/actions/workflows/pytest.yml/badge.svg)&nbsp;![](https://img.shields.io/pypi/v/patcherctl?color=yellow)
7
+ ![](https://img.shields.io/pypi/l/patcherctl)&nbsp;![](https://img.shields.io/badge/Python-3.10+-3776AB.svg?style=flat&logo=python&logoColor=white)&nbsp;![](https://img.shields.io/github/v/release/liquidz00/Patcher?color=orange)&nbsp;![](https://github.com/liquidz00/patcher/actions/workflows/pytest.yml/badge.svg)&nbsp;![](https://img.shields.io/pypi/v/patcherctl?color=yellow)&nbsp;![](https://img.shields.io/badge/macOS-10.13%2B-blueviolet?logo=apple&logoSize=auto)
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:main"
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]] = None # account for variants (e.g., zulujdk8, zulujdk9)
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 = ["\u25E4", "\u25E5", "\u25E2", "\u25E3"]
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 index, row in df.iterrows():
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"Error processing row at {index} due to {exception_name}. Skipping this row. Details: {e}."
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
- {key: value for key, value in patch.model_dump().items() if key not in self._IGNORED}
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
- ![](https://img.shields.io/pypi/l/patcherctl)&nbsp;![](https://img.shields.io/badge/Python-3.10+-3776AB.svg?style=flat&logo=python&logoColor=white)&nbsp;![](https://img.shields.io/github/v/release/liquidz00/Patcher?color=orange)&nbsp;![](https://github.com/liquidz00/patcher/actions/workflows/pytest.yml/badge.svg)&nbsp;![](https://img.shields.io/pypi/v/patcherctl?color=yellow)
271
+ ![](https://img.shields.io/pypi/l/patcherctl)&nbsp;![](https://img.shields.io/badge/Python-3.10+-3776AB.svg?style=flat&logo=python&logoColor=white)&nbsp;![](https://img.shields.io/github/v/release/liquidz00/Patcher?color=orange)&nbsp;![](https://github.com/liquidz00/patcher/actions/workflows/pytest.yml/badge.svg)&nbsp;![](https://img.shields.io/pypi/v/patcherctl?color=yellow)&nbsp;![](https://img.shields.io/badge/macOS-10.13%2B-blueviolet?logo=apple&logoSize=auto)
272
+
272
273
 
273
274
  ----
274
275
 
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ patcherctl = patcher.cli:cli
@@ -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
- assert config["FONT_REGULAR_PATH"].endswith("Assistant-Regular.ttf")
85
- assert config["FONT_BOLD_PATH"].endswith("Assistant-Bold.ttf")
84
+ mock_create_default.assert_called_once()
85
+ assert config == {}
86
86
 
87
87
 
88
88
  def test_download_font_success(ui_manager):
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- patcherctl = patcher.cli:main
File without changes
File without changes
File without changes
File without changes
File without changes