Skip to content

Commit 1100ab9

Browse files
author
Paul K. Korir, PhD
committed
compute a check digit for a noid
* new option `-d/--check-digit` * reassigned `-v` for verbose; now `-V` for validate * better test coverage * extended documentation
1 parent 1d12b26 commit 1100ab9

File tree

5 files changed

+142
-15
lines changed

5 files changed

+142
-15
lines changed

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ noid
2525
There are various options available using `-h/--help`:
2626
```shell
2727
noid -h
28-
usage: noid [-h] [-c CONFIG_FILE] [-v] [-s SCHEME] [-N NAA] [-t TEMPLATE] [-n INDEX] [--verbose] [noid]
28+
usage: noid [-h] [-c CONFIG_FILE] [-V | -d] [-s SCHEME] [-N NAA] [-t TEMPLATE] [-n INDEX] [-v] [noid]
2929

30-
generate nice and opaque identifiers (noids)
30+
generate nice and opaque identifiers
3131

3232
positional arguments:
3333
noid a noid
@@ -36,15 +36,16 @@ optional arguments:
3636
-h, --help show this help message and exit
3737
-c CONFIG_FILE, --config-file CONFIG_FILE
3838
path to a config file with a noid section
39-
-v, --validate validate the given noid [default: False]
39+
-V, --validate validate the given noid [default: False]
40+
-d, --check-digit compute and print the corresponding check digit for the given noid [default: False]
4041
-s SCHEME, --scheme SCHEME
4142
the noid scheme [default: 'ark:/']
4243
-N NAA, --naa NAA the name assigning authority (NAA) number [default: ]
4344
-t TEMPLATE, --template TEMPLATE
4445
the template by which to generate noids [default: 'zeeddk']
4546
-n INDEX, --index INDEX
4647
a number for which to generate a valid noid [default: random positive integer]
47-
--verbose turn on verbose text [default: False]
48+
-v, --verbose turn on verbose text [default: False]
4849

4950
```
5051

