openbadgeslib 1.1.1__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.
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ OpenBadges Library
4
+
5
+ Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
6
+ Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
7
+
8
+ All rights reserved.
9
+
10
+ This library is free software; you can redistribute it and/or
11
+ modify it under the terms of the GNU Lesser General Public
12
+ License as published by the Free Software Foundation; either
13
+ version 3.0 of the License, or (at your option) any later version.
14
+
15
+ This library is distributed in the hope that it will be useful,
16
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
+ Lesser General Public License for more details.
19
+
20
+ You should have received a copy of the GNU Lesser General Public
21
+ License along with this library.
22
+ """
23
+
24
+ # ── OpenBadges 2.0 ─────────────────────────────────────────────────────────────
25
+ from .ob2 import ( # noqa: F401
26
+ Signer, Verifier, VerifyInfo,
27
+ Badge, BadgeSigned, Assertion,
28
+ BadgeStatus, BadgeImgType, BadgeType,
29
+ extract_svg_assertion, extract_png_assertion,
30
+ )
31
+
32
+ # ── OpenBadges 3.0 ─────────────────────────────────────────────────────────────
33
+ from .ob3 import ( # noqa: F401
34
+ OB3Signer, OB3Verifier, OB3VerificationError,
35
+ OpenBadgeCredential, Achievement, Issuer,
36
+ )
37
+
38
+ # ── Shared utilities ────────────────────────────────────────────────────────────
39
+ from .keys import KeyFactory, KeyRSA, KeyECC # noqa: F401
40
+ from .util import __version__ # noqa: F401
@@ -0,0 +1,102 @@
1
+ """JWS sign/verify backed by PyJWT algorithm implementations (RS256/384/512, ES256/384/512)."""
2
+
3
+ from . import utils
4
+ from .exceptions import SignatureError, MissingKey, MissingSigner, MissingVerifier, RouteMissingError
5
+
6
+ from jwt.algorithms import RSAAlgorithm, ECAlgorithm
7
+ from jwt.exceptions import InvalidKeyError
8
+
9
+ from ..keys import KeyType, detect_key_type, key_to_pem
10
+ from ..errors import UnknownKeyType
11
+
12
+ _ALGORITHMS = {
13
+ 'RS256': (RSAAlgorithm, RSAAlgorithm.SHA256),
14
+ 'RS384': (RSAAlgorithm, RSAAlgorithm.SHA384),
15
+ 'RS512': (RSAAlgorithm, RSAAlgorithm.SHA512),
16
+ 'ES256': (ECAlgorithm, ECAlgorithm.SHA256),
17
+ 'ES384': (ECAlgorithm, ECAlgorithm.SHA384),
18
+ 'ES512': (ECAlgorithm, ECAlgorithm.SHA512),
19
+ }
20
+
21
+
22
+ def _algo_for(alg_name):
23
+ entry = _ALGORITHMS.get(alg_name)
24
+ if entry is None:
25
+ raise RouteMissingError(f"Algorithm {alg_name!r} is not supported")
26
+ cls, hash_id = entry
27
+ return cls(hash_id)
28
+
29
+
30
+ def _allowed_algs_for_key(key):
31
+ """Signature algorithms permitted for a verification key, bound to its type.
32
+
33
+ Binding the accepted algorithm to the key type stops a forged JWS header
34
+ from dictating the algorithm (cross-type confusion, and—were a symmetric
35
+ entry ever added to _ALGORITHMS—the classic RS256->HS256 downgrade).
36
+ """
37
+ try:
38
+ key_type = detect_key_type(key_to_pem(key))
39
+ except UnknownKeyType:
40
+ return set()
41
+ if key_type is KeyType.RSA:
42
+ return {'RS256', 'RS384', 'RS512'}
43
+ if key_type is KeyType.ECC:
44
+ return {'ES256', 'ES384', 'ES512'}
45
+ return set()
46
+
47
+
48
+ def sign(header_dict, payload_dict, key):
49
+ """Sign header+payload dicts and return raw signature bytes."""
50
+ if key is None:
51
+ raise MissingKey("No signing key provided")
52
+ alg_name = header_dict.get('alg')
53
+ if not alg_name:
54
+ raise MissingSigner("Header is missing 'alg'")
55
+
56
+ signing_input = utils.encode(header_dict) + b'.' + utils.encode(payload_dict)
57
+ algo = _algo_for(alg_name)
58
+ try:
59
+ prepared = algo.prepare_key(key_to_pem(key))
60
+ return algo.sign(signing_input, prepared)
61
+ except (InvalidKeyError, ValueError) as exc:
62
+ raise SignatureError(str(exc)) from exc
63
+
64
+
65
+ def verify_block(msg, key=None):
66
+ """Verify a JWS compact serialization (bytes or str). Returns True or raises SignatureError."""
67
+ if isinstance(msg, str):
68
+ msg = msg.encode('utf-8')
69
+
70
+ try:
71
+ head_b64, payload_b64, sig_b64 = msg.split(b'.')
72
+ except ValueError:
73
+ raise SignatureError("Malformed JWS: expected header.payload.signature")
74
+
75
+ if key is None:
76
+ raise MissingKey("No verification key provided")
77
+
78
+ header = utils.decode(head_b64)
79
+ alg_name = header.get('alg')
80
+ if not alg_name:
81
+ raise MissingVerifier("JWS header is missing 'alg'")
82
+
83
+ allowed = _allowed_algs_for_key(key)
84
+ if allowed and alg_name not in allowed:
85
+ raise SignatureError(
86
+ "Algorithm %r in JWS header is not allowed for this key type"
87
+ % alg_name)
88
+
89
+ signing_input = head_b64 + b'.' + payload_b64
90
+ raw_sig = utils.from_base64(sig_b64)
91
+
92
+ algo = _algo_for(alg_name)
93
+ try:
94
+ prepared = algo.prepare_key(key_to_pem(key))
95
+ valid = algo.verify(signing_input, prepared, raw_sig)
96
+ except (InvalidKeyError, ValueError) as exc:
97
+ raise SignatureError(str(exc)) from exc
98
+
99
+ if not valid:
100
+ raise SignatureError("Signature verification failed")
101
+
102
+ return True
@@ -0,0 +1,18 @@
1
+ class MissingKey(Exception):
2
+ pass
3
+
4
+
5
+ class MissingSigner(Exception):
6
+ pass
7
+
8
+
9
+ class MissingVerifier(Exception):
10
+ pass
11
+
12
+
13
+ class SignatureError(Exception):
14
+ pass
15
+
16
+
17
+ class RouteMissingError(Exception):
18
+ pass
@@ -0,0 +1,30 @@
1
+ import base64
2
+ import json
3
+
4
+
5
+ def base64url_decode(data):
6
+ # JWS/JWT base64url strips the '=' padding; restore it before decoding.
7
+ # The required pad count is (-len) % 4, which is 0 when the length is
8
+ # already a multiple of 4 (the old `4 - len % 4` wrongly added 4 there).
9
+ data += b'=' * (-len(data) % 4)
10
+ return base64.urlsafe_b64decode(data)
11
+
12
+
13
+ def base64url_encode(input):
14
+ return base64.urlsafe_b64encode(input).replace(b'=', b'')
15
+
16
+
17
+ def to_json(a):
18
+ return json.dumps(a).encode('utf-8')
19
+
20
+
21
+ def from_json(a):
22
+ if isinstance(a, (bytes, bytearray)):
23
+ a = a.decode('utf-8')
24
+ return json.loads(a)
25
+
26
+
27
+ def to_base64(a): return base64url_encode(a)
28
+ def from_base64(a): return base64url_decode(a)
29
+ def encode(a): return to_base64(to_json(a))
30
+ def decode(a): return from_json(from_base64(a))
openbadgeslib/badge.py ADDED
@@ -0,0 +1,12 @@
1
+ """OpenBadges 2.0 badge objects — compatibility shim, re-exports from openbadgeslib.ob2."""
2
+ from .ob2.badge import (
3
+ BadgeStatus, BadgeImgType, BadgeType,
4
+ Assertion, Badge, BadgeSigned,
5
+ extract_svg_assertion, extract_png_assertion,
6
+ )
7
+
8
+ __all__ = [
9
+ 'BadgeStatus', 'BadgeImgType', 'BadgeType',
10
+ 'Assertion', 'Badge', 'BadgeSigned',
11
+ 'extract_svg_assertion', 'extract_png_assertion',
12
+ ]
@@ -0,0 +1,166 @@
1
+ """
2
+ OpenBadges Library
3
+
4
+ Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
5
+ Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
6
+
7
+ All rights reserved.
8
+
9
+ This library is free software; you can redistribute it and/or
10
+ modify it under the terms of the GNU Lesser General Public
11
+ License as published by the Free Software Foundation; either
12
+ version 3.0 of the License, or (at your option) any later version.
13
+
14
+ This library is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17
+ Lesser General Public License for more details.
18
+
19
+ You should have received a copy of the GNU Lesser General Public
20
+ License along with this library.
21
+ """
22
+
23
+ # Version-agnostic carrier format for embedding an OpenBadges token (an OB 2.0
24
+ # JWS or an OB 3.0 JWT-VC) into an SVG or PNG badge image, and extracting it
25
+ # back out. OB2 and OB3 share the exact on-disk format; keeping a single
26
+ # implementation here stops the two paths from drifting (e.g. two PNG readers,
27
+ # one fixed-offset and one structured).
28
+
29
+ from struct import pack
30
+ from zlib import crc32
31
+
32
+ from defusedxml.minidom import parseString
33
+ from png import Reader, signature as _png_signature
34
+
35
+ ITXT_KEYWORD = b'openbadges'
36
+ SVG_ELEMENT = 'openbadges:assertion'
37
+
38
+ # Maximum bytes a compressed iTXt token is allowed to inflate to. A JWS/JWT-VC
39
+ # is a few KB; this cap stops a crafted zlib bomb from exhausting memory during
40
+ # extraction (which runs on untrusted input, before any signature check).
41
+ MAX_ITXT_DECOMPRESSED = 256 * 1024
42
+
43
+
44
+ class DecompressionLimitExceeded(Exception):
45
+ """Raised when a compressed iTXt token inflates beyond the allowed size."""
46
+
47
+
48
+ def _bounded_inflate(data, limit=MAX_ITXT_DECOMPRESSED):
49
+ import zlib
50
+ inflator = zlib.decompressobj()
51
+ out = inflator.decompress(data, limit)
52
+ if inflator.unconsumed_tail:
53
+ raise DecompressionLimitExceeded(
54
+ "Compressed token exceeds the %d-byte limit" % limit)
55
+ return out
56
+
57
+
58
+ # ── SVG ─────────────────────────────────────────────────────────────────────
59
+
60
+ def bake_svg(image_bytes, token, comment=None):
61
+ """Return *image_bytes* with an ``<openbadges:assertion verify=token>``
62
+ element (and an optional XML comment) appended to the root ``<svg>``."""
63
+ svg_doc = parseString(image_bytes)
64
+ try:
65
+ svg_tag = svg_doc.getElementsByTagName('svg').item(0)
66
+ node = svg_doc.createElement(SVG_ELEMENT)
67
+ node.attributes['xmlns:openbadges'] = 'http://openbadges.org'
68
+ node.attributes['verify'] = token
69
+ svg_tag.appendChild(node)
70
+ if comment:
71
+ svg_tag.appendChild(svg_doc.createComment(comment))
72
+ return svg_doc.toxml().encode('utf-8')
73
+ finally:
74
+ svg_doc.unlink()
75
+
76
+
77
+ def has_svg(image_bytes):
78
+ """Return True if *image_bytes* already carries an OpenBadges assertion."""
79
+ svg_doc = parseString(image_bytes)
80
+ try:
81
+ return bool(svg_doc.getElementsByTagName(SVG_ELEMENT))
82
+ finally:
83
+ svg_doc.unlink()
84
+
85
+
86
+ def extract_svg(image_bytes):
87
+ """Return the embedded token string, or None if there is no assertion node.
88
+
89
+ Raises on malformed XML (left to the caller to map to its own error type).
90
+ """
91
+ svg_doc = None
92
+ try:
93
+ svg_doc = parseString(image_bytes)
94
+ nodes = svg_doc.getElementsByTagName(SVG_ELEMENT)
95
+ if not nodes:
96
+ return None
97
+ return nodes[0].attributes['verify'].nodeValue
98
+ finally:
99
+ if svg_doc is not None:
100
+ svg_doc.unlink()
101
+
102
+
103
+ # ── PNG ─────────────────────────────────────────────────────────────────────
104
+
105
+ def _serialize_png(chunks):
106
+ out = _png_signature
107
+ for tag, data in chunks:
108
+ out += pack("!I", len(data))
109
+ if isinstance(tag, str):
110
+ tag = tag.encode('iso8859-1')
111
+ out += tag + data
112
+ checksum = crc32(tag)
113
+ checksum = crc32(data, checksum) & 0xFFFFFFFF
114
+ out += pack("!I", checksum)
115
+ return out
116
+
117
+
118
+ def bake_png(image_bytes, token, text_comment=None):
119
+ """Return *image_bytes* with the token stored in an ``openbadges`` iTXt
120
+ chunk (and an optional ``tEXt`` comment chunk) inserted before IEND."""
121
+ chunks = list(Reader(bytes=image_bytes).chunks())
122
+ itxt_data = ITXT_KEYWORD + pack('BBBBB', 0, 0, 0, 0, 0) + token.encode('utf-8')
123
+ chunks.insert(len(chunks) - 1, ('iTXt', itxt_data))
124
+ if text_comment:
125
+ chunks.insert(len(chunks) - 1, ('tEXt', text_comment.encode('utf-8')))
126
+ return _serialize_png(chunks)
127
+
128
+
129
+ def has_png(image_bytes):
130
+ """Return True if *image_bytes* already carries an OpenBadges iTXt chunk."""
131
+ for tag, data in Reader(bytes=image_bytes).chunks():
132
+ tag_str = tag.decode('ascii') if isinstance(tag, bytes) else tag
133
+ if tag_str == 'iTXt' and data.startswith(ITXT_KEYWORD):
134
+ return True
135
+ return False
136
+
137
+
138
+ def extract_png(image_bytes, max_decompressed=MAX_ITXT_DECOMPRESSED):
139
+ """Return the embedded token string, or None if there is no openbadges
140
+ iTXt chunk.
141
+
142
+ Parses the iTXt structure (keyword, compression flag/method, language tag,
143
+ translated keyword, then text) rather than a fixed byte offset, so tokens
144
+ baked by any conformant tool — including compressed ones — are recovered.
145
+ Raises :class:`DecompressionLimitExceeded` if a compressed token inflates
146
+ beyond *max_decompressed*.
147
+ """
148
+ for tag, data in Reader(bytes=image_bytes).chunks():
149
+ tag_str = tag.decode('ascii') if isinstance(tag, bytes) else tag
150
+ if tag_str != 'iTXt':
151
+ continue
152
+
153
+ # iTXt layout: keyword \0 comp_flag comp_method lang \0 trans \0 text
154
+ keyword, sep, rest = data.partition(b'\x00')
155
+ if sep != b'\x00' or keyword != ITXT_KEYWORD or len(rest) < 2:
156
+ continue
157
+ compression_flag = rest[0]
158
+ _, sep_lang, rest = rest[2:].partition(b'\x00') # drop language tag
159
+ _, sep_trans, text = rest.partition(b'\x00') # drop translated keyword
160
+ if sep_lang != b'\x00' or sep_trans != b'\x00':
161
+ continue
162
+ if compression_flag:
163
+ text = _bounded_inflate(text, max_decompressed)
164
+ return text.decode('utf-8')
165
+
166
+ return None
@@ -0,0 +1,67 @@
1
+ ;
2
+ ; OpenBadges Lib configuration example for RSA keys.
3
+ ;
4
+
5
+ ; Paths to the keys and log
6
+ [paths]
7
+ base = .
8
+ base_key = ${base}/keys
9
+ base_log = ${base}/log
10
+ base_image = ${base}/images
11
+
12
+ ; Log configuration. Stored in ${base_log}
13
+ [logs]
14
+ general = general.log
15
+ signer = signer.log
16
+
17
+ ; SMTP Configuration
18
+ [smtp]
19
+ smtp_server = localhost
20
+ smtp_port = 25
21
+ use_ssl = False
22
+ mail_from = no-reply@issuer.badge
23
+ ; Uncomment this if your SMTP server needs authentication
24
+ ;username =
25
+ ;password =
26
+
27
+ ; Configuration of the OpenBadges issuer.
28
+ [issuer]
29
+ name = OpenBadge issuer
30
+ url = https://www.issuer.badge
31
+ image = issuer_logo.png
32
+ email = issuer_mail@issuer.badge
33
+ publish_url = https://openbadges.issuer.badge/issuer/
34
+ revocationList = revoked.json
35
+
36
+ ;Badge configuration sections.
37
+ [badge_1]
38
+ name = Badge 1
39
+ description = Given to any user that install this library
40
+ local_image = image_badge1.svg
41
+ image = https://www.issuer.badge/issuer/badge_1/badge1.svg
42
+ criteria = https://www.issuer.badge/issuer/badge_1/criteria.html
43
+ verify_key = https://www.issuer.badge/issuer/badge_1/verify_rsa_key.pem
44
+ badge = https://www.issuer.badge/issuer/badge_1/badge.json
45
+ private_key = ${paths:base_key}/sign_rsa_key_1.pem
46
+ public_key = ${paths:base_key}/verify_rsa_key_1.pem
47
+ ; key_type selects the algorithm for openbadges-keygenerator: RSA (default) or ECC
48
+ key_type = RSA
49
+ ;alignement =
50
+ ;tags =
51
+ ;mail = ${paths:base}/badge_1_mail.txt
52
+
53
+ [badge_2]
54
+ name = Badge 2
55
+ description = Given to any user that promote the library
56
+ local_image = image_badge2.svg
57
+ image = https://www.issuer.badge/issuer/badge_2/badge2.svg
58
+ criteria = https://www.issuer.badge/issuer/badge_2/criteria.html
59
+ verify_key = https://www.issuer.badge/issuer/badge_2/verify_rsa_key.pem
60
+ badge = https://www.issuer.badge/issuer/badge_2/badge.json
61
+ private_key = ${paths:base_key}/sign_rsa_key_2.pem
62
+ public_key = ${paths:base_key}/verify_rsa_key_2.pem
63
+ key_type = RSA
64
+ ;alignement =
65
+ ;tags =
66
+ ;mail = ${paths:base}/badge_2_mail.txt
67
+
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ OpenBadges Library
4
+
5
+ Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
6
+ Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
7
+
8
+ All rights reserved.
9
+
10
+ This library is free software; you can redistribute it and/or
11
+ modify it under the terms of the GNU Lesser General Public
12
+ License as published by the Free Software Foundation; either
13
+ version 3.0 of the License, or (at your option) any later version.
14
+
15
+ This library is distributed in the hope that it will be useful,
16
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
+ Lesser General Public License for more details.
19
+
20
+ You should have received a copy of the GNU Lesser General Public
21
+ License along with this library.
22
+ """
23
+
24
+ from configparser import ConfigParser, ExtendedInterpolation
25
+ import os
26
+ import sys
27
+ import logging
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ def read_config_or_exit(config_file):
32
+ """Read a config file for a CLI tool, exiting with a clear message if it is
33
+ missing or empty. Shared by all the console-script entrypoints."""
34
+ conf = ConfParser(config_file).read_conf()
35
+ if not conf:
36
+ print('[!] The config file %s does not exist or is empty' % config_file)
37
+ sys.exit(-1)
38
+ return conf
39
+
40
+
41
+ def resolve_badge_section(conf, name):
42
+ """Return the ``badge_<name>`` section name, exiting if it is not defined."""
43
+ section = 'badge_' + name
44
+ if section not in conf:
45
+ sys.exit('There is no "%s" badge in the configuration' % name)
46
+ return section
47
+
48
+
49
+ class ConfParser():
50
+ def __init__(self, config_file='config.ini'):
51
+ self.config_file = config_file
52
+
53
+ def read_conf(self):
54
+ if not os.path.isfile(self.config_file):
55
+ return None
56
+
57
+ self.parser = ConfigParser(interpolation=ExtendedInterpolation())
58
+
59
+ try:
60
+ self.parser.read(self.config_file)
61
+ except UnicodeDecodeError:
62
+ # We should raise an UnicodeDecodeError, but the error message is too cryptic.#
63
+ raise ValueError("The encoding of the configuration file and the default encoding of "
64
+ "the operating system mismatch") from None
65
+ if self.parser['paths']['base'][0] == '.':
66
+ abs_path = os.path.dirname(self.config_file)
67
+ full_path = os.path.abspath(abs_path)
68
+ self.parser['paths']['base'] = full_path
69
+ return self.parser
70
+
71
+
72
+ if __name__ == '__main__':
73
+ pass
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ OpenBadges Library
4
+
5
+ Copyright (c) 2014-2026, Luis González Fernández, luisgf@luisgf.es
6
+ Copyright (c) 2014-2026, Jesús Cea Avión, jcea@jcea.es
7
+
8
+ All rights reserved.
9
+
10
+ This library is free software; you can redistribute it and/or
11
+ modify it under the terms of the GNU Lesser General Public
12
+ License as published by the Free Software Foundation; either
13
+ version 3.0 of the License, or (at your option) any later version.
14
+
15
+ This library is distributed in the hope that it will be useful,
16
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
+ Lesser General Public License for more details.
19
+
20
+ You should have received a copy of the GNU Lesser General Public
21
+ License along with this library.
22
+ """
23
+
24
+
25
+ class LibOpenBadgesException(Exception):
26
+ pass
27
+
28
+
29
+ """ Exception base classes """
30
+
31
+
32
+ class KeyGenExceptions(LibOpenBadgesException):
33
+ pass
34
+
35
+
36
+ class SignerExceptions(LibOpenBadgesException):
37
+ pass
38
+
39
+
40
+ class VerifierExceptions(LibOpenBadgesException):
41
+ pass
42
+
43
+
44
+ """ User-defined Exceptions """
45
+
46
+
47
+ class GenPrivateKeyError(KeyGenExceptions):
48
+ pass
49
+
50
+
51
+ class GenPublicKeyError(KeyGenExceptions):
52
+ pass
53
+
54
+
55
+ class PrivateKeySaveError(KeyGenExceptions):
56
+ pass
57
+
58
+
59
+ class PublicKeySaveError(KeyGenExceptions):
60
+ pass
61
+
62
+
63
+ class PrivateKeyReadError(KeyGenExceptions):
64
+ pass
65
+
66
+
67
+ class PublicKeyReadError(KeyGenExceptions):
68
+ pass
69
+
70
+
71
+ class UnknownKeyType(KeyGenExceptions):
72
+ pass
73
+
74
+
75
+ """ Signer Exceptions """
76
+
77
+
78
+ class ErrorSigningFile(SignerExceptions):
79
+ pass
80
+
81
+
82
+ """ Verifier Exceptions """
83
+
84
+
85
+ class AssertionFormatIncorrect(VerifierExceptions):
86
+ pass
87
+
88
+
89
+ class NotIdentityInAssertion(VerifierExceptions):
90
+ pass
91
+
92
+
93
+ class ErrorParsingFile(VerifierExceptions):
94
+ pass
95
+
96
+
97
+ """ Badge Object Exceptions """
98
+
99
+
100
+ class BadgeImgFormatUnsupported(LibOpenBadgesException):
101
+ pass