Skip to content

Commit 86fc824

Browse files
authored
Merge pull request #34 from Jamf-Custom-Profile-Schemas/1.2.0
1.2.0 merge to main
2 parents 60368fc + f1bfd39 commit 86fc824

File tree

2 files changed

+75
-35
lines changed

2 files changed

+75
-35
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
repos:
22
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v5.0.0
3+
rev: v6.0.0
44
hooks:
55
- id: check-added-large-files
66
args: [--maxkb=100]
@@ -20,27 +20,31 @@ repos:
2020
- id: trailing-whitespace
2121
args: [--markdown-linebreak-ext=md]
2222
- repo: https://github.com/python/black
23-
rev: 24.10.0
23+
rev: 25.1.0
2424
hooks:
2525
- id: black
2626
- repo: https://github.com/pre-commit/mirrors-isort
2727
rev: v5.10.1
2828
hooks:
2929
- id: isort
3030
- repo: https://github.com/pycqa/flake8
31-
rev: 7.1.1
31+
rev: 7.3.0
3232
hooks:
3333
- id: flake8
3434
- repo: https://github.com/asottile/pyupgrade
35-
rev: v3.19.0
35+
rev: v3.20.0
3636
hooks:
3737
- id: pyupgrade
38-
args: ['--py36-plus']
38+
args: ['--py311-plus']
3939
- repo: https://github.com/asottile/blacken-docs
4040
rev: 1.19.1
4141
hooks:
4242
- id: blacken-docs
43-
additional_dependencies: [black==24.4.2]
43+
additional_dependencies: [black>=25.1.0]
44+
- repo: https://github.com/pre-commit/mirrors-mypy
45+
rev: v1.17.1
46+
hooks:
47+
- id: mypy
4448
# - repo: https://github.com/homebysix/pre-commit-macadmin
4549
# rev: profile-manifests
4650
# exclude: "last_build\.json"

build.py

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,39 @@
1919

2020

2121
__author__ = "Elliot Jordan"
22-
__version__ = "1.1.0"
22+
__version__ = "1.2.0"
2323

2424
import argparse
2525
import json
26+
import logging
2627
import os
2728
import plistlib
2829
import shutil
2930
import sys
30-
import xml
31+
import xml.parsers.expat
32+
from typing import Any
3133

3234

