Skip to content

Commit 7f67455

Browse files
authored
Add nrfcloud-util base command (#84)
Several changes, related to adding a base command named `nrfcloud-util`, so users can run it via eg `uvx nrfcloud-util blahblah`, for aiding discoverability. - add the base command, and collect subcommand args into it - this is a bit awkward, because we want to preserve the old names for the base commands (don't require running each as `nrfcloud-utils claim_and_provision_device ...`, keep the old direct names `claim_and_provision_device ...`) - fix up pyproject.toml to adhere to PEP 621 (instead of using poetry-specific configs), so we can use `uv` as tooling if we want to instead of being fixed to `poetry` (poetry since version 2.0 has supported these, so there's no loss) - update poetry version to latest since I'm touching everything anyway!
1 parent 7e87afc commit 7f67455

16 files changed

Lines changed: 1298 additions & 97 deletions

.github/workflows/python.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
env:
1212
REGISTRY_NAME: ${{ vars.REGISTRY_NAME }}
1313
REGISTRY_URI: ${{ vars.REGISTRY_URI }}
14-
POETRY_VERSION: 2.2.1
14+
POETRY_VERSION: 2.3.4
1515

1616
jobs:
1717
build:

poetry.lock

Lines changed: 1066 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,33 @@
22
requires = ["poetry-core"]
33
build-backend = "poetry.core.masonry.api"
44

5-
[tool.poetry]
5+
[project]
66
name = "nrfcloud-utils"
77
version = "0.0.1"
88
description = "Scripts and utilities for working with the nRF Cloud"
9-
authors = ["Nordic Semiconductor ASA"]
109
license = "BSD-3-Clause"
1110
readme = "README.md"
12-
repository = "https://github.com/nRFCloud/utils"
13-
packages = [
14-
{ include = "nrfcloud_utils", from = "src" }
11+
requires-python = ">=3.10,<4.0"
12+
authors = [
13+
{ name = "Nordic Semiconductor ASA" }
14+
]
15+
dependencies = [
16+
"cbor2>=5.4.2.post1",
17+
"cryptography>=46.0.5",
18+
"cffi>=2.0.0",
19+
"requests>=2.32.0",
20+
"urllib3>=2.2.2",
21+
"semver>=3.0.0",
22+
"pyjwt>=2.3.0",
23+
"inquirer>=3.4.0",
24+
"nrfcredstore>=2.0.9",
1525
]
1626

17-
[tool.poetry.dependencies]
18-
python = "^3.10"
19-
cbor2 = "^5.4.2.post1"
20-
cryptography = "^46.0.5"
21-
cffi = "^2.0.0"
22-
requests = "^2.32.0"
23-
urllib3 = "^2.2.2"
24-
semver = "^3.0.0"
25-
pyjwt = "^2.3.0"
26-
inquirer = "^3.4.0"
27-
nrfcredstore = "^2.0.9"
28-
29-
[tool.poetry.group.dev.dependencies]
30-
pytest = "^7.3.1"
31-
pytest-cov = "^4.0.0"
32-
pytest-watch = "^4.2.0"
27+
[project.urls]
28+
repository = "https://github.com/nRFCloud/utils"
3329

34-
[tool.poetry.scripts]
30+
[project.scripts]
31+
nrfcloud-utils = "nrfcloud_utils.cli:run"
3532
claim_and_provision_device = "nrfcloud_utils.claim_and_provision_device:run"
3633
claim_devices = "nrfcloud_utils.claim_devices:run"
3734
create_ca_cert = "nrfcloud_utils.create_ca_cert:run"
@@ -44,5 +41,18 @@ nrf_cloud_device_mgmt = "nrfcloud_utils.nrf_cloud_device_mgmt:run"
4441
nrf_cloud_onboard = "nrfcloud_utils.nrf_cloud_onboard:run"
4542
nrf93_onboard = "nrfcloud_utils.nrf93_onboard:run"
4643

44+
[dependency-groups]
45+
dev = [
46+
"pytest>=7.3.1",
47+
"pytest-cov>=4.0.0",
48+
"pytest-watch>=4.2.0",
49+
"poetry (>=2.3.4,<3.0.0)", # used for development today. might be migrated to uv in the future.
50+
]
51+
52+
[tool.poetry]
53+
packages = [
54+
{ include = "nrfcloud_utils", from = "src" }
55+
]
56+
4757
[tool.pytest.ini_options]
4858
addopts = ["--import-mode=importlib"]

src/nrfcloud_utils/claim_and_provision_device.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727
IMEI_LEN = 15
2828
DEV_ID_MAX_LEN = 64
2929

30-
def parse_args(in_args):
30+
def get_parser():
3131
parser = argparse.ArgumentParser(description="nRF Cloud Claim and Provision",
32-
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
32+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
33+
add_help=False)
3334
parser_add_comms_args(parser)
3435
parser.add_argument("--dv", type=int, help="Number of days cert is valid",
3536
default=(10 * 365))
@@ -81,6 +82,11 @@ def parse_args(in_args):
8182
choices=['debug', 'info', 'warning', 'error', 'critical'],
8283
help='Set the logging level'
8384
)
85+
return parser
86+
87+
def parse_args(in_args):
88+
_p = get_parser()
89+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
8490
args = parser.parse_args(in_args)
8591
setup_logging(level=args.log_level, use_color=not args.plain)
8692
return args

src/nrfcloud_utils/claim_devices.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
IMEI_LEN = 15
1818
MAX_CSV_ROWS = 1000
1919

20-
def parse_args(in_args):
20+
def get_parser():
2121
parser = argparse.ArgumentParser(description="nRF Cloud Claim Devices",
22-
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
22+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
23+
add_help=False)
2324

2425
parser.add_argument("--csv", type=str,
2526
help="Filepath to attestation token CSV file",
@@ -39,6 +40,11 @@ def parse_args(in_args):
3940
choices=['debug', 'info', 'warning', 'error', 'critical'],
4041
help='Set the logging level'
4142
)
43+
return parser
44+
45+
def parse_args(in_args):
46+
_p = get_parser()
47+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
4248
args = parser.parse_args(in_args)
4349
setup_logging(level=args.log_level, use_color=not args.plain)
4450
return args

src/nrfcloud_utils/cli.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#!/usr/bin/env python3
2+
#
3+
# Copyright (c) 2025 Nordic Semiconductor ASA
4+
#
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
import argparse
7+
import sys
8+
9+
from nrfcloud_utils import (
10+
claim_and_provision_device,
11+
claim_devices,
12+
create_ca_cert,
13+
create_device_credentials,
14+
create_proxy_jwt,
15+
device_credentials_installer,
16+
gather_attestation_tokens,
17+
modem_credentials_parser,
18+
nrf_cloud_device_mgmt,
19+
nrf_cloud_onboard,
20+
nrf93_onboard,
21+
)
22+
23+
_MODULES = {
24+
"claim_and_provision_device": claim_and_provision_device,
25+
"claim_devices": claim_devices,
26+
"create_ca_cert": create_ca_cert,
27+
"create_device_credentials": create_device_credentials,
28+
"create_proxy_jwt": create_proxy_jwt,
29+
"device_credentials_installer": device_credentials_installer,
30+
"gather_attestation_tokens": gather_attestation_tokens,
31+
"modem_credentials_parser": modem_credentials_parser,
32+
"nrf_cloud_device_mgmt": nrf_cloud_device_mgmt,
33+
"nrf_cloud_onboard": nrf_cloud_onboard,
34+
"nrf93_onboard": nrf93_onboard,
35+
}
36+
37+
38+
def _build_parser():
39+
parser = argparse.ArgumentParser(
40+
prog="nrfcloud-utils",
41+
description="Scripts and utilities for working with nRF Cloud",
42+
)
43+
subparsers = parser.add_subparsers(dest="command", metavar="command", required=False)
44+
for name, mod in _MODULES.items():
45+
_p = mod.get_parser()
46+
subparsers.add_parser(
47+
name,
48+
parents=[_p],
49+
description=_p.description,
50+
formatter_class=_p.formatter_class,
51+
help=_p.description,
52+
)
53+
return parser
54+
55+
56+
def run():
57+
parser = _build_parser()
58+
# parse_args() handles --help at both levels and validates the subcommand name
59+
# and its args (using each module's get_parser() for the subparser definition).
60+
# We then re-dispatch to the module's own main() so setup_logging and other
61+
# module init runs exactly as it does when the command is called standalone.
62+
args = parser.parse_args()
63+
64+
if not args.command:
65+
parser.print_help()
66+
sys.exit(0)
67+
68+
cmd = args.command
69+
sys.argv = [cmd] + sys.argv[2:]
70+
_MODULES[cmd].main(sys.argv[1:])
71+
72+
73+
if __name__ == "__main__":
74+
run()

src/nrfcloud_utils/create_ca_cert.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
2727

2828
logger = logging.getLogger(__name__)
2929

30-
def parse_args(in_args):
31-
parser = argparse.ArgumentParser(description="Create CA Certificate")
30+
def get_parser():
31+
parser = argparse.ArgumentParser(description="Create CA Certificate",
32+
add_help=False)
3233
parser.add_argument("-c", type=str, help="2 character country code", default="NO")
3334
parser.add_argument("--st", type=str, help="State or Province", default="")
3435
parser.add_argument("-l", type=str, help="Locality", default="")
@@ -50,6 +51,11 @@ def parse_args(in_args):
5051
choices=['debug', 'info', 'warning', 'error', 'critical'],
5152
help='Set the logging level'
5253
)
54+
return parser
55+
56+
def parse_args(in_args):
57+
_p = get_parser()
58+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
5359
args = parser.parse_args(in_args)
5460
setup_logging(level=args.log_level)
5561
return args