@@ -54,6 +55,13 @@ Validate a noid using the `-v/--validate` flag and pass a noid.
5455
noid -v $(noid) # self-validation
5556
```
5657

58+
### Compute the check digit for a noid
59+
Compute the check digit using `-d/--check-digit` flag and pass a noid.
60+
```shell
61+
noid -d $(noid -t zeee -n 1234) && noid -t zeee -n 1234 && noid -t zeeek -n 1234
62+
```
63+
The example above prints out the check digit, the full noid without a check digit and the full noid with a check digit.
64+
5765
### Options
5866
#### Specify the NAA
5967
Use the `-N/--naa` option.

noid/cli.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,19 @@
2222
'-c', '--config-file',
2323
help="path to a config file with a noid section"
2424
)
25-
parser.add_argument(
26-
'-v', '--validate',
25+
noid_action = parser.add_mutually_exclusive_group()
26+
noid_action.add_argument(
27+
'-V', '--validate',
2728
action='store_true',
2829
default=False,
2930
help="validate the given noid [default: False]"
3031
)
32+
noid_action.add_argument(
33+
'-d', '--check-digit',
34+
action='store_true',
35+
default=False,
36+
help="compute and print the corresponding check digit for the given noid [default: False]"
37+
)
3138
parser.add_argument(
3239
'-s', '--scheme',
3340
default=DEFAULT_SCHEME,
@@ -50,7 +57,7 @@
5057
help="a number for which to generate a valid noid [default: random positive integer]"
5158
)
5259
parser.add_argument(
53-
'--verbose',
60+
'-v', '--verbose',
5461
action='store_true',
5562
default=False,
5663
help="turn on verbose text [default: False]"
@@ -94,12 +101,13 @@ def parse_args():
94101
if o in configs['noid']:
95102
setattr(args, o, configs.get('noid', o))
96103
else:
97-
print(f"warning: configs missing option '{o}'; using default value ({getattr(args, o)})", file=sys.stderr)
104+
print(f"warning: configs missing option '{o}'; using default value ({getattr(args, o)})",
105+
file=sys.stderr)
98106
else:
99107
print(f"warning: config file '{args.config_file}' lacks 'noid' section; ignoring config file",
100108
file=sys.stderr)
101109
# argument validation
102-
if args.validate and args.noid is None:
110+
if (args.validate or args.check_digit) and args.noid is None:
103111
print("error: missing noid to validate", file=sys.stderr)
104112
return None
105113
return args

noid/pynoid.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from noid import utils, cli
66

77

8-
def mint(template='zek', n=-1, scheme='', naa='') -> str:
8+
def mint(template: str = 'zek', n: int = -1, scheme: str = '', naa: str = '') -> str:
99
""" Mint identifiers according to template with a prefix of scheme + naa.
1010
1111
:param str template: a string consisting of GENTYPE + (DIGTYPE)+ [+ CHECKDIGIT]
@@ -52,7 +52,7 @@ def mint(template='zek', n=-1, scheme='', naa='') -> str:
5252
return noid
5353

5454

55-
def generate_noid(mask:str, n:int) -> str:
55+
def generate_noid(mask: str, n: int) -> str:
5656
"""The actual noid generation
5757
5858
:param str mask: the mask string
@@ -134,6 +134,7 @@ def index_of(x):
134134
try:
135135
return utils.XDIGIT.index(x)
136136
except:
137+
print(f"error: invalid character '{x}'; digits should be in '{''.join(utils.XDIGIT)}'", file=sys.stderr)
137138
return 0
138139

139140
total = list()
@@ -152,6 +153,11 @@ def main():
152153
if args.verbose:
153154
print(f"info: validating '{args.noid}'...", file=sys.stderr)
154155
print(f"'{args.noid}' valid? {validate(args.noid)}")
156+
elif args.check_digit:
157+
if args.verbose:
158+
print(f"info: computing check digit for '{args.noid}'...", file=sys.stderr)
159+
check_digit = calculate_check_digit(args.noid)
160+
print(check_digit)
155161
else:
156162
if args.verbose:
157163
print(f"info: generating noid using template={args.template}, n={args.index}, "

setup.py

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

66
setuptools.setup(
77
name="noid",
8-
version="1.0.1",
8+
version="1.1.0",
99
author="Paul K. Korir",
1010
author_email="pkorir@ebi.ac.uk, paulkorir@gmail.com",
1111
description="Mint NOIDs using a CLI or API",

test/test.py

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,17 @@
2727
noid -n/--index
2828
2929
"""
30+
import io
31+
import os
32+
import pathlib
3033
import random
3134
import re
35+
import sys
36+
import tempfile
3237
import unittest
3338

3439
from noid import cli, pynoid, utils
3540

36-
import pathlib
37-
3841
BASE_DIR = pathlib.Path(__file__).parent.parent
3942
CONFIG_FILE = BASE_DIR / 'noid' / 'noid.cfg'
4043

@@ -53,7 +56,16 @@ def test_default(self):
5356

5457
def test_validate(self):
5558
"""Validation requires noid positional argument"""
56-
self.assertIsNone(cli.cli(f"noid -v"))
59+
self.assertIsNone(cli.cli(f"noid -V"))
60+
61+
def test_check_digit(self):
62+
"""Show the check digit for the given string"""
63+
args = cli.cli(f"noid -d 123456")
64+
self.assertEqual('123456', args.noid)
65+
self.assertTrue(args.check_digit)
66+
# you cannot validate and check at the same time
67+
with self.assertRaises(SystemExit):
68+
cli.cli(f"noid -d -V 123456")
5769

5870
def test_config(self):
5971
"""Using a config file"""
@@ -66,6 +78,37 @@ def test_config(self):
6678
self.assertEqual('zeeeeddddk', args.template)
6779
self.assertEqual(-1, args.index)
6880
self.assertFalse(args.verbose)
81+
sys.stdout = io.StringIO()
82+
configs = cli.read_configs(args)
83+
print(configs)
84+
self.assertRegex(sys.stdout.getvalue(), r"(?ms:.*[[]noid[]].*template.*scheme.*naa.*)")
85+
86+
def test_missing_noid_validate(self):
87+
"""Print error for validating a blank noid"""
88+
args = cli.cli(f"noid -V")
89+
self.assertIsNone(args)
90+
91+
def test_invalid_configs(self):
92+
"""Use defaults when configs invalid"""
93+
# case 1: no 'noid' section
94+
_configs = """[noids]\ntemplate = zeedddeeek\nscheme = doi:\nnaa = 1234\n"""
95+
temp_configs = tempfile.NamedTemporaryFile()
96+
with open(temp_configs.name, 'w') as f:
97+
print(_configs, file=f)
98+
sys.stdout = sys.stderr = io.StringIO()
99+
cli.cli(f"noid -c {temp_configs.name}")
100+
self.assertRegex(sys.stderr.getvalue(), r"(?ms:^warning: config file .* lacks 'noid' section.*ignoring.*)")
101+
# case 2: some sections missing
102+
_configs = """[noid]\ntemplates = zeedddeeek\nschemes = doi:\nnaat = 1234\n"""
103+
temp_configs = tempfile.NamedTemporaryFile()
104+
with open(temp_configs.name, 'w') as f:
105+
print(_configs, file=f)
106+
sys.stderr = io.StringIO()
107+
cli.cli(f"noid -c {temp_configs.name}")
108+
self.assertRegex(
109+
sys.stderr.getvalue(),
110+
r"(?ms:^warning: configs missing option 'template'.*missing option 'scheme'.*missing option 'naa'.*)"
111+
)
69112

70113

71114
class PynoidAPI(unittest.TestCase):
@@ -125,6 +168,68 @@ def test_mint_invalid_template(self):
125168
self.assertEqual('', noid)
126169

127170

171+
class PynoidNoid(unittest.TestCase):
172+
"""Tests for the entry point"""
173+
174+
def test_default(self):
175+
"""The result of simply calling 'noid'"""
176+
cli.cli(f"noid -v")
177+
sys.stdout = sys.stderr = io.StringIO()
178+
pynoid.main()
179+
self.assertRegex(sys.stdout.getvalue(), r"(?ms:^info: generating noid.*template=.*scheme=.*ark[:][/][\w\d]+)")
180+
181+
def test_error(self):
182+
"""Exit status on *nix is os.EX_USAGE"""
183+
cli.cli(f"noid -V")
184+
ex = pynoid.main()
185+
self.assertEqual(os.EX_USAGE, ex)
186+
187+
def test_config(self):
188+
"""Using config"""
189+
cli.cli(f"noid -v -c {CONFIG_FILE}")
190+
sys.stdout = sys.stderr = io.StringIO()
191+
pynoid.main()
192+
self.assertRegex(
193+
sys.stdout.getvalue(),
194+
r"(?ms:^info: generating noid.*template=zeeeeddddk.*scheme="
195+
r"http[:][/][/].*naa=83812.*http[:][/][/]83812[/][\w\d]+)"
196+
)
197+
198+
def test_validate(self):
199+
"""Using check digit"""
200+
noid = 'xg64G'
201+
cli.cli(f"noid -v -V {noid}")
202+
sys.stdout = sys.stderr = io.StringIO()
203+
pynoid.main()
204+
self.assertRegex(sys.stdout.getvalue(), rf"(?ms:^info: validating '{noid}'.*'{noid}' valid[?] True)")
205+
206+
def test_check_digit(self):
207+
"""Using check digit"""
208+
noid = '123456'
209+
check_digit = pynoid.calculate_check_digit(noid)
210+
cli.cli(f"noid -v -d {noid}")
211+
sys.stdout = sys.stderr = io.StringIO()
212+
pynoid.main()
213+
self.assertRegex(sys.stdout.getvalue(), rf"(?ms:^info: computing check digit for '{noid}'.*{check_digit})")
214+
215+
def test_scheme_naa_template(self):
216+
"""Minting options"""
217+
cli.cli(f"noid -v -t zeeddk -s https:// -N 54321")
218+
sys.stdout = sys.stderr = io.StringIO()
219+
pynoid.main()
220+
self.assertRegex(sys.stdout.getvalue(),
221+
r"(?ms:^info: generating noid.*template=zeeddk.*scheme=https[:][/][/].*naa=54321.*https[:][/][/]54321[/][\w\d]+)")
222+
223+
def test_index(self):
224+
"""Set index"""
225+
index = random.randint(1000, 2000)
226+
cli.cli(f"noid -v -n {index}")
227+
sys.stdout = sys.stderr = io.StringIO()
228+
pynoid.main()
229+
self.assertRegex(sys.stdout.getvalue(),
230+
rf"(?ms:^info: generating noid.*template=zeeddk.*n={index}.*scheme=ark[:][/].*naa=.*ark[:][/][\w\d]+)")
231+
232+
128233
class PynoidUtils(unittest.TestCase):
129234
def test_validate_mask(self):
130235
"""Validate a mask"""

0 commit comments

Comments
 (0)