pl8-base 0.0.1__tar.gz
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.
- pl8_base-0.0.1/LICENSE +21 -0
- pl8_base-0.0.1/PKG-INFO +96 -0
- pl8_base-0.0.1/README.md +71 -0
- pl8_base-0.0.1/pyproject.toml +55 -0
- pl8_base-0.0.1/pyproject.toml.orig +56 -0
- pl8_base-0.0.1/src/pl8_base/__init__.py +1 -0
- pl8_base-0.0.1/src/pl8_base/const.py +25 -0
- pl8_base-0.0.1/src/pl8_base/errors.py +86 -0
- pl8_base-0.0.1/src/pl8_base/manager.py +413 -0
- pl8_base-0.0.1/src/pl8_base/mixins/__init__.py +9 -0
- pl8_base-0.0.1/src/pl8_base/mixins/issue.py +805 -0
- pl8_base-0.0.1/src/pl8_base/mixins/space.py +184 -0
- pl8_base-0.0.1/src/pl8_base/py.typed +0 -0
- pl8_base-0.0.1/src/pl8_base/types/__init__.py +43 -0
- pl8_base-0.0.1/src/pl8_base/types/base.py +125 -0
- pl8_base-0.0.1/src/pl8_base/types/enums.py +25 -0
- pl8_base-0.0.1/src/pl8_base/types/events.py +103 -0
- pl8_base-0.0.1/src/pl8_base/types/issue.py +91 -0
- pl8_base-0.0.1/src/pl8_base/types/space.py +50 -0
- pl8_base-0.0.1/src/pl8_base/util.py +338 -0
pl8_base-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Chen
|
|
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.
|
pl8_base-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pl8-base
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: PL8, a lightweight issue tracker backed by DynamoDB
|
|
5
|
+
Keywords: issue-tracker,dynamodb,aws,lambda
|
|
6
|
+
Author: Daniel Chen
|
|
7
|
+
Author-email: Daniel Chen <danieljnchen@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: boto3~=1.43.40
|
|
19
|
+
Requires-Dist: msgspec~=0.21.1
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Project-URL: Homepage, https://github.com/dchenstealth/pl8-base
|
|
22
|
+
Project-URL: Repository, https://github.com/dchenstealth/pl8-base
|
|
23
|
+
Project-URL: Issues, https://github.com/dchenstealth/pl8-base/issues
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pl8-base
|
|
27
|
+
|
|
28
|
+
`pl8-base` is the data layer for PL8, a lightweight issue tracker backed by
|
|
29
|
+
DynamoDB. It's a Python library, not a service: it defines PL8's entities
|
|
30
|
+
(`Issue`, `Space`, `IssueBlocker`), its events, and the `BasePL8` manager
|
|
31
|
+
that reads and writes them against a DynamoDB table. It's the source of
|
|
32
|
+
truth for PL8's data model, consumed by the Lambda functions that actually
|
|
33
|
+
run PL8 in AWS.
|
|
34
|
+
|
|
35
|
+
## Getting started
|
|
36
|
+
|
|
37
|
+
### Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install pl8-base
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
or with [uv](https://docs.astral.sh/uv/):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv add pl8-base
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Basic usage
|
|
50
|
+
|
|
51
|
+
`BasePL8` wraps a `boto3` DynamoDB client and a structured logger. Error
|
|
52
|
+
logging passes arbitrary keyword args through to be merged into the log
|
|
53
|
+
record (e.g. `self.logger.error(msg, item=item)`), which a stdlib
|
|
54
|
+
`logging.Logger` rejects — pass an
|
|
55
|
+
[`aws-lambda-powertools`](https://docs.powertools.aws.dev/lambda/python/latest/)
|
|
56
|
+
`Logger` instead (`pip install aws-lambda-powertools` — it's not a dependency
|
|
57
|
+
of this package, since only your logger instance needs it, not `pl8-base`
|
|
58
|
+
itself). It also expects a table already provisioned with `PK`/`SK` and a
|
|
59
|
+
`GSI1` global secondary index (`GSI1PK`/`GSI1SK`) — see
|
|
60
|
+
[Deploying](#deploying) below.
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import boto3
|
|
64
|
+
from aws_lambda_powertools import Logger
|
|
65
|
+
|
|
66
|
+
from pl8_base.manager import BasePL8
|
|
67
|
+
from pl8_base.types import IssueStatus
|
|
68
|
+
|
|
69
|
+
pl8 = BasePL8(
|
|
70
|
+
dynamodb_client=boto3.client("dynamodb"),
|
|
71
|
+
table_name="pl8",
|
|
72
|
+
logger=Logger(),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
space = pl8.create_space(
|
|
76
|
+
space_id="eng",
|
|
77
|
+
name="Engineering",
|
|
78
|
+
description="Issues for the engineering team",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
issue = pl8.create_issue(
|
|
82
|
+
space_id="eng",
|
|
83
|
+
title="Fix login bug",
|
|
84
|
+
description="Users can't log in on Safari",
|
|
85
|
+
status=IssueStatus.TODO,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
pl8.transition_issue(space_id="eng", issue_id=issue.issue_id,
|
|
89
|
+
status=IssueStatus.IN_PROGRESS)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Deploying
|
|
93
|
+
|
|
94
|
+
`pl8-base` only talks to a DynamoDB table you already have; it doesn't
|
|
95
|
+
provision or run anything in AWS itself. Work is underway to create other
|
|
96
|
+
repos to scaffold this.
|
pl8_base-0.0.1/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# pl8-base
|
|
2
|
+
|
|
3
|
+
`pl8-base` is the data layer for PL8, a lightweight issue tracker backed by
|
|
4
|
+
DynamoDB. It's a Python library, not a service: it defines PL8's entities
|
|
5
|
+
(`Issue`, `Space`, `IssueBlocker`), its events, and the `BasePL8` manager
|
|
6
|
+
that reads and writes them against a DynamoDB table. It's the source of
|
|
7
|
+
truth for PL8's data model, consumed by the Lambda functions that actually
|
|
8
|
+
run PL8 in AWS.
|
|
9
|
+
|
|
10
|
+
## Getting started
|
|
11
|
+
|
|
12
|
+
### Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install pl8-base
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
or with [uv](https://docs.astral.sh/uv/):
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv add pl8-base
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Basic usage
|
|
25
|
+
|
|
26
|
+
`BasePL8` wraps a `boto3` DynamoDB client and a structured logger. Error
|
|
27
|
+
logging passes arbitrary keyword args through to be merged into the log
|
|
28
|
+
record (e.g. `self.logger.error(msg, item=item)`), which a stdlib
|
|
29
|
+
`logging.Logger` rejects — pass an
|
|
30
|
+
[`aws-lambda-powertools`](https://docs.powertools.aws.dev/lambda/python/latest/)
|
|
31
|
+
`Logger` instead (`pip install aws-lambda-powertools` — it's not a dependency
|
|
32
|
+
of this package, since only your logger instance needs it, not `pl8-base`
|
|
33
|
+
itself). It also expects a table already provisioned with `PK`/`SK` and a
|
|
34
|
+
`GSI1` global secondary index (`GSI1PK`/`GSI1SK`) — see
|
|
35
|
+
[Deploying](#deploying) below.
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import boto3
|
|
39
|
+
from aws_lambda_powertools import Logger
|
|
40
|
+
|
|
41
|
+
from pl8_base.manager import BasePL8
|
|
42
|
+
from pl8_base.types import IssueStatus
|
|
43
|
+
|
|
44
|
+
pl8 = BasePL8(
|
|
45
|
+
dynamodb_client=boto3.client("dynamodb"),
|
|
46
|
+
table_name="pl8",
|
|
47
|
+
logger=Logger(),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
space = pl8.create_space(
|
|
51
|
+
space_id="eng",
|
|
52
|
+
name="Engineering",
|
|
53
|
+
description="Issues for the engineering team",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
issue = pl8.create_issue(
|
|
57
|
+
space_id="eng",
|
|
58
|
+
title="Fix login bug",
|
|
59
|
+
description="Users can't log in on Safari",
|
|
60
|
+
status=IssueStatus.TODO,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
pl8.transition_issue(space_id="eng", issue_id=issue.issue_id,
|
|
64
|
+
status=IssueStatus.IN_PROGRESS)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Deploying
|
|
68
|
+
|
|
69
|
+
`pl8-base` only talks to a DynamoDB table you already have; it doesn't
|
|
70
|
+
provision or run anything in AWS itself. Work is underway to create other
|
|
71
|
+
repos to scaffold this.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pl8-base"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "PL8, a lightweight issue tracker backed by DynamoDB"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
keywords = [
|
|
10
|
+
"issue-tracker",
|
|
11
|
+
"dynamodb",
|
|
12
|
+
"aws",
|
|
13
|
+
"lambda",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"boto3~=1.43.40",
|
|
27
|
+
"msgspec~=0.21.1",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[[project.authors]]
|
|
31
|
+
name = "Daniel Chen"
|
|
32
|
+
email = "danieljnchen@gmail.com"
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/dchenstealth/pl8-base"
|
|
36
|
+
Repository = "https://github.com/dchenstealth/pl8-base"
|
|
37
|
+
Issues = "https://github.com/dchenstealth/pl8-base/issues"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"aws-lambda-powertools~=3.35.0",
|
|
42
|
+
"moto~=5.2.2",
|
|
43
|
+
"ruff~=0.16.8",
|
|
44
|
+
"pytest~=9.1.1",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint.per-file-ignores]
|
|
48
|
+
"tests/test_util.py" = ["FURB157"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
pythonpath = ["src"]
|
|
52
|
+
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
55
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pl8-base"
|
|
3
|
+
version = "0.0.1"
|
|
4
|
+
description = "PL8, a lightweight issue tracker backed by DynamoDB"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Daniel Chen", email = "danieljnchen@gmail.com" }
|
|
10
|
+
]
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
keywords = ["issue-tracker", "dynamodb", "aws", "lambda"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"boto3~=1.43.40",
|
|
25
|
+
"msgspec~=0.21.1",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/dchenstealth/pl8-base"
|
|
30
|
+
Repository = "https://github.com/dchenstealth/pl8-base"
|
|
31
|
+
Issues = "https://github.com/dchenstealth/pl8-base/issues"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"aws-lambda-powertools~=3.35.0",
|
|
36
|
+
"moto~=5.2.2",
|
|
37
|
+
"ruff~=0.16.8",
|
|
38
|
+
"pytest~=9.1.1",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint.per-file-ignores]
|
|
42
|
+
# FURB157 would rewrite Decimal("5") as Decimal(5). The cleanup_decimals
|
|
43
|
+
# tests pair an integral case with a fractional one throughout, and only the
|
|
44
|
+
# integral half of each pair can be rewritten, so taking the fix leaves every
|
|
45
|
+
# pair visually mismatched. The string form also mirrors how boto3 hands
|
|
46
|
+
# these values back.
|
|
47
|
+
"tests/test_util.py" = ["FURB157"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
pythonpath = [
|
|
51
|
+
"src",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[build-system]
|
|
55
|
+
requires = ["uv_build>=0.12.10,<0.13.0"]
|
|
56
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
MAX_ISSUE_ID_LEN = 16
|
|
4
|
+
MIN_ISSUE_ID_LEN = 3
|
|
5
|
+
RETRY_ISSUE_ID_COLLISIONS = 5
|
|
6
|
+
|
|
7
|
+
# Bounded because space_id is caller-supplied and composes both SPACE#{space_id}
|
|
8
|
+
# and ISSUE#{space_id}#{issue_id}. 64 leaves the composite key far inside
|
|
9
|
+
# DynamoDB's 2048 byte limit.
|
|
10
|
+
MAX_SPACE_ID_LEN = 64
|
|
11
|
+
|
|
12
|
+
# Full-jitter exponential backoff for TransactionConflict retries
|
|
13
|
+
TRANSACT_RETRY_ATTEMPTS = 5
|
|
14
|
+
TRANSACT_RETRY_BASE_DELAY = 0.05
|
|
15
|
+
TRANSACT_RETRY_MAX_DELAY = 1.0
|
|
16
|
+
|
|
17
|
+
# CancellationReasons code marking a transient transaction conflict
|
|
18
|
+
TRANSACT_CONFLICT_REASON = "TransactionConflict"
|
|
19
|
+
# CancellationReasons code marking a failed ConditionExpression
|
|
20
|
+
CONDITION_FAILED_REASON = "ConditionalCheckFailed"
|
|
21
|
+
# Error code a failed ConditionExpression raises outside a transaction
|
|
22
|
+
CONDITION_FAILED_CODE = "ConditionalCheckFailedException"
|
|
23
|
+
|
|
24
|
+
# Name of the single GSI on the base table
|
|
25
|
+
GSI1_INDEX_NAME = "GSI1"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
class DDBError(Exception):
|
|
4
|
+
"""Base class for DDB Issues"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DDBInternalError(DDBError):
|
|
8
|
+
"""Raised for general service errors"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DDBCorruptedError(DDBInternalError):
|
|
12
|
+
"""Raised if data read from database is corrupted"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DDBMissingError(DDBError):
|
|
16
|
+
"""Raised when a requested item does not exist"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DDBExistsError(DDBError):
|
|
20
|
+
"""Raised when an item already exists"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DDBArgsError(DDBError):
|
|
24
|
+
"""Raised on invalid arguments passed in"""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DDBIdCollisionError(DDBError):
|
|
28
|
+
"""Raised on ID collision after too many retries"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DDBTransactionConflictError(DDBError):
|
|
32
|
+
"""Raised on transaction conflict.
|
|
33
|
+
|
|
34
|
+
Transient contention. The same request may be retried unchanged; see
|
|
35
|
+
util.retry_on_transaction_conflict.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class DDBVersionConflictError(DDBError):
|
|
40
|
+
"""Raised when a write's version condition fails.
|
|
41
|
+
|
|
42
|
+
The caller's view of the item is stale, so retrying the same request will
|
|
43
|
+
fail again. The caller must re-read and reapply its change.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DDBTerminalStatusError(DDBError):
|
|
48
|
+
"""Raised when an operation would move an Issue out of DONE, or would
|
|
49
|
+
mutate an Issue whose DONE status forbids the change.
|
|
50
|
+
|
|
51
|
+
DONE is terminal; see types.enums.IssueStatus for why that rule is
|
|
52
|
+
load-bearing beyond the product requirement.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DDBStillBlockedError(DDBError):
|
|
57
|
+
"""Raised when an Issue would be transitioned out of BLOCKED while it
|
|
58
|
+
still has active IssueBlockers.
|
|
59
|
+
|
|
60
|
+
The caller must delete the remaining IssueBlockers first, or wait for the
|
|
61
|
+
blocking Issues to reach DONE.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DDBBlockingIssueDoneError(DDBError):
|
|
66
|
+
"""Raised when an IssueBlocker would name a DONE Issue as the blocker.
|
|
67
|
+
|
|
68
|
+
A DONE Issue blocks nothing; the relationship would be created already
|
|
69
|
+
satisfied.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class EventError(Exception):
|
|
74
|
+
"""Base class for event issues"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class EventSendError(EventError):
|
|
78
|
+
"""Raised when EventBridge rejects or fails to accept an event.
|
|
79
|
+
|
|
80
|
+
Covers both a failed put_events call and a per-entry failure reported
|
|
81
|
+
back with FailedEntryCount > 0.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class EventCorruptedError(EventError):
|
|
86
|
+
"""Raised when an event's Detail is missing/unknown type, or malformed"""
|