33-
def build_argument_parser():
35+
def setup_logging(verbosity: int = 0) -> logging.Logger:
36+
"""Set up logging configuration based on verbosity level."""
37+
if verbosity == 0:
38+
level = logging.WARNING
39+
elif verbosity == 1:
40+
level = logging.INFO
41+
else:
42+
level = logging.DEBUG
43+
44+
# Configure logging format
45+
log_format = "%(levelname)s: %(message)s"
46+
if verbosity >= 2:
47+
log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
48+
49+
logging.basicConfig(level=level, format=log_format, datefmt="%Y-%m-%d %H:%M:%S")
50+
51+
return logging.getLogger(__name__)
52+
53+
54+
def build_argument_parser() -> argparse.ArgumentParser:
3455
"""Build and return the argument parser."""
3556
parser = argparse.ArgumentParser(
3657
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
@@ -78,14 +99,15 @@ def build_argument_parser():
7899
return parser
79100

80101

81-
def validate_args(args):
102+
def validate_args(args: argparse.Namespace) -> argparse.Namespace:
82103
"""Do sanity checking and validation on provided input arguments."""
104+
logger = logging.getLogger(__name__)
83105

84106
if not os.path.isdir(os.path.expanduser(args.input_dir)):
85107
sys.exit("Input path provided is not a directory: %s" % args.input_dir)
86108
if os.path.exists(os.path.expanduser(args.output_dir)):
87109
if args.overwrite:
88-
print("WARNING: Will overwrite output dir: %s" % args.output_dir)
110+
logger.warning("Will overwrite output dir: %s", args.output_dir)
89111
else:
90112
sys.exit(
91113
"Output path already exists: %s\nUse --overwrite to replace contents "
@@ -99,18 +121,22 @@ def validate_args(args):
99121
return args
100122

101123

102-
def read_manifest_plist(path):
124+
def read_manifest_plist(path: str) -> dict[str, Any]:
103125
"""Given a path to a profile manifest plist, return the contents of
104126
the plist."""
127+
logger = logging.getLogger(__name__)
128+
plist = {}
105129
with open(path, "rb") as openfile:
106130
try:
107-
return plistlib.load(openfile)
131+
plist = plistlib.load(openfile)
108132
except xml.parsers.expat.ExpatError:
109-
print("Error reading %s" % path)
133+
logger.error("Error reading %s", path)
134+
return plist
110135

111136

112-
def process_subkeys(subkeys):
137+
def process_subkeys(subkeys: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
113138
"""Given a list of subkeys, return equivalent JSON schema manifest properties."""
139+
logger = logging.getLogger(__name__)
114140

115141
# Skip keys that describe the payload instead of the setting
116142
meta_keys = (
@@ -132,15 +158,15 @@ def process_subkeys(subkeys):
132158
)
133159

134160
properties = {}
135-
for idx, subkey in enumerate(subkeys):
161+
for subkey in subkeys:
136162
# Get subkey name
137163
name = ""
138164
try:
139165
if subkey.get("pfm_name", "") != "":
140166
name = subkey["pfm_name"]
141167
except AttributeError:
142-
print("WARNING: Syntax error. Skipping.")
143-
return
168+
logger.warning("Syntax error. Skipping.")
169+
return {}
144170

145171
# Skip specific names
146172
if name in meta_keys:
@@ -195,7 +221,7 @@ def process_subkeys(subkeys):
195221

196222
# Recurse into sub-sub-keys
197223
if "pfm_subkeys" in subkey and not isinstance(subkey["pfm_subkeys"], list):
198-
print("WARNING: Not a list: %s" % subkey["pfm_subkeys"])
224+
logger.warning("Not a list: %s", subkey["pfm_subkeys"])
199225
if isinstance(subkey.get("pfm_subkeys"), list):
200226
subprop = process_subkeys(subkey["pfm_subkeys"])
201227
if "items" in properties[name]:
@@ -204,12 +230,13 @@ def process_subkeys(subkeys):
204230
# TODO: Validate this assumption. Some warnings seen in the wild.
205231
subprop_keys = list(subprop.keys())
206232
if len(subprop_keys) > 1:
207-
print(
208-
"WARNING: Array type should only have one subproperty "
209-
"key. Skipping all but the first: %s" % subprop_keys
233+
logger.warning(
234+
"Array type should only have one subproperty "
235+
"key. Skipping all but the first: %s",
236+
subprop_keys,
210237
)
211238
elif len(subprop_keys) == 0:
212-
print("WARNING: No subproperty keys found in %s key." % name)
239+
logger.warning("No subproperty keys found in %s key.", name)
213240
continue
214241
array_props = subprop[subprop_keys[0]]
215242
properties[name]["items"] = array_props
@@ -219,11 +246,14 @@ def process_subkeys(subkeys):
219246
return properties
220247

221248

222-
def convert_to_jamf_manifest(data, property_order_increment=5):
249+
def convert_to_jamf_manifest(
250+
data: dict[str, Any], property_order_increment: int = 5
251+
) -> dict[str, Any] | None:
223252
"""Convert a profile manifest plist object to a Jamf JSON schema manifest.
224253
225254
Reference: https://docs.jamf.com/technical-papers/jamf-pro/json-schema/10.19.0/Understanding_the_Structure_of_a_JSON_Schema_Manifest.html
226255
"""
256+
logger = logging.getLogger(__name__)
227257

228258
# Create schema object
229259
try:
@@ -233,20 +263,20 @@ def convert_to_jamf_manifest(data, property_order_increment=5):
233263
"properties": process_subkeys(data["pfm_subkeys"]),
234264
}
235265
except KeyError:
236-
print("ERROR: Manifest is missing a title, domain, or description.")
237-
return
266+
logger.error("Manifest is missing a title, domain, or description.")
267+
return None
238268

239269
# Lock property order
240270
if property_order_increment > 0:
241271
order = property_order_increment
242-
for property in schema["properties"]:
243-
schema["properties"][property]["property_order"] = order
272+
for prop_name in schema["properties"]:
273+
schema["properties"][prop_name]["property_order"] = order
244274
order += property_order_increment
245275

246276
return schema
247277

248278

249-
def write_to_file(path, data):
279+
def write_to_file(path: str, data: dict[str, Any]) -> None:
250280
"""Given a path to a file and JSON data, write the file."""
251281
path_head, path_tail = os.path.split(path)
252282

@@ -267,8 +297,9 @@ def write_to_file(path, data):
267297
)
268298

269299

270-
def update_readme(count):
300+
def update_readme(count: int) -> None:
271301
"""Updates README.md file with latest manifest count."""
302+
logger = logging.getLogger(__name__)
272303

273304
with open("README.md", encoding="utf-8") as f:
274305
readme = f.readlines()
@@ -281,16 +312,19 @@ def update_readme(count):
281312
break
282313
with open("README.md", "w", encoding="utf-8") as f:
283314
f.write("".join(readme))
284-
print("Updated README.md")
315+
logger.info("Updated README.md")
285316

286317

287-
def main():
318+
def main() -> None:
288319
"""Main process."""
289320

290321
# Parse command line arguments.
291322
argparser = build_argument_parser()
292323
args = validate_args(argparser.parse_args())
293324

325+
# Set up logging based on verbosity
326+
logger = setup_logging(args.verbose)
327+
294328
# Expand to full paths
295329
input_dir = os.path.expanduser(args.input_dir)
296330
output_dir = os.path.expanduser(args.output_dir)
@@ -303,14 +337,14 @@ def main():
303337

304338
# Iterate through manifests in the input path
305339
count = {"done": 0, "skipped": 0}
306-
for root, dirs, files in os.walk(input_dir):
340+
for root, _dirs, files in os.walk(input_dir):
307341
for name in files:
308342
if name.endswith(".plist"):
309343
relpath = os.path.relpath(os.path.join(root, name), start=input_dir)
310344

311345
# Output filename if in verbose mode
312346
if args.verbose > 0:
313-
print("Processing %s" % relpath)
347+
logger.info("Processing %s", relpath)
314348

315349
# Load manifest
316350
pfm_data = read_manifest_plist(os.path.join(root, name))
@@ -333,7 +367,9 @@ def main():
333367
write_to_file(output_path, manifest)
334368
count["done"] += 1
335369

336-
print("Converted %d files. Skipped %d files." % (count["done"], count["skipped"]))
370+
logger.info(
371+
"Converted %d files. Skipped %d files.", count["done"], count["skipped"]
372+
)
337373
update_readme(count["done"])
338374

339375

0 commit comments

Comments
 (0)