sep2tools 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sep2tools/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """ Useful functions for working with IEEE 2030.5 (SEP2)"""
2
+
3
+ from .cert_id import get_der_certificate_lfdi, get_pem_certificate_lfdi
4
+ from .ids import generate_mrid, proxy_device_lfdi
5
+ from .version import __version__
6
+
7
+ __all__ = [
8
+ "__version__",
9
+ "get_pem_certificate_lfdi",
10
+ "get_der_certificate_lfdi",
11
+ "proxy_device_lfdi",
12
+ "generate_mrid",
13
+ ]
@@ -0,0 +1,175 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import asn1
7
+ from cryptography import x509
8
+ from cryptography.hazmat.primitives import serialization
9
+ from cryptography.hazmat.primitives.asymmetric import ec
10
+ from cryptography.hazmat.primitives.hashes import SHA256
11
+ from cryptography.x509.oid import ObjectIdentifier
12
+ from dateutil import tz
13
+
14
+ log = logging.getLogger(__name__)
15
+
16
+ DEFAULT_KEY_PATH = Path("key.pem")
17
+ DEFAULT_CSR_PATH = Path("csr.pem")
18
+
19
+ # IEEE 2030.5 device type assignments (Section 6.11.7.2)
20
+ SEP2_DEV_GENERIC = ObjectIdentifier("1.3.6.1.4.1.40732.1.1")
21
+ SEP2_DEV_MOBILE = ObjectIdentifier("1.3.6.1.4.1.40732.1.2")
22
+ SEP2_DEV_POSTMANUF = ObjectIdentifier("1.3.6.1.4.1.40732.1.3")
23
+
24
+ # IEEE 2030.5 policy assignments (Section 6.11.7.3)
25
+ SEP2_TEST_CERT = ObjectIdentifier("1.3.6.1.4.1.40732.2.1")
26
+ SEP2_TEST_SELFSIGNED = ObjectIdentifier("1.3.6.1.4.1.40732.2.2")
27
+ SEP2_TEST_SERVPROV = ObjectIdentifier("1.3.6.1.4.1.40732.2.3")
28
+ SEP2_TEST_BULK_CERT = ObjectIdentifier("1.3.6.1.4.1.40732.2.4")
29
+
30
+ # HardwareModuleName (Section 6.11.7.4)
31
+ SEP2_HARDWARE_MODULE_NAME = ObjectIdentifier("1.3.6.1.5.5.7.8.4")
32
+
33
+
34
+ def generate_key_and_csr(
35
+ key_file: Path = DEFAULT_KEY_PATH, csr_file: Path = DEFAULT_CSR_PATH
36
+ ) -> tuple[Path, Path]:
37
+ """Generate a Private Key and Certificate Signing Request (CSR)"""
38
+ key = ec.generate_private_key(ec.SECP256R1)
39
+ key_pem = key.private_bytes(
40
+ encoding=serialization.Encoding.PEM,
41
+ format=serialization.PrivateFormat.PKCS8,
42
+ encryption_algorithm=serialization.NoEncryption(),
43
+ )
44
+ with open(key_file, "wb") as fh:
45
+ fh.write(key_pem)
46
+ log.info("Created Key at %s", key_file)
47
+
48
+ subject_name = ""
49
+ csr = (
50
+ x509.CertificateSigningRequestBuilder()
51
+ .subject_name(x509.Name(subject_name))
52
+ .sign(key, SHA256())
53
+ )
54
+ csr_pem = csr.public_bytes(serialization.Encoding.PEM)
55
+ with open(csr_file, "wb") as fh:
56
+ fh.write(csr_pem)
57
+ log.info("Created CSR at %s", csr_file)
58
+ return key_file, csr_file
59
+
60
+
61
+ def convert_pem_to_der(filename: Path) -> Path:
62
+ """Convert a PEM file to DER Certificate"""
63
+ with open(filename, "rb") as pem_file:
64
+ cert_data = pem_file.read()
65
+ cert = x509.load_pem_x509_certificate(cert_data)
66
+ der_data = cert.public_bytes(encoding=serialization.Encoding.DER)
67
+ output_der_path = filename.with_suffix(".der")
68
+ with open(output_der_path, "wb") as fh:
69
+ fh.write(der_data)
70
+ return output_der_path
71
+
72
+
73
+ def generate_device_certificate(
74
+ filename: Path,
75
+ csr_path: Path,
76
+ mica_cert_path: Path,
77
+ mica_key_path: Path,
78
+ hardware_type_oid: str,
79
+ hardware_serial_number: str,
80
+ policy_oids: list[ObjectIdentifier],
81
+ serca_cert_path: Optional[Path] = None,
82
+ ) -> Path:
83
+ """Use a CSR and MICA key pair to generate a SEP2 Certificate"""
84
+
85
+ with open(csr_path, "rb") as pem_file:
86
+ csr_data = pem_file.read()
87
+ csr = x509.load_pem_x509_csr(csr_data)
88
+ with open(mica_cert_path, "rb") as pem_file:
89
+ mica_pem_data = pem_file.read()
90
+ mica_cert = x509.load_pem_x509_certificate(mica_pem_data)
91
+ with open(mica_key_path, "rb") as pem_file:
92
+ pem_data = pem_file.read()
93
+ mica_key = serialization.load_pem_private_key(pem_data, password=None)
94
+ valid_from = datetime.now(tz=tz.UTC)
95
+ valid_to = datetime(9999, 12, 31, 23, 59, 59, 0) # as per standard
96
+
97
+ policies = [x509.PolicyInformation(oid, None) for oid in policy_oids]
98
+
99
+ # SAN OtherName encoder
100
+ encoder = asn1.Encoder()
101
+ encoder.start()
102
+ encoder.enter(asn1.Numbers.Sequence)
103
+ encoder.write(str(hardware_type_oid), asn1.Numbers.ObjectIdentifier)
104
+ encoder.write(str(hardware_serial_number), asn1.Numbers.OctetString)
105
+ encoder.leave()
106
+ hw_module_name = encoder.output()
107
+
108
+ sname = x509.Name("") # SubjectName should be blank
109
+
110
+ issuer_name = mica_cert.subject
111
+ iname = x509.Name(issuer_name)
112
+ cert = (
113
+ x509.CertificateBuilder()
114
+ .subject_name(sname)
115
+ .issuer_name(iname)
116
+ .public_key(csr.public_key())
117
+ .serial_number(x509.random_serial_number())
118
+ .not_valid_before(valid_from)
119
+ .not_valid_after(valid_to)
120
+ .add_extension(
121
+ x509.BasicConstraints(ca=False, path_length=None),
122
+ critical=True,
123
+ )
124
+ .add_extension(
125
+ x509.KeyUsage(
126
+ key_agreement=True,
127
+ key_cert_sign=False,
128
+ crl_sign=False,
129
+ digital_signature=True,
130
+ content_commitment=False,
131
+ key_encipherment=False,
132
+ data_encipherment=False,
133
+ encipher_only=False,
134
+ decipher_only=False,
135
+ ),
136
+ critical=True,
137
+ )
138
+ .add_extension(
139
+ x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(
140
+ mica_cert.extensions.get_extension_for_class(
141
+ x509.SubjectKeyIdentifier
142
+ ).value
143
+ ),
144
+ critical=False,
145
+ )
146
+ .add_extension(
147
+ x509.SubjectAlternativeName(
148
+ general_names=[
149
+ x509.OtherName(SEP2_HARDWARE_MODULE_NAME, hw_module_name)
150
+ ]
151
+ ),
152
+ critical=True,
153
+ )
154
+ .add_extension(
155
+ x509.CertificatePolicies(policies=policies),
156
+ critical=True,
157
+ )
158
+ .sign(mica_key, SHA256())
159
+ )
160
+
161
+ serca_pem_data = ""
162
+ if serca_cert_path:
163
+ with open(serca_cert_path, "rb") as pem_file:
164
+ serca_pem_data = pem_file.read()
165
+
166
+ cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
167
+ with open(filename, "wb") as fh:
168
+ fh.write(cert_pem)
169
+ # Append the intermediate certificate
170
+ fh.write(mica_pem_data)
171
+ # Append the root certificate
172
+ if serca_pem_data:
173
+ fh.write(serca_pem_data)
174
+ log.info("Created Cert %s", filename)
175
+ return filename
sep2tools/cert_id.py ADDED
@@ -0,0 +1,87 @@
1
+ import hashlib
2
+ import logging
3
+ from itertools import zip_longest
4
+ from pathlib import Path
5
+
6
+ from cryptography import x509
7
+ from cryptography.hazmat.primitives import serialization
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+
12
+ def group_hex(item: str, group_size: int = 4) -> str:
13
+ """Group into groups of four"""
14
+
15
+ def grouper(iterable, n, fillvalue=None):
16
+ args = [iter(iterable)] * n
17
+ return zip_longest(*args, fillvalue=fillvalue)
18
+
19
+ return "-".join("".join(x) for x in grouper(item, n=group_size))
20
+
21
+
22
+ def get_lfdi(fingerprint: str, group: bool = True) -> str:
23
+ """Long-form device identifier (LFDI)
24
+ The LFDI SHALL be the certificate fingerprint left-truncated to 160 bits.
25
+ For display purposes, this SHALL be expressed as 40 hexadecimal digits in
26
+ groups of four.
27
+ """
28
+ lfdi = fingerprint.replace("-", "")[0:40].upper()
29
+ if not group:
30
+ return lfdi
31
+ return group_hex(lfdi)
32
+
33
+
34
+ def get_sfdi(fingerprint, group: bool = True) -> str:
35
+ """Short-form device identifier (SFDI)
36
+ The SFDI SHALL be the certificate fingerprint left-truncated to 36 bits.
37
+ For display purposes, this SHALL be expressed as 11 decimal (base 10) digits,
38
+ with an additional sum-of-digits checksum digit right concatenated.
39
+ """
40
+ trunc = fingerprint.replace("-", "")[0:9]
41
+ sfdi = int(trunc, base=16) # Convert from Base 16 to Base 10
42
+ digits = map(int, str(sfdi))
43
+ checksum = 10 - sum(digits) % 10
44
+ if checksum == 10:
45
+ checksum = 0
46
+
47
+ full_sfdi = str(sfdi) + str(checksum)
48
+ if not group:
49
+ return full_sfdi
50
+ return group_hex(full_sfdi, 3)
51
+
52
+
53
+ def get_fingerprint(cert_path: Path) -> str:
54
+ """Certificate fingerprint
55
+ The certificate fingerprint is the result of performing a SHA256 operation
56
+ over the whole DER-encoded certificate and is used to derive the SFDI and LFDI.
57
+ """
58
+ with open(cert_path, "rb") as f:
59
+ fbytes = f.read() # read entire file as bytes
60
+ readable_hash = hashlib.sha256(fbytes).hexdigest().upper()
61
+ return readable_hash
62
+
63
+
64
+ def get_der_certificate_lfdi(cert_path: Path, group: bool = True):
65
+ """Load X.509 DER Certificate and return LFDI"""
66
+
67
+ if not cert_path.exists():
68
+ raise ValueError(f"Cert not found at {cert_path}")
69
+
70
+ fingerprint = get_fingerprint(cert_path)
71
+ return get_lfdi(fingerprint, group=group)
72
+
73
+
74
+ def get_pem_certificate_lfdi(cert_path: Path, group: bool = True):
75
+ """Load X.509 DER Certificate in PEM format and return LFDI"""
76
+
77
+ if not cert_path.exists():
78
+ raise ValueError(f"Cert not found at {cert_path}")
79
+
80
+ with open(cert_path, "rb") as pem_file:
81
+ cert_data = pem_file.read()
82
+
83
+ cert = x509.load_pem_x509_certificate(cert_data)
84
+ der_bytes = cert.public_bytes(serialization.Encoding.DER)
85
+ fingerprint = hashlib.sha256(der_bytes).hexdigest().upper()
86
+ lfdi = get_lfdi(fingerprint, group=group)
87
+ return lfdi
sep2tools/hexmaps.py ADDED
@@ -0,0 +1,177 @@
1
+ def get_role_flag(
2
+ is_mirror=0, is_premise=0, is_pev=0, is_der=0, is_revenue=0, is_dc=0, is_submeter=0
3
+ ) -> tuple[str, str]:
4
+ """
5
+ RoleFlagsType object (HexBinary16)
6
+ Specifies the roles that apply to a usage point.
7
+ Bit 0—isMirror—SHALL be set if the server is not the measurement device
8
+ Bit 1—isPremisesAggregationPoint—SHALL be set if the UsagePoint is the
9
+ point of delivery for a premises
10
+ Bit 2—isPEV—SHALL be set if the usage applies to an electric vehicle
11
+ Bit 3—isDER—SHALL be set if the usage applies to a distributed energy resource,
12
+ capable of delivering
13
+ power to the grid
14
+ Bit 4—isRevenueQuality—SHALL be set if usage was measured by a device certified
15
+ as revenue quality
16
+ Bit 5—isDC—SHALL be set if the usage point measures direct current
17
+ Bit 6—isSubmeter—SHALL be set if the usage point is not a premises aggregation point
18
+ Bit 7 to 15—Reserved
19
+ """
20
+
21
+ a = is_mirror
22
+ b = is_premise
23
+ c = is_pev
24
+ d = is_der
25
+ e = is_revenue
26
+ f = is_dc
27
+ g = is_submeter
28
+
29
+ binary_str = f"000000000{g}{f}{e}{d}{c}{b}{a}"
30
+ val = int(binary_str, 2)
31
+ hex_str = str(hex(val))[2:].zfill(2).upper()
32
+ return binary_str, hex_str
33
+
34
+
35
+ def get_quality_flag(
36
+ valid=0, manual=0, estimate=0, interpolated=0, questionable=0, derived=0, future=0
37
+ ) -> tuple[str, str]:
38
+ """
39
+ qualityFlags (HexBinary16)
40
+ Bit 0 - valid: data that has gone through all required validation
41
+ checks and either passed them all or has been verified
42
+ Bit 1 - manually edited: Replaced or approved by a human
43
+ Bit 2 - estimated using reference day: data value was replaced
44
+ by a machine computed value based on analysis of historical
45
+ data using the same type of measurement.
46
+ Bit 3 - estimated using linear interpolation: data value was
47
+ computed using linear interpolation based on the readings
48
+ before and after it
49
+ Bit 4 - questionable: data that has failed one or more checks
50
+ Bit 5 - derived: data that has been calculated (using logic or
51
+ mathematical operations), not necessarily measured directly
52
+ Bit 6 - projected (forecast): data that has been calculated as a
53
+ projection or forecast of future reading
54
+ """
55
+
56
+ a = valid
57
+ b = manual
58
+ c = estimate
59
+ d = interpolated
60
+ e = questionable
61
+ f = derived
62
+ g = future
63
+
64
+ binary_str = f"000000000{g}{f}{e}{d}{c}{b}{a}"
65
+ val = int(binary_str, 2)
66
+ hex_str = str(hex(val))[2:].zfill(2).upper()
67
+ return binary_str, hex_str
68
+
69
+
70
+ def get_connect_status(
71
+ connected=0, available=0, operating=0, test=0, fault=0
72
+ ) -> tuple[str, str]:
73
+ """
74
+ ConnectStatusType object (HexBinary8)
75
+ 0 = Connected
76
+ 1 = Available
77
+ 2 = Operating
78
+ 3 = Test
79
+ 4 = Fault/Error
80
+ """
81
+
82
+ a = connected
83
+ b = available
84
+ c = operating
85
+ d = test
86
+ e = fault
87
+
88
+ binary_str = f"000{e}{d}{c}{b}{a}"
89
+ val = int(binary_str, 2)
90
+ hex_str = str(hex(val))[2:].zfill(2).upper()
91
+ return binary_str, hex_str
92
+
93
+
94
+ def get_modes_supported(modes: list[str]) -> tuple[str, str]:
95
+ """DERControlType object (HexBinary32)
96
+ Control modes supported by the DER. Bit positions SHALL be defined as follows:
97
+ 0 = Charge mode
98
+ 1 = Discharge mode
99
+ 2 = opModConnect (connect/disconnect—implies galvanic isolation)
100
+ 3 = opModEnergize (energize/de-energize)
101
+ 4 = opModFixedPFAbsorbW (fixed power factor setpoint when absorbing active power)
102
+ 5 = opModFixedPFInjectW (fixed power factor setpoint when injecting active power)
103
+ 6 = opModFixedVar (reactive power setpoint)
104
+ 7 = opModFixedW (charge/discharge setpoint)
105
+ 8 = opModFreqDroop (Frequency-Watt Parameterized mode)
106
+ 9 = opModFreqWatt (Frequency-Watt Curve mode)
107
+ 10 = opModHFRTMayTrip (High Frequency Ride-Through, May Trip mode)
108
+ 11 = opModHFRTMustTrip (High Frequency Ride-Through, Must Trip mode)
109
+ 12 = opModHVRTMayTrip (High Voltage Ride-Through, May Trip mode)
110
+ 13 = opModHVRTMomentaryCessation (HV Ride-Through, Momentary Cessation mode)
111
+ 14 = opModHVRTMustTrip (High Voltage Ride-Through, Must Trip mode)
112
+ 15 = opModLFRTMayTrip (Low Frequency Ride-Through, May Trip mode)
113
+ 16 = opModLFRTMustTrip (Low Frequency Ride-Through, Must Trip mode)
114
+ 17 = opModLVRTMayTrip (Low Voltage Ride-Through, May Trip mode)
115
+ 18 = opModLVRTMomentaryCessation (LV Ride-Through, Momentary Cessation mode)
116
+ 19 = opModLVRTMustTrip (Low Voltage Ride-Through, Must Trip mode)
117
+ 20 = opModMaxLimW (maximum active power)
118
+ 21 = opModTargetVar (target reactive power)
119
+ 22 = opModTargetW (target active power)
120
+ 23 = opModVoltVar (Volt-Var mode)
121
+ 24 = opModVoltWatt (Volt-Watt mode)
122
+ 25 = opModWattPF (Watt-Powerfactor mode)
123
+ 26 = opModWattVar (Watt-Var mode)
124
+ """
125
+
126
+ a = 1 if "Charge mode" in modes else 0
127
+ b = 1 if "Discharge mode" in modes else 0
128
+ c = 1 if "opModConnect" in modes else 0
129
+ d = 1 if "opModEnergize" in modes else 0
130
+ e = 1 if "opModFixedPFAbsorbW" in modes else 0
131
+ f = 1 if "opModFixedPFInjectW" in modes else 0
132
+ g = 1 if "opModFixedVar" in modes else 0
133
+ h = 1 if "opModFixedW" in modes else 0
134
+ i = 1 if "opModFreqDroop" in modes else 0
135
+ j = 1 if "opModFreqWatt" in modes else 0
136
+ k = 1 if "opModHFRTMayTrip" in modes else 0
137
+ l = 1 if "opModHFRTMustTrip" in modes else 0 # noqa: E741
138
+ m = 1 if "opModHVRTMayTrip" in modes else 0
139
+ n = 1 if "opModHVRTMomentaryCessation" in modes else 0
140
+ o = 1 if "opModHVRTMustTrip" in modes else 0
141
+ p = 1 if "opModLFRTMayTrip" in modes else 0
142
+ q = 1 if "opModLFRTMustTrip" in modes else 0
143
+ r = 1 if "opModLVRTMayTrip" in modes else 0
144
+ s = 1 if "opModLVRTMomentaryCessation" in modes else 0
145
+ t = 1 if "opModLVRTMustTrip" in modes else 0
146
+ u = 1 if "opModMaxLimW" in modes else 0
147
+ v = 1 if "opModTargetVar" in modes else 0
148
+ w = 1 if "opModTargetW" in modes else 0
149
+ x = 1 if "opModVoltVar" in modes else 0
150
+ y = 1 if "opModVoltWatt" in modes else 0
151
+ z = 1 if "opModWattPF" in modes else 0
152
+ aa = 1 if "opModWattVar" in modes else 0
153
+
154
+ binary_str = f"00000{aa}{z}{y}{x}{w}{v}{u}{t}{s}{r}{q}"
155
+ binary_str += f"{p}{o}{n}{m}{l}{k}{j}{i}{h}{g}{f}{e}{d}{c}{b}{a}"
156
+ val = int(binary_str, 2)
157
+ hex_str = str(hex(val))[2:].zfill(8).upper()
158
+ return binary_str, hex_str
159
+
160
+
161
+ def get_doe_modes_supported(modes: list[str]) -> tuple[str, str]:
162
+ """DERControlType object (HexBinary32)
163
+ Control modes supported by the DER. Bit positions SHALL be defined as follows:
164
+ 0 = opModExpLimW
165
+ 1 = opModImpLimW
166
+ 2 = opModGenLimW
167
+ 3 = opModLoadLimW
168
+ """
169
+ a = 1 if "opModExpLimW" in modes else 0
170
+ b = 1 if "opModImpLimW" in modes else 0
171
+ c = 1 if "opModGenLimW" in modes else 0
172
+ d = 1 if "opModLoadLimW" in modes else 0
173
+
174
+ binary_str = f"000000000000{d}{c}{b}{a}"
175
+ val = int(binary_str, 2)
176
+ hex_str = str(hex(val))[2:].zfill(8).upper()
177
+ return binary_str, hex_str
sep2tools/ids.py ADDED
@@ -0,0 +1,37 @@
1
+ import hashlib
2
+ import logging
3
+ from itertools import zip_longest
4
+ from uuid import uuid4
5
+
6
+ log = logging.getLogger(__name__)
7
+
8
+ # When generatating IDs, the PEN should always be used at the end
9
+ # to ensure that there are no conflicts between entities
10
+
11
+
12
+ def group_hex(item: str, group_size: int = 4) -> str:
13
+ """Group into groups of four"""
14
+
15
+ def grouper(iterable, n, fillvalue=None):
16
+ args = [iter(iterable)] * n
17
+ return zip_longest(*args, fillvalue=fillvalue)
18
+
19
+ return "-".join("".join(x) for x in grouper(item, n=group_size))
20
+
21
+
22
+ def generate_mrid(pen: int, group: bool = True) -> str:
23
+ """Generate an random mRID"""
24
+ random_hex = uuid4().hex
25
+ mrid = f"{random_hex[0:24]}{pen:08}".upper()
26
+ if not group:
27
+ return mrid
28
+ return group_hex(mrid)
29
+
30
+
31
+ def proxy_device_lfdi(pen: int, con_id: str, group: bool = True) -> str:
32
+ """Generate a LFDI for use by a cloud proxy"""
33
+ id_hash = hashlib.sha256(con_id.encode("utf-8")).hexdigest().upper()
34
+ lfdi = f"{id_hash[0:32]}{pen:08}"
35
+ if not group:
36
+ return lfdi
37
+ return group_hex(lfdi)
sep2tools/version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Alex Guinman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.1
2
+ Name: sep2tools
3
+ Version: 0.1.0
4
+ Summary: Useful functions for working with IEEE 2030.5 (SEP2)
5
+ Author-email: Alex Guinman <alex@guinman.id.au>
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Operating System :: OS Independent
15
+ Requires-Dist: cryptography
16
+ Requires-Dist: asn1
17
+ Requires-Dist: python-dateutil
18
+ Requires-Dist: ruff ; extra == "test"
19
+ Requires-Dist: pytest >=2.7.3 ; extra == "test"
20
+ Requires-Dist: pytest-cov ; extra == "test"
21
+ Requires-Dist: mypy ; extra == "test"
22
+ Project-URL: Source, https://github.com/aguinane/SEP2-Tools
23
+ Provides-Extra: test
24
+
25
+ # SEP2-Tools
26
+ This library provides some useful functions for working with IEEE 2030.5 (SEP2).
27
+
@@ -0,0 +1,10 @@
1
+ sep2tools/__init__.py,sha256=QKj6hnzYzIhdOLn0gJrJt5x1oFfhtfPU3ZcU6uOR4VE,360
2
+ sep2tools/cert_create.py,sha256=eKLolPMGEPpBjqJG2dazYiex07jhAiJpsMnKuXF19YA,5985
3
+ sep2tools/cert_id.py,sha256=2krUXeExPGn56l0Hn9fDNn0nOBKZ9SGC9T75IoCXuLI,2895
4
+ sep2tools/hexmaps.py,sha256=8Qk98sfCD4h_hB9fakY_A4oI1b2LsTinWBaOQlAEouM,6926
5
+ sep2tools/ids.py,sha256=o2KfrWDYwi7s5xuJpT0j1bSs63YxxW7BSoq5ckUtAVc,1083
6
+ sep2tools/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
7
+ sep2tools-0.1.0.dist-info/LICENSE,sha256=iLry15lbsTDTiEPjqk639lVNnA8zDDEjpWAlhobgr0M,1069
8
+ sep2tools-0.1.0.dist-info/WHEEL,sha256=jPMR_Dzkc4X4icQtmz81lnNY_kAsfog7ry7qoRvYLXw,81
9
+ sep2tools-0.1.0.dist-info/METADATA,sha256=qHrQHN6bRWYJgLgrIPhMCQn0AW9j8tlB8vvnoQH5ESc,999
10
+ sep2tools-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.6.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any