workpeg 0.3.2__tar.gz → 0.4.0__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.
- {workpeg-0.3.2/src/workpeg.egg-info → workpeg-0.4.0}/PKG-INFO +1 -1
- {workpeg-0.3.2 → workpeg-0.4.0}/pyproject.toml +1 -1
- workpeg-0.4.0/src/workpeg/context.py +134 -0
- {workpeg-0.3.2 → workpeg-0.4.0/src/workpeg.egg-info}/PKG-INFO +1 -1
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg.egg-info/SOURCES.txt +2 -0
- workpeg-0.4.0/tests/test_context.py +201 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/LICENSE +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/MANIFEST.in +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/README.md +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/setup.cfg +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/__init__.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/build.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/cli.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/config.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/create_new.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/run.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/runtime.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/submit.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/templates/__init__.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/templates/functions/Dockerfile +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/templates/functions/LICENSE +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/templates/functions/README.md +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/templates/functions/app/__init__.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/templates/functions/app/main.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg/templates/functions/requirements.txt +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg.egg-info/dependency_links.txt +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg.egg-info/entry_points.txt +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg.egg-info/requires.txt +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/src/workpeg.egg-info/top_level.txt +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/tests/test_build.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/tests/test_cli.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/tests/test_create_new.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/tests/test_run.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/tests/test_runtime.py +0 -0
- {workpeg-0.3.2 → workpeg-0.4.0}/tests/test_submit.py +0 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
UUID_REGEX = re.compile(
|
|
5
|
+
r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-"
|
|
6
|
+
r"[89ab][a-f0-9]{3}-[a-f0-9]{12}$",
|
|
7
|
+
re.IGNORECASE,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseContext:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def verify(data):
|
|
14
|
+
raise NotImplementedError("Context must implement verify().")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _TableContext(BaseContext):
|
|
18
|
+
REQUIRED_FIELDS = {
|
|
19
|
+
"uuid",
|
|
20
|
+
"name",
|
|
21
|
+
"owner_id",
|
|
22
|
+
"metadata",
|
|
23
|
+
"columns",
|
|
24
|
+
"namespace",
|
|
25
|
+
"peg",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def verify(cls, data):
|
|
30
|
+
if data is None:
|
|
31
|
+
return True
|
|
32
|
+
|
|
33
|
+
cls._verify_keys(data)
|
|
34
|
+
cls._verify_uuid_format(data["uuid"])
|
|
35
|
+
cls._verify_owner_id(data["owner_id"])
|
|
36
|
+
cls._verify_columns(data["columns"])
|
|
37
|
+
cls._verify_namespace(data["namespace"])
|
|
38
|
+
cls._verify_peg(data["peg"])
|
|
39
|
+
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def _verify_keys(cls, data):
|
|
44
|
+
if not isinstance(data, dict):
|
|
45
|
+
raise ValueError("Data must be a dictionary.")
|
|
46
|
+
|
|
47
|
+
missing = cls.REQUIRED_FIELDS - data.keys()
|
|
48
|
+
if missing:
|
|
49
|
+
raise ValueError(f"Missing required fields: {missing}")
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def _verify_uuid_format(cls, uuid_str):
|
|
53
|
+
if not isinstance(uuid_str, str) or not UUID_REGEX.match(uuid_str):
|
|
54
|
+
raise ValueError("Invalid UUID format.")
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def _verify_owner_id(cls, owner_id):
|
|
58
|
+
if owner_id is not None and (
|
|
59
|
+
not isinstance(owner_id, int) or owner_id <= 0
|
|
60
|
+
):
|
|
61
|
+
raise ValueError("Owner ID must be a positive integer or None.")
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def _verify_columns(cls, columns):
|
|
65
|
+
return True # TODO
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def _verify_namespace(cls, namespace):
|
|
69
|
+
if namespace is not None and not isinstance(namespace, str):
|
|
70
|
+
raise ValueError("Namespace must be a non-empty string.")
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def _verify_peg(cls, peg):
|
|
74
|
+
return True # TODO
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class _StaffContext(BaseContext):
|
|
78
|
+
@staticmethod
|
|
79
|
+
def verify(data):
|
|
80
|
+
if data is None:
|
|
81
|
+
return True
|
|
82
|
+
|
|
83
|
+
if not isinstance(data, dict):
|
|
84
|
+
raise ValueError("Staff context must be a dictionary.")
|
|
85
|
+
|
|
86
|
+
if "email" not in data:
|
|
87
|
+
raise ValueError("Missing required field: 'email'")
|
|
88
|
+
|
|
89
|
+
return True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _MetadataContext(BaseContext):
|
|
93
|
+
@staticmethod
|
|
94
|
+
def verify(data):
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ContextBuilder:
|
|
99
|
+
registry = {}
|
|
100
|
+
|
|
101
|
+
def __init__(self):
|
|
102
|
+
self.context = {}
|
|
103
|
+
|
|
104
|
+
@classmethod
|
|
105
|
+
def register(cls, key, handler_class):
|
|
106
|
+
cls.registry[key] = handler_class
|
|
107
|
+
|
|
108
|
+
def add(self, key, serialized_data):
|
|
109
|
+
handler = self.registry.get(key)
|
|
110
|
+
|
|
111
|
+
if handler is None:
|
|
112
|
+
raise ValueError(f"{key} rejected: no handler registered")
|
|
113
|
+
|
|
114
|
+
verify_method = getattr(handler, "verify", None)
|
|
115
|
+
|
|
116
|
+
if not callable(verify_method):
|
|
117
|
+
raise ValueError(f"{key} rejected: 'verify' not implemented")
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
if not verify_method(serialized_data):
|
|
121
|
+
raise ValueError(f"{key} rejected: verification failed")
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise ValueError(f"{key} rejected: verification error - {str(e)}")
|
|
124
|
+
|
|
125
|
+
self.context[key] = serialized_data
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def build(self):
|
|
129
|
+
return self.context
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
ContextBuilder.register("table", _TableContext)
|
|
133
|
+
ContextBuilder.register("staff", _StaffContext)
|
|
134
|
+
ContextBuilder.register("metadata", _MetadataContext)
|
|
@@ -6,6 +6,7 @@ src/workpeg/__init__.py
|
|
|
6
6
|
src/workpeg/build.py
|
|
7
7
|
src/workpeg/cli.py
|
|
8
8
|
src/workpeg/config.py
|
|
9
|
+
src/workpeg/context.py
|
|
9
10
|
src/workpeg/create_new.py
|
|
10
11
|
src/workpeg/run.py
|
|
11
12
|
src/workpeg/runtime.py
|
|
@@ -25,6 +26,7 @@ src/workpeg/templates/functions/app/__init__.py
|
|
|
25
26
|
src/workpeg/templates/functions/app/main.py
|
|
26
27
|
tests/test_build.py
|
|
27
28
|
tests/test_cli.py
|
|
29
|
+
tests/test_context.py
|
|
28
30
|
tests/test_create_new.py
|
|
29
31
|
tests/test_run.py
|
|
30
32
|
tests/test_runtime.py
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
from uuid import uuid4
|
|
4
|
+
|
|
5
|
+
from workpeg.context import (
|
|
6
|
+
ContextBuilder,
|
|
7
|
+
_TableContext, _StaffContext, _MetadataContext
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture
|
|
12
|
+
def valid_table_data():
|
|
13
|
+
return {
|
|
14
|
+
"uuid": str(uuid4()),
|
|
15
|
+
"name": "Employees",
|
|
16
|
+
"owner_id": 1,
|
|
17
|
+
"metadata": {},
|
|
18
|
+
"columns": [],
|
|
19
|
+
"namespace": "workpeg.hr",
|
|
20
|
+
"peg": 10,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestTableContext:
|
|
25
|
+
|
|
26
|
+
def test_verify_table_returns_true_for_valid_data(self, valid_table_data):
|
|
27
|
+
assert _TableContext.verify(valid_table_data) is True
|
|
28
|
+
|
|
29
|
+
def test_verify_table_returns_true_when_owner_id_is_none(
|
|
30
|
+
self,
|
|
31
|
+
valid_table_data,
|
|
32
|
+
):
|
|
33
|
+
valid_table_data["owner_id"] = None
|
|
34
|
+
|
|
35
|
+
assert _TableContext.verify(valid_table_data) is True
|
|
36
|
+
|
|
37
|
+
def test_verify_table_raises_when_required_key_is_missing(
|
|
38
|
+
self,
|
|
39
|
+
valid_table_data,
|
|
40
|
+
):
|
|
41
|
+
del valid_table_data["uuid"]
|
|
42
|
+
|
|
43
|
+
with pytest.raises(ValueError, match="Missing required fields"):
|
|
44
|
+
_TableContext.verify(valid_table_data)
|
|
45
|
+
|
|
46
|
+
def test_verify_table_raises_for_invalid_uuid(self, valid_table_data):
|
|
47
|
+
valid_table_data["uuid"] = "bad-uuid"
|
|
48
|
+
|
|
49
|
+
with pytest.raises(ValueError, match="Invalid UUID format."):
|
|
50
|
+
_TableContext.verify(valid_table_data)
|
|
51
|
+
|
|
52
|
+
def test_verify_table_raises_for_invalid_owner_id(self, valid_table_data):
|
|
53
|
+
valid_table_data["owner_id"] = 0
|
|
54
|
+
|
|
55
|
+
with pytest.raises(
|
|
56
|
+
ValueError,
|
|
57
|
+
match="Owner ID must be a positive integer or None.",
|
|
58
|
+
):
|
|
59
|
+
_TableContext.verify(valid_table_data)
|
|
60
|
+
|
|
61
|
+
def test_verify_table_raises_for_invalid_namespace(self, valid_table_data):
|
|
62
|
+
valid_table_data["namespace"] = {}
|
|
63
|
+
|
|
64
|
+
with pytest.raises(
|
|
65
|
+
ValueError,
|
|
66
|
+
match="Namespace must be a non-empty string.",
|
|
67
|
+
):
|
|
68
|
+
_TableContext.verify(valid_table_data)
|
|
69
|
+
|
|
70
|
+
def test_verify_keys_accepts_extra_fields(self, valid_table_data):
|
|
71
|
+
valid_table_data["extra"] = "allowed"
|
|
72
|
+
|
|
73
|
+
assert _TableContext._verify_keys(valid_table_data) is None
|
|
74
|
+
|
|
75
|
+
def test_verify_uuid_format_accepts_valid_uuid(self):
|
|
76
|
+
assert _TableContext._verify_uuid_format(str(uuid4())) is None
|
|
77
|
+
|
|
78
|
+
def test_verify_uuid_format_raises_for_invalid_uuid(self):
|
|
79
|
+
with pytest.raises(ValueError, match="Invalid UUID format."):
|
|
80
|
+
_TableContext._verify_uuid_format("123")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestContextBuilder:
|
|
84
|
+
|
|
85
|
+
def test_adds_valid_table_context(self, valid_table_data):
|
|
86
|
+
builder = ContextBuilder()
|
|
87
|
+
builder.register("table", _TableContext)
|
|
88
|
+
|
|
89
|
+
context = builder.add("table", valid_table_data).build()
|
|
90
|
+
|
|
91
|
+
assert context["table"] == valid_table_data
|
|
92
|
+
|
|
93
|
+
def test_add_returns_self_for_chaining(self, valid_table_data):
|
|
94
|
+
builder = ContextBuilder()
|
|
95
|
+
builder.register("table", _TableContext)
|
|
96
|
+
|
|
97
|
+
result = builder.add("table", valid_table_data)
|
|
98
|
+
|
|
99
|
+
assert result is builder
|
|
100
|
+
|
|
101
|
+
def test_rejects_unregistered_context_key(self, valid_table_data):
|
|
102
|
+
builder = ContextBuilder()
|
|
103
|
+
|
|
104
|
+
with pytest.raises(
|
|
105
|
+
ValueError,
|
|
106
|
+
match="unknown rejected: no handler registered",
|
|
107
|
+
):
|
|
108
|
+
builder.add("unknown", valid_table_data)
|
|
109
|
+
|
|
110
|
+
def test_rejects_handler_without_verify_method(self, valid_table_data):
|
|
111
|
+
class BadTableContext:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
builder = ContextBuilder()
|
|
115
|
+
builder.register("something", BadTableContext)
|
|
116
|
+
|
|
117
|
+
with pytest.raises(
|
|
118
|
+
ValueError,
|
|
119
|
+
match="something rejected: 'verify' not implemented",
|
|
120
|
+
):
|
|
121
|
+
builder.add("something", valid_table_data)
|
|
122
|
+
|
|
123
|
+
def test_rejects_invalid_table_context_with_exact_reason(
|
|
124
|
+
self,
|
|
125
|
+
valid_table_data,
|
|
126
|
+
):
|
|
127
|
+
builder = ContextBuilder()
|
|
128
|
+
builder.register("table", _TableContext)
|
|
129
|
+
|
|
130
|
+
valid_table_data["uuid"] = "bad-uuid"
|
|
131
|
+
|
|
132
|
+
with pytest.raises(
|
|
133
|
+
ValueError,
|
|
134
|
+
match="table rejected: verification error - Invalid UUID format.",
|
|
135
|
+
):
|
|
136
|
+
builder.add("table", valid_table_data)
|
|
137
|
+
|
|
138
|
+
def test_uses_verify_table_method(self, valid_table_data):
|
|
139
|
+
builder = ContextBuilder()
|
|
140
|
+
builder.register("table", _TableContext)
|
|
141
|
+
|
|
142
|
+
with patch.object(_TableContext, "verify", return_value=True) as mock_verify:
|
|
143
|
+
builder.add("table", valid_table_data)
|
|
144
|
+
|
|
145
|
+
mock_verify.assert_called_once_with(valid_table_data)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TestStaffContext:
|
|
149
|
+
|
|
150
|
+
def test_verify_staff_allows_none(self):
|
|
151
|
+
assert _StaffContext.verify(None) is True
|
|
152
|
+
|
|
153
|
+
def test_verify_staff_accepts_valid_data(self):
|
|
154
|
+
data = {"email": "test@example.com"}
|
|
155
|
+
|
|
156
|
+
assert _StaffContext.verify(data) is True
|
|
157
|
+
|
|
158
|
+
def test_verify_staff_rejects_non_dict(self):
|
|
159
|
+
with pytest.raises(
|
|
160
|
+
ValueError,
|
|
161
|
+
match="Staff context must be a dictionary.",
|
|
162
|
+
):
|
|
163
|
+
_StaffContext.verify("not-a-dict")
|
|
164
|
+
|
|
165
|
+
def test_verify_staff_rejects_missing_email(self):
|
|
166
|
+
with pytest.raises(
|
|
167
|
+
ValueError,
|
|
168
|
+
match="Missing required field: 'email'",
|
|
169
|
+
):
|
|
170
|
+
_StaffContext.verify({})
|
|
171
|
+
|
|
172
|
+
def test_verify_staff_allows_extra_fields(self):
|
|
173
|
+
data = {
|
|
174
|
+
"email": "test@example.com",
|
|
175
|
+
"name": "John",
|
|
176
|
+
"role": "admin",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
assert _StaffContext.verify(data) is True
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TestMetadataContext:
|
|
183
|
+
|
|
184
|
+
def test_verify_extra_accepts_none(self):
|
|
185
|
+
assert _MetadataContext.verify(None) is True
|
|
186
|
+
|
|
187
|
+
def test_verify_metadata_accepts_dict(self):
|
|
188
|
+
assert _MetadataContext.verify({"foo": "bar"}) is True
|
|
189
|
+
|
|
190
|
+
def test_verify_metadata_accepts_any_type(self):
|
|
191
|
+
test_cases = [
|
|
192
|
+
"string",
|
|
193
|
+
123,
|
|
194
|
+
12.5,
|
|
195
|
+
[],
|
|
196
|
+
(),
|
|
197
|
+
object(),
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
for case in test_cases:
|
|
201
|
+
assert _MetadataContext.verify(case) is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|