Compare commits
18
Commits
20359ce455
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12e68b4dae | ||
|
|
bdb7ce0469 | ||
|
|
2d4870b0e8 | ||
|
|
d09a4e9280 | ||
|
|
931e774fb7 | ||
|
|
8d2f0a6519 | ||
|
|
0fffca4e9a | ||
|
|
c4fdb3a0d3 | ||
|
|
e971ac700f | ||
|
|
e39d177f45 | ||
|
|
9ba2ce9d6b | ||
|
|
17908b55e8 | ||
|
|
f702f99b3b | ||
|
|
ff678d71d1 | ||
|
|
8e42faf6af | ||
|
|
10f0555011 | ||
|
|
832cf45441 | ||
|
|
1e05501c1e |
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from noid import Minter
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(prog="noid", description="Generate noid, archival persistent identifiers")
|
||||||
|
subparsers = parser.add_subparsers(dest="command")
|
||||||
|
new_sp = subparsers.add_parser("new")
|
||||||
|
new_sp.add_argument("name")
|
||||||
|
new_sp.add_argument("template")
|
||||||
|
mint_sp = subparsers.add_parser("mint")
|
||||||
|
mint_sp.add_argument("name")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
match args.command:
|
||||||
|
case "new":
|
||||||
|
minter = Minter()
|
||||||
|
if (total := minter.parse_template(args.template)) > 0:
|
||||||
|
minter.initcounters()
|
||||||
|
print(f"The minter is ready. {total} noid are ready to be minted.")
|
||||||
|
else:
|
||||||
|
print("Someting happened, probably a problem with the template: {args.template}")
|
||||||
|
minter.store(f"{args.name}.noid")
|
||||||
|
case "mint":
|
||||||
|
minter = Minter.load(f"{args.name}.noid")
|
||||||
|
print(f"New noid: {minter.genid()}")
|
||||||
|
minter.store(f"{args.name}.noid")
|
||||||
|
case _:
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
from dataclass_persistence import Persistent
|
from dataclass_persistence import Persistent
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import ClassVar
|
from typing import ClassVar
|
||||||
@@ -7,6 +9,7 @@ from typing import ClassVar
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Counter():
|
class Counter():
|
||||||
|
rank: int
|
||||||
top: int
|
top: int
|
||||||
value: int
|
value: int
|
||||||
|
|
||||||
@@ -14,6 +17,7 @@ class Counter():
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Minter(Persistent):
|
class Minter(Persistent):
|
||||||
legalstring: ClassVar[str] = "0123456789bcdfghjkmnpqrstvwxz"
|
legalstring: ClassVar[str] = "0123456789bcdfghjkmnpqrstvwxz"
|
||||||
|
xdig: tuple[str] = tuple(legalstring)
|
||||||
alphacount: ClassVar[int] = len(legalstring)
|
alphacount: ClassVar[int] = len(legalstring)
|
||||||
digitcount: ClassVar[int] = 10
|
digitcount: ClassVar[int] = 10
|
||||||
|
|
||||||
@@ -71,7 +75,18 @@ class Minter(Persistent):
|
|||||||
# - value + (index * percounter)
|
# - value + (index * percounter)
|
||||||
# - mask
|
# - mask
|
||||||
# 10. return the noid
|
# 10. return the noid
|
||||||
return ""
|
if self.oacounter >= self.oatop:
|
||||||
|
raise Exception("noid are exhausted.")
|
||||||
|
s = len(self.active_counters)
|
||||||
|
if s < 1:
|
||||||
|
raise Exception("noid are exhausted.")
|
||||||
|
idx = random.randrange(s)
|
||||||
|
c = self.active_counters[idx]
|
||||||
|
c.value += 1
|
||||||
|
self.oacounter += 1
|
||||||
|
if c.value >= c.top:
|
||||||
|
self.active_counters.pop(idx)
|
||||||
|
return self._n2xdig(c.value + (c.rank * self.percounter))
|
||||||
|
|
||||||
def initcounters(self) -> None:
|
def initcounters(self) -> None:
|
||||||
# 1. [X] Initialize oacounter to 0
|
# 1. [X] Initialize oacounter to 0
|
||||||
@@ -89,14 +104,18 @@ class Minter(Persistent):
|
|||||||
# - set value of this counter to 0
|
# - set value of this counter to 0
|
||||||
# - add the counter to saclist
|
# - add the counter to saclist
|
||||||
# - substract pctr from t
|
# - substract pctr from t
|
||||||
|
self.active_counters = []
|
||||||
|
self.inactive_counters = []
|
||||||
maxcounters = 293
|
maxcounters = 293
|
||||||
self.percounter = int(self.oatop / maxcounters + 1)
|
self.percounter = int(self.oatop / maxcounters + 1)
|
||||||
t = self.oatop
|
t = self.oatop
|
||||||
|
n = 0
|
||||||
while t > 0:
|
while t > 0:
|
||||||
top = self.percounter if t >= self.percounter else t
|
top = self.percounter if t >= self.percounter else t
|
||||||
c = Counter(top=top, value=0)
|
c = Counter(top=top, value=0, rank=n)
|
||||||
self.active_counters.append(c)
|
self.active_counters.append(c)
|
||||||
t -= self.percounter
|
t -= self.percounter
|
||||||
|
n += 1
|
||||||
|
|
||||||
def _n2xdig(self, num) -> str:
|
def _n2xdig(self, num) -> str:
|
||||||
# 1. Initialize s to ''
|
# 1. Initialize s to ''
|
||||||
@@ -118,10 +137,71 @@ class Minter(Persistent):
|
|||||||
# - s = self.legalstring[remainder] + s
|
# - s = self.legalstring[remainder] + s
|
||||||
# 7. if mask contains a 'k' then add a '+' at the end of s
|
# 7. if mask contains a 'k' then add a '+' at the end of s
|
||||||
# 8. return s
|
# 8. return s
|
||||||
return ""
|
s = ""
|
||||||
|
varwidth = 0
|
||||||
|
rmask = list(self.template)
|
||||||
|
rmask.reverse()
|
||||||
|
while num != 0 or not varwidth:
|
||||||
|
if not varwidth:
|
||||||
|
try:
|
||||||
|
c = rmask.pop(0)
|
||||||
|
except IndexError:
|
||||||
|
print(self)
|
||||||
|
break
|
||||||
|
match c:
|
||||||
|
case "r":
|
||||||
|
break
|
||||||
|
case "s":
|
||||||
|
break
|
||||||
|
case "e":
|
||||||
|
div = self.alphacount
|
||||||
|
case "d":
|
||||||
|
div = self.digitcount
|
||||||
|
case "z":
|
||||||
|
varwidth = 1
|
||||||
|
continue
|
||||||
|
case "k":
|
||||||
|
continue
|
||||||
|
remainder = num % div
|
||||||
|
num = int(num / div)
|
||||||
|
s = self.xdig[remainder] + s
|
||||||
|
if self.template[-1] == "k":
|
||||||
|
s += "+"
|
||||||
|
return s
|
||||||
|
|
||||||
def _checkbar(self, id):
|
def _checkchar(self, id):
|
||||||
pass
|
# 1. Return undef if id is not set
|
||||||
|
# 2. init lastchar to last char of the id, and remove that char from id
|
||||||
|
# 3. init pos to 1
|
||||||
|
# 4. init sum to 0
|
||||||
|
# 5. declare c
|
||||||
|
# 6. for each character of the id
|
||||||
|
# - get the ordinal value of the character
|
||||||
|
# - increment sum with the pos * the index value of the char in legalstring
|
||||||
|
# - increment pos
|
||||||
|
# 7. init checkchar to the value of xdig[sum % alphacount]
|
||||||
|
# 8. return the concat of id and checkchar is lastchar equal to '+' or checkchar
|
||||||
|
# 9. return None
|
||||||
|
if id is None:
|
||||||
|
return None
|
||||||
|
lastchar = id[-1]
|
||||||
|
sum = 0
|
||||||
|
for idx, char in enumerate(id[:-1], 1):
|
||||||
|
sum += idx * (self.legalstring.index(char) if self.legalstring.index(char) else 0)
|
||||||
|
checkchar = self.xdig[sum % self.alphacount]
|
||||||
|
if lastchar == "+" or lastchar == checkchar:
|
||||||
|
return id[:-1] + checkchar
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def mint(self, n=1):
|
||||||
|
for _ in range(n):
|
||||||
|
_id = self.genid()
|
||||||
|
if self.template[-1] == "k":
|
||||||
|
_id = self._checkchar(_id)
|
||||||
|
if _id is not None:
|
||||||
|
return self.prefix + _id
|
||||||
|
return self.prefix + _id
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from noid import Minter
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
dedupe = Counter()
|
||||||
|
noid = Minter()
|
||||||
|
noid.parse_template("rek")
|
||||||
|
print(noid.template)
|
||||||
|
noid.initcounters()
|
||||||
|
print(noid.oatop)
|
||||||
|
print(noid.active_counters)
|
||||||
|
print(noid.oacounter)
|
||||||
|
print(len(noid.active_counters))
|
||||||
|
print("---" * 10)
|
||||||
|
for i in range(noid.oatop + 2):
|
||||||
|
print(f"--> {i=}")
|
||||||
|
try:
|
||||||
|
n = noid.mint()
|
||||||
|
print("noid:", n)
|
||||||
|
print(noid.active_counters)
|
||||||
|
print(noid.inactive_counters)
|
||||||
|
dedupe.update([n])
|
||||||
|
except Exception:
|
||||||
|
print(noid)
|
||||||
|
print("Noid are exhausted")
|
||||||
|
print(f"{dedupe=}")
|
||||||
+17
-18
@@ -20,11 +20,11 @@ class TestInitCounters:
|
|||||||
m.oatop = 293
|
m.oatop = 293
|
||||||
m.initcounters()
|
m.initcounters()
|
||||||
|
|
||||||
assert len(m.active_counters) == 1
|
assert len(m.active_counters) == 147
|
||||||
assert m.active_counters[0].top == 293
|
assert m.active_counters[0].top == 2
|
||||||
assert m.active_counters[0].value == 0
|
assert m.active_counters[0].value == 0
|
||||||
assert m.oacounter == 0
|
assert m.oacounter == 0
|
||||||
assert m.percounter == 0 # 293 / 293 + 1 = 2 (mais l'algo donne 1)
|
assert m.percounter == 2
|
||||||
|
|
||||||
def test_initcounters_with_one_more_than_maxcounters(self):
|
def test_initcounters_with_one_more_than_maxcounters(self):
|
||||||
"""Test avec oatop = 294 (maxcounters + 1)"""
|
"""Test avec oatop = 294 (maxcounters + 1)"""
|
||||||
@@ -32,12 +32,12 @@ class TestInitCounters:
|
|||||||
m.oatop = 294
|
m.oatop = 294
|
||||||
m.initcounters()
|
m.initcounters()
|
||||||
|
|
||||||
assert len(m.active_counters) == 2
|
assert len(m.active_counters) == 147
|
||||||
assert m.active_counters[0].top == 293
|
assert m.active_counters[0].top == 2
|
||||||
assert m.active_counters[1].top == 1
|
assert m.active_counters[1].top == 2
|
||||||
assert all(c.value == 0 for c in m.active_counters)
|
assert all(c.value == 0 for c in m.active_counters)
|
||||||
assert m.oacounter == 0
|
assert m.oacounter == 0
|
||||||
assert m.percounter == 2 # 294 / 293 + 1 = 2
|
assert m.percounter == 2
|
||||||
|
|
||||||
def test_initcounters_with_two_maxcounters(self):
|
def test_initcounters_with_two_maxcounters(self):
|
||||||
"""Test avec oatop = 586 (2 * maxcounters)"""
|
"""Test avec oatop = 586 (2 * maxcounters)"""
|
||||||
@@ -45,11 +45,11 @@ class TestInitCounters:
|
|||||||
m.oatop = 586
|
m.oatop = 586
|
||||||
m.initcounters()
|
m.initcounters()
|
||||||
|
|
||||||
assert len(m.active_counters) == 2
|
assert len(m.active_counters) == 196
|
||||||
assert all(c.top == 293 for c in m.active_counters)
|
assert all(c.top == 3 for c in m.active_counters[:-2])
|
||||||
assert all(c.value == 0 for c in m.active_counters)
|
assert all(c.value == 0 for c in m.active_counters)
|
||||||
assert m.oacounter == 0
|
assert m.oacounter == 0
|
||||||
assert m.percounter == 2 # 586 / 293 + 1 = 3 (mais l'algo donne 2)
|
assert m.percounter == 3
|
||||||
|
|
||||||
def test_initcounters_with_large_value(self):
|
def test_initcounters_with_large_value(self):
|
||||||
"""Test avec une grande valeur de oatop (1000)"""
|
"""Test avec une grande valeur de oatop (1000)"""
|
||||||
@@ -59,7 +59,7 @@ class TestInitCounters:
|
|||||||
|
|
||||||
# 1000 / 293 ≈ 3.41 → percounter = 4
|
# 1000 / 293 ≈ 3.41 → percounter = 4
|
||||||
# 1000 / 4 = 250 compteurs
|
# 1000 / 4 = 250 compteurs
|
||||||
expected_count = (1000 + 293 - 1) // 293 # Division entière arrondie vers le haut
|
expected_count = int(1000 / 4) # Division entière arrondie vers le haut
|
||||||
assert len(m.active_counters) == expected_count
|
assert len(m.active_counters) == expected_count
|
||||||
|
|
||||||
# Vérification de la somme des tops
|
# Vérification de la somme des tops
|
||||||
@@ -67,9 +67,8 @@ class TestInitCounters:
|
|||||||
assert total == 1000
|
assert total == 1000
|
||||||
|
|
||||||
# Vérification que tous les compteurs sauf le dernier ont top = 293
|
# Vérification que tous les compteurs sauf le dernier ont top = 293
|
||||||
for i in range(expected_count - 1):
|
for i in range(expected_count):
|
||||||
assert m.active_counters[i].top == 293
|
assert m.active_counters[i].top == 4
|
||||||
assert m.active_counters[-1].top == 1000 - 293 * (expected_count - 1)
|
|
||||||
|
|
||||||
assert all(c.value == 0 for c in m.active_counters)
|
assert all(c.value == 0 for c in m.active_counters)
|
||||||
assert m.oacounter == 0
|
assert m.oacounter == 0
|
||||||
@@ -85,7 +84,7 @@ class TestInitCounters:
|
|||||||
m.oatop = 200
|
m.oatop = 200
|
||||||
m.initcounters()
|
m.initcounters()
|
||||||
|
|
||||||
assert len(m.active_counters) == (200 + 293 - 1) // 293
|
assert len(m.active_counters) == 200
|
||||||
assert m.oacounter == 0
|
assert m.oacounter == 0
|
||||||
assert all(c.value == 0 for c in m.active_counters)
|
assert all(c.value == 0 for c in m.active_counters)
|
||||||
|
|
||||||
@@ -103,10 +102,10 @@ class TestInitCounters:
|
|||||||
def test_initcounters_percounter_calculation(self):
|
def test_initcounters_percounter_calculation(self):
|
||||||
"""Test que percounter est calculé correctement"""
|
"""Test que percounter est calculé correctement"""
|
||||||
test_cases = [
|
test_cases = [
|
||||||
(0, 0),
|
(0, 1),
|
||||||
(293, 1), # 293 / 293 + 1 = 2 mais l'algo donne 1
|
(293, 2), # 293 / 293 + 1 = 2
|
||||||
(294, 2), # 294 / 293 + 1 = 2
|
(294, 2), # 294 / 293 + 1 = 2
|
||||||
(586, 2), # 586 / 293 + 1 = 3 mais l'algo donne 2
|
(586, 3), # 586 / 293 + 1 = 3
|
||||||
(1000, 4), # 1000 / 293 + 1 = 4
|
(1000, 4), # 1000 / 293 + 1 = 4
|
||||||
(10000, 35) # 10000 / 293 + 1 = 35
|
(10000, 35) # 10000 / 293 + 1 = 35
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user