Skip to content

Commit 1ad61b1

Browse files
authored
Merge pull request #25 from MScottBlake/refactor-appconfig
Replace `AppConfig` with `settings`
2 parents 11231fa + 44bec13 commit 1ad61b1

File tree

13 files changed

+529
-301
lines changed

13 files changed

+529
-301
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dev = [
1414

1515
[project]
1616
name = "cloud-autopkg-runner"
17-
version = "0.10.0"
17+
version = "0.11.0"
1818
description = "A Python library designed to level-up your AutoPkg automations with a focus on CI/CD performance."
1919
readme = { file = "README.md", content-type = "text/markdown" }
2020
requires-python = ">=3.10"

src/cloud_autopkg_runner/__init__.py

Lines changed: 4 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -13,159 +13,8 @@
1313
- Metadata caching to reduce redundant downloads.
1414
"""
1515

16-
from pathlib import Path
16+
from .settings import settings
1717

18-
19-
class AppConfig:
20-
"""Manages application-wide configuration settings.
21-
22-
This class uses class variables to store configuration data and
23-
class methods to manage and access that data. It provides access
24-
to configuration parameters such as verbosity level, log file path,
25-
and the metadata cache file path.
26-
"""
27-
28-
_cache_file: Path = Path("metadata_cache.json")
29-
_log_file: Path | None = None
30-
_max_concurrency: int = 10
31-
_report_dir: Path = Path("recipe_reports")
32-
_verbosity_level: int = 0
33-
34-
@classmethod
35-
def set_config(
36-
cls,
37-
*,
38-
max_concurrency: int | None = None,
39-
verbosity_level: int | None = None,
40-
cache_file: Path | None = None,
41-
report_dir: Path | None = None,
42-
log_file: Path | None = None,
43-
) -> None:
44-
"""Set the application configuration parameters.
45-
46-
This method updates the class variables that store the verbosity
47-
level, log file path, and cache file path. It does *not* initialize
48-
the logging system; `initialize_logger()` must be called separately.
49-
50-
Args:
51-
verbosity_level: The integer verbosity level (0, 1, 2, etc.).
52-
max_concurrency: The integer concurrency limit.
53-
log_file: Optional path to the log file. If specified, logging
54-
output will be written to this file in addition to the console.
55-
cache_file: The path to the cache file.
56-
report_dir: The path to the directory used for storing AutoPkg recipe
57-
recipts and recipe reports.
58-
"""
59-
if verbosity_level is not None:
60-
cls._verbosity_level = verbosity_level
61-
62-
if log_file is not None:
63-
cls._log_file = log_file
64-
65-
if cache_file is not None:
66-
cls._cache_file = cache_file
67-
68-
if max_concurrency is not None:
69-
cls._max_concurrency = max_concurrency
70-
71-
if report_dir is not None:
72-
cls._report_dir = report_dir
73-
74-
@classmethod
75-
def cache_file(cls) -> Path:
76-
"""Returns the path to the metadata cache file."""
77-
return cls._cache_file
78-
79-
@classmethod
80-
def log_file(cls) -> Path | None:
81-
"""Returns the path to the log file, if any.
82-
83-
Returns:
84-
The path to the log file as a `pathlib.Path`, or None if no log file is
85-
configured.
86-
"""
87-
return cls._log_file
88-
89-
@classmethod
90-
def report_dir(cls) -> Path:
91-
"""Returns the directory path for recipe reports.
92-
93-
Returns:
94-
The path to the report directory as a `pathlib.Path`.
95-
"""
96-
return cls._report_dir
97-
98-
@classmethod
99-
def max_concurrency(cls) -> int:
100-
"""Returns the maximum number of concurrent tasks.
101-
102-
Returns:
103-
Returns the maximum number of concurrent tasks as an integer.
104-
"""
105-
return cls._max_concurrency
106-
107-
@classmethod
108-
def verbosity_int(cls, delta: int = 0) -> int:
109-
"""Returns the verbosity level.
110-
111-
Args:
112-
delta: An optional integer to add to the base verbosity level.
113-
This can be used to temporarily increase or decrease the
114-
verbosity for specific operations.
115-
116-
Returns:
117-
The integer verbosity level, adjusted by the delta.
118-
"""
119-
level = cls._verbosity_level + delta
120-
if level <= 0:
121-
return 0
122-
return level
123-
124-
@classmethod
125-
def verbosity_str(cls, delta: int = 0) -> str:
126-
"""Convert an integer verbosity level to a string of `-v` flags.
127-
128-
Args:
129-
delta: An optional integer to add to the base verbosity level.
130-
This can be used to temporarily increase or decrease the
131-
verbosity for specific operations.
132-
133-
Returns:
134-
A string consisting of `-` followed by `v` repeated `verbosity_level`
135-
times. Returns an empty string if verbosity_level is 0 or negative.
136-
137-
Examples:
138-
verbosity_str(0) == ""
139-
verbosity_str(1) == "-v"
140-
verbosity_str(2) == "-vv"
141-
verbosity_str(3) == "-vvv"
142-
"""
143-
level = cls._verbosity_level + delta
144-
if level <= 0:
145-
return ""
146-
return "-" + "v" * level
147-
148-
149-
# Located here to prevent circular imports
150-
def list_possible_file_names(recipe_name: str) -> list[str]:
151-
"""Generate a list of possible AutoPkg recipe file names.
152-
153-
Given a recipe name, this function returns a list of possible file names
154-
by appending common AutoPkg recipe file extensions. If the recipe name
155-
already ends with a known extension, it returns a list containing only the
156-
original recipe name.
157-
158-
Args:
159-
recipe_name: The name of the AutoPkg recipe.
160-
161-
Returns:
162-
A list of possible file names for the recipe.
163-
"""
164-
if recipe_name.endswith((".recipe", ".recipe.plist", ".recipe.yaml")):
165-
return [recipe_name]
166-
167-
return [
168-
recipe_name + ".recipe",
169-
recipe_name + ".recipe.plist",
170-
recipe_name + ".recipe.yaml",
171-
]
18+
__all__ = [
19+
"settings",
20+
]

src/cloud_autopkg_runner/__main__.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from types import FrameType
2525
from typing import NoReturn
2626

27-
from cloud_autopkg_runner import AppConfig
27+
from cloud_autopkg_runner import settings
2828
from cloud_autopkg_runner.exceptions import (
2929
InvalidFileContents,
3030
InvalidJsonContents,
@@ -152,15 +152,13 @@ async def _process_recipe_list(
152152
logger = get_logger(__name__)
153153
logger.debug("Processing recipes...")
154154

155-
report_dir = AppConfig.report_dir()
156-
157155
recipes: list[Recipe] = []
158156
for recipe_name in recipe_list:
159157
with contextlib.suppress(InvalidFileContents, RecipeException):
160-
recipes.append(Recipe(recipe_name, report_dir)) # Skips failures
158+
recipes.append(Recipe(recipe_name, settings.report_dir)) # Skips failures
161159

162160
async def run_limited(recipe: Recipe) -> tuple[str, ConsolidatedReport]:
163-
async with asyncio.Semaphore(AppConfig.max_concurrency()): # Limit concurrency
161+
async with asyncio.Semaphore(settings.max_concurrency): # Limit concurrency
164162
return recipe.name, await recipe.run()
165163

166164
results = await asyncio.gather(*[run_limited(recipe) for recipe in recipes])
@@ -202,13 +200,11 @@ async def _async_main() -> None:
202200
args = _parse_arguments()
203201
initialize_logger(args.verbose, args.log_file)
204202

205-
AppConfig.set_config(
206-
cache_file=args.cache_file,
207-
log_file=args.log_file,
208-
max_concurrency=args.max_concurrency,
209-
report_dir=args.report_dir,
210-
verbosity_level=args.verbose,
211-
)
203+
settings.cache_file = args.cache_file
204+
settings.log_file = args.log_file
205+
settings.max_concurrency = args.max_concurrency
206+
settings.report_dir = args.report_dir
207+
settings.verbosity_level = args.verbose
212208

213209
recipe_list = _generate_recipe_list(args)
214210

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Provides common utility functions for cloud-autopkg-runner.
2+
3+
This module encapsulates general-purpose utility functions that are
4+
used throughout the cloud-autopkg-runner project. Currently, it includes
5+
a function for generating a list of possible AutoPkg recipe file names
6+
based on common naming conventions.
7+
8+
Functions:
9+
list_possible_file_names: Generates a list of possible AutoPkg recipe
10+
file names for a given recipe name.
11+
"""
12+
13+
14+
def list_possible_file_names(recipe_name: str) -> list[str]:
15+
"""Generate a list of possible AutoPkg recipe file names.
16+
17+
Given a recipe name, this function returns a list of possible file names
18+
by appending common AutoPkg recipe file extensions. If the recipe name
19+
already ends with a known extension, it returns a list containing only the
20+
original recipe name.
21+
22+
Args:
23+
recipe_name: The name of the AutoPkg recipe.
24+
25+
Returns:
26+
A list of possible file names for the recipe.
27+
"""
28+
if recipe_name.endswith((".recipe", ".recipe.plist", ".recipe.yaml")):
29+
return [recipe_name]
30+
31+
return [
32+
recipe_name + ".recipe",
33+
recipe_name + ".recipe.plist",
34+
recipe_name + ".recipe.yaml",
35+
]

src/cloud_autopkg_runner/exceptions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,23 @@ def __init__(self, recipe_extension: str) -> None:
144144
super().__init__(f"Invalid recipe format: {recipe_extension}")
145145

146146

147+
# Settings
148+
class SettingsValidationError(AutoPkgRunnerException):
149+
"""Exception class for handling validation errors in Settings.
150+
151+
This exception is raised when a Setting value does not validate successfully.
152+
"""
153+
154+
def __init__(self, field_name: str, validation_error: str) -> None:
155+
"""Initializes SettingsValidationError with the invalid file extension.
156+
157+
Args:
158+
field_name: The name of the invalid Setting.
159+
validation_error: The message explaining the failure.
160+
"""
161+
super().__init__(f"Invalid value for '{field_name}': {validation_error}")
162+
163+
147164
# Shell Command
148165
class ShellCommandException(AutoPkgRunnerException):
149166
"""Base exception class for handling issues with shell commands.

src/cloud_autopkg_runner/file_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import xattr # pyright: ignore[reportMissingTypeStubs]
2424

25-
from cloud_autopkg_runner import list_possible_file_names
25+
from cloud_autopkg_runner.common_utils import list_possible_file_names
2626
from cloud_autopkg_runner.logging_config import get_logger
2727
from cloud_autopkg_runner.metadata_cache import DownloadMetadata, MetadataCache
2828

src/cloud_autopkg_runner/metadata_cache.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def _load_from_disk(file_path: Path) -> MetadataCache:
135135
if not file_path.exists():
136136
logger = get_logger(__name__)
137137
logger.warning("%s does not exist. Creating...", file_path)
138+
file_path.parent.mkdir(parents=True, exist_ok=True)
138139
file_path.write_text("{}")
139140
logger.info("%s created.", file_path)
140141

src/cloud_autopkg_runner/recipe.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import yaml
2323

24-
from cloud_autopkg_runner import AppConfig
24+
from cloud_autopkg_runner import settings
2525
from cloud_autopkg_runner.autopkg_prefs import AutoPkgPrefs
2626
from cloud_autopkg_runner.exceptions import (
2727
InvalidPlistContents,
@@ -102,7 +102,7 @@ def __init__(self, recipe_name: str, report_dir: Path | None = None) -> None:
102102
Args:
103103
recipe_name: Name of the recipe file.
104104
report_dir: Path to the report directory. If None, a the value returned
105-
from `AppConfig.report_dir()` is used.
105+
from `settings.report_dir` is used.
106106
"""
107107
self._name: str = recipe_name
108108

@@ -119,7 +119,7 @@ def __init__(self, recipe_name: str, report_dir: Path | None = None) -> None:
119119

120120
now_str = datetime.now(tz=timezone.utc).strftime("%y%m%d_%H%M")
121121
if report_dir is None:
122-
report_dir = AppConfig.report_dir()
122+
report_dir = settings.report_dir
123123
report_path: Path = report_dir / f"report_{now_str}_{self.name}.plist"
124124

125125
counter = 1
@@ -278,8 +278,8 @@ def _autopkg_run_cmd(self, *, check: bool = False) -> list[str]:
278278
f"--report-plist={self._result.file_path()}",
279279
]
280280

281-
if AppConfig.verbosity_int(-1) > 0:
282-
cmd.append(AppConfig.verbosity_str(-1))
281+
if settings.verbosity_int(-1) > 0:
282+
cmd.append(settings.verbosity_str(-1))
283283

284284
if check:
285285
cmd.append("--check")
@@ -460,7 +460,7 @@ async def run(self) -> ConsolidatedReport:
460460
output = await self.run_check_phase()
461461
if output["downloaded_items"]:
462462
metadata = await self._get_metadata(output["downloaded_items"])
463-
await MetadataCacheManager.save(AppConfig.cache_file(), self.name, metadata)
463+
await MetadataCacheManager.save(settings.cache_file, self.name, metadata)
464464

465465
return await self.run_full()
466466
return output
@@ -570,8 +570,8 @@ async def verify_trust_info(self) -> bool:
570570
f"--override-dir={self._path.parent}",
571571
]
572572

573-
if AppConfig.verbosity_int() > 0:
574-
cmd.append(AppConfig.verbosity_str())
573+
if settings.verbosity_int() > 0:
574+
cmd.append(settings.verbosity_str())
575575

576576
returncode, _stdout, _stderr = await run_cmd(cmd, check=False)
577577

0 commit comments

Comments
 (0)