src/nrfcloud_utils/create_device_credentials.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424

2525
logger = logging.getLogger(__name__)
2626

27-
def parse_args(in_args):
28-
parser = argparse.ArgumentParser(description="Create Device Credentials")
27+
def get_parser():
28+
parser = argparse.ArgumentParser(description="Create Device Credentials",
29+
add_help=False)
2930
parser.add_argument("--ca", type=str, required=True, help="Filepath to your CA cert PEM", default="")
3031
parser.add_argument("--ca-key", type=str, required=True, help="Filepath to your CA's private key PEM", default="")
3132
parser.add_argument("-c", type=str, help="2 character country code; required if CSR is not provided", default="NO")
@@ -55,6 +56,11 @@ def parse_args(in_args):
5556
choices=['debug', 'info', 'warning', 'error', 'critical'],
5657
help='Set the logging level'
5758
)
59+
return parser
60+
61+
def parse_args(in_args):
62+
_p = get_parser()
63+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
5864
args = parser.parse_args(in_args)
5965
setup_logging(level=args.log_level)
6066
if len(args.csr) == 0 and len(args.cn) == 0:

src/nrfcloud_utils/create_proxy_jwt.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414

1515
logger = logging.getLogger(__name__)
1616

17-
def parse_args(in_args):
17+
def get_parser():
1818
parser = argparse.ArgumentParser(description="Create JWT for proxy (cloud-to-cloud) requests to nRF Cloud",
19-
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
19+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
20+
add_help=False)
2021

2122
parser.add_argument("--key", type=str, required=True,
2223
help="Required filepath to the ES256 private key PEM (Service Key) used for JWT signing. \
@@ -43,6 +44,11 @@ def parse_args(in_args):
4344
choices=['debug', 'info', 'warning', 'error', 'critical'],
4445
help='Set the logging level'
4546
)
47+
return parser
48+
49+
def parse_args(in_args):
50+
_p = get_parser()
51+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
4652
args = parser.parse_args(in_args)
4753
setup_logging(level=args.log_level)
4854
return args

src/nrfcloud_utils/device_credentials_installer.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@
2727
MIN_REQD_MFW_VER = "1.3.0"
2828
MIN_REQD_MFW_VER_FOR_VERIFY = "1.3.2"
2929

30-
def parse_args(in_args):
30+
def get_parser():
3131
parser = argparse.ArgumentParser(description="Device Credentials Installer",
32-
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
32+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
33+
add_help=False)
3334
parser_add_comms_args(parser)
3435
parser.add_argument("--dv", type=int, help="Number of days cert is valid",
3536
default=(10 * 365))
@@ -105,6 +106,11 @@ def parse_args(in_args):
105106
choices=['debug', 'info', 'warning', 'error', 'critical'],
106107
help='Set the logging level'
107108
)
109+
return parser
110+
111+
def parse_args(in_args):
112+
_p = get_parser()
113+
parser = argparse.ArgumentParser(parents=[_p], description=_p.description, formatter_class=_p.formatter_class)
108114
args = parser.parse_args(in_args)
109115
setup_logging(level=args.log_level, use_color=not args.plain)
110116
return args

0 commit comments

Comments
 (0)