hardpy 0.1.0__py3-none-any.whl → 0.3.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.
- hardpy/__init__.py +0 -2
- hardpy/hardpy_panel/api.py +1 -1
- hardpy/hardpy_panel/runner.py +1 -1
- hardpy/pytest_hardpy/db/base_store.py +21 -11
- hardpy/pytest_hardpy/plugin.py +75 -3
- hardpy/pytest_hardpy/pytest_call.py +16 -8
- hardpy/pytest_hardpy/pytest_wrapper.py +3 -0
- hardpy/pytest_hardpy/reporter/base.py +30 -12
- hardpy/pytest_hardpy/reporter/hook_reporter.py +28 -37
- hardpy/pytest_hardpy/utils/node_info.py +66 -0
- {hardpy-0.1.0.dist-info → hardpy-0.3.0.dist-info}/METADATA +27 -15
- {hardpy-0.1.0.dist-info → hardpy-0.3.0.dist-info}/RECORD +15 -15
- {hardpy-0.1.0.dist-info → hardpy-0.3.0.dist-info}/WHEEL +0 -0
- {hardpy-0.1.0.dist-info → hardpy-0.3.0.dist-info}/entry_points.txt +0 -0
- {hardpy-0.1.0.dist-info → hardpy-0.3.0.dist-info}/licenses/LICENSE +0 -0
hardpy/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
3
|
|
|
4
|
-
from hardpy.pytest_hardpy.plugin import HardpyPlugin
|
|
5
4
|
from hardpy.pytest_hardpy.result import CouchdbLoader
|
|
6
5
|
from hardpy.pytest_hardpy.utils import DuplicateSerialNumberError
|
|
7
6
|
from hardpy.pytest_hardpy.result.couchdb_config import CouchdbConfig
|
|
@@ -18,7 +17,6 @@ from hardpy.pytest_hardpy.pytest_call import (
|
|
|
18
17
|
)
|
|
19
18
|
|
|
20
19
|
__all__ = [
|
|
21
|
-
"HardpyPlugin",
|
|
22
20
|
"CouchdbLoader",
|
|
23
21
|
"CouchdbConfig",
|
|
24
22
|
"DuplicateSerialNumberError",
|
hardpy/hardpy_panel/api.py
CHANGED
hardpy/hardpy_panel/runner.py
CHANGED
|
@@ -15,7 +15,7 @@ def run():
|
|
|
15
15
|
config = ConfigData()
|
|
16
16
|
parser = ArgumentParser(description="Usage: hardpy-panel [OPTION]... [PATH]")
|
|
17
17
|
# fmt: off
|
|
18
|
-
parser.add_argument("-dbu", "--db_user", default=config.db_user, help="database user")
|
|
18
|
+
parser.add_argument("-dbu", "--db_user", default=config.db_user, help="database user") # noqa: E501
|
|
19
19
|
parser.add_argument("-dbpw", "--db_pswd", default=config.db_pswd, help="database user password") # noqa: E501
|
|
20
20
|
parser.add_argument("-dbp", "--db_port", type=int, default=config.db_port, help="database port number") # noqa: E501
|
|
21
21
|
parser.add_argument("-dbh", "--db_host", type=str, default=config.db_host, help="database hostname") # noqa: E501
|
|
@@ -36,19 +36,29 @@ class BaseStore(BaseConnector):
|
|
|
36
36
|
"""
|
|
37
37
|
return glom(self._doc, key)
|
|
38
38
|
|
|
39
|
-
def
|
|
40
|
-
"""
|
|
41
|
-
|
|
39
|
+
def update_doc(self, key: str, value):
|
|
40
|
+
"""Update document.
|
|
41
|
+
|
|
42
|
+
HardPy collecting uses a simple key without dots.
|
|
43
|
+
Assign is used to update a document.
|
|
44
|
+
Assign is a longer function.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
key (str): document key
|
|
48
|
+
value: document value
|
|
49
|
+
"""
|
|
50
|
+
if "." in key:
|
|
51
|
+
assign(self._doc, key, value)
|
|
52
|
+
else:
|
|
53
|
+
self._doc[key] = value
|
|
54
|
+
|
|
55
|
+
def update_db(self):
|
|
56
|
+
"""Update database by current document."""
|
|
42
57
|
try:
|
|
43
58
|
self._doc = self._db.save(self._doc)
|
|
44
|
-
except Conflict
|
|
45
|
-
self.
|
|
46
|
-
|
|
47
|
-
f"when trying to save key={key}, value={value}. "
|
|
48
|
-
"Current document will be changed by "
|
|
49
|
-
"document from database."
|
|
50
|
-
)
|
|
51
|
-
self._doc = self._db.get(self._doc_id)
|
|
59
|
+
except Conflict:
|
|
60
|
+
self._doc["_rev"] = self._db.get(self._doc_id)["_rev"]
|
|
61
|
+
self._doc = self._db.save(self._doc)
|
|
52
62
|
|
|
53
63
|
def get_document(self) -> ModelMetaclass:
|
|
54
64
|
"""Get document by schema.
|
hardpy/pytest_hardpy/plugin.py
CHANGED
|
@@ -9,6 +9,7 @@ from platform import system
|
|
|
9
9
|
|
|
10
10
|
from natsort import natsorted
|
|
11
11
|
from pytest import (
|
|
12
|
+
skip,
|
|
12
13
|
exit,
|
|
13
14
|
TestReport,
|
|
14
15
|
Item,
|
|
@@ -27,6 +28,7 @@ from hardpy.pytest_hardpy.utils import (
|
|
|
27
28
|
ProgressCalculator,
|
|
28
29
|
ConfigData,
|
|
29
30
|
)
|
|
31
|
+
from hardpy.pytest_hardpy.utils.node_info import TestDependencyInfo
|
|
30
32
|
|
|
31
33
|
|
|
32
34
|
def pytest_addoption(parser: Parser):
|
|
@@ -37,9 +39,17 @@ def pytest_addoption(parser: Parser):
|
|
|
37
39
|
parser.addoption("--hardpy-dbpw", action="store", default=config_data.db_pswd, help="database user password") # noqa: E501
|
|
38
40
|
parser.addoption("--hardpy-dbp", action="store", default=config_data.db_port, help="database port number") # noqa: E501
|
|
39
41
|
parser.addoption("--hardpy-dbh", action="store", default=config_data.db_host, help="database hostname") # noqa: E501
|
|
42
|
+
parser.addoption("--hardpy-pt", action="store_true", default=False, help="enable pytest-hardpy plugin") # noqa: E501
|
|
40
43
|
# fmt: on
|
|
41
44
|
|
|
42
45
|
|
|
46
|
+
# Bootstrapping hooks
|
|
47
|
+
def pytest_load_initial_conftests(early_config, parser, args):
|
|
48
|
+
if "--hardpy-pt" in args:
|
|
49
|
+
plugin = HardpyPlugin()
|
|
50
|
+
early_config.pluginmanager.register(plugin)
|
|
51
|
+
|
|
52
|
+
|
|
43
53
|
class HardpyPlugin(object):
|
|
44
54
|
"""HardPy integration plugin for pytest.
|
|
45
55
|
|
|
@@ -50,6 +60,7 @@ class HardpyPlugin(object):
|
|
|
50
60
|
self._progress = ProgressCalculator()
|
|
51
61
|
self._results = {}
|
|
52
62
|
self._post_run_functions: list[Callable] = []
|
|
63
|
+
self._dependencies = {}
|
|
53
64
|
|
|
54
65
|
if system() == "Linux":
|
|
55
66
|
signal.signal(signal.SIGTERM, self._stop_handler)
|
|
@@ -69,6 +80,7 @@ class HardpyPlugin(object):
|
|
|
69
80
|
|
|
70
81
|
config.addinivalue_line("markers", "case_name")
|
|
71
82
|
config.addinivalue_line("markers", "module_name")
|
|
83
|
+
config.addinivalue_line("markers", "dependency")
|
|
72
84
|
|
|
73
85
|
# must be init after config data is set
|
|
74
86
|
self._reporter = HookReporter()
|
|
@@ -84,6 +96,8 @@ class HardpyPlugin(object):
|
|
|
84
96
|
return
|
|
85
97
|
status = self._get_run_status(exitstatus)
|
|
86
98
|
self._reporter.finish(status)
|
|
99
|
+
self._reporter.update_db_by_doc()
|
|
100
|
+
self._reporter.compact_all()
|
|
87
101
|
|
|
88
102
|
# call post run methods
|
|
89
103
|
if self._post_run_functions:
|
|
@@ -99,6 +113,7 @@ class HardpyPlugin(object):
|
|
|
99
113
|
self._reporter.init_doc(str(PurePath(config.rootpath).name))
|
|
100
114
|
|
|
101
115
|
nodes = {}
|
|
116
|
+
modules = set()
|
|
102
117
|
|
|
103
118
|
session.items = natsorted(
|
|
104
119
|
session.items,
|
|
@@ -107,18 +122,26 @@ class HardpyPlugin(object):
|
|
|
107
122
|
for item in session.items:
|
|
108
123
|
if item.parent is None:
|
|
109
124
|
continue
|
|
110
|
-
|
|
125
|
+
try:
|
|
126
|
+
node_info = NodeInfo(item)
|
|
127
|
+
except ValueError:
|
|
128
|
+
error_msg = f"Error creating NodeInfo for item: {item}\n"
|
|
129
|
+
exit(error_msg, 1)
|
|
111
130
|
|
|
112
131
|
self._init_case_result(node_info.module_id, node_info.case_id)
|
|
113
|
-
|
|
114
132
|
if node_info.module_id not in nodes:
|
|
115
133
|
nodes[node_info.module_id] = [node_info.case_id]
|
|
116
134
|
else:
|
|
117
135
|
nodes[node_info.module_id].append(node_info.case_id)
|
|
118
136
|
|
|
119
137
|
self._reporter.add_case(node_info)
|
|
120
|
-
|
|
138
|
+
|
|
139
|
+
self._add_dependency(node_info, nodes)
|
|
140
|
+
modules.add(node_info.module_id)
|
|
141
|
+
for module_id in modules:
|
|
142
|
+
self._reporter.set_module_status(module_id, TestStatus.READY)
|
|
121
143
|
self._reporter.update_node_order(nodes)
|
|
144
|
+
self._reporter.update_db_by_doc()
|
|
122
145
|
|
|
123
146
|
# Test running (runtest) hooks
|
|
124
147
|
|
|
@@ -131,6 +154,7 @@ class HardpyPlugin(object):
|
|
|
131
154
|
|
|
132
155
|
# testrun entrypoint
|
|
133
156
|
self._reporter.start()
|
|
157
|
+
self._reporter.update_db_by_doc()
|
|
134
158
|
|
|
135
159
|
def pytest_runtest_setup(self, item: Item):
|
|
136
160
|
"""Call before each test setup phase."""
|
|
@@ -140,6 +164,8 @@ class HardpyPlugin(object):
|
|
|
140
164
|
|
|
141
165
|
node_info = NodeInfo(item)
|
|
142
166
|
|
|
167
|
+
self._handle_dependency(node_info)
|
|
168
|
+
|
|
143
169
|
self._reporter.set_module_status(node_info.module_id, TestStatus.RUN)
|
|
144
170
|
self._reporter.set_module_start_time(node_info.module_id)
|
|
145
171
|
self._reporter.set_case_status(
|
|
@@ -151,6 +177,7 @@ class HardpyPlugin(object):
|
|
|
151
177
|
node_info.module_id,
|
|
152
178
|
node_info.case_id,
|
|
153
179
|
)
|
|
180
|
+
self._reporter.update_db_by_doc()
|
|
154
181
|
|
|
155
182
|
# Reporting hooks
|
|
156
183
|
|
|
@@ -180,6 +207,7 @@ class HardpyPlugin(object):
|
|
|
180
207
|
|
|
181
208
|
if None not in self._results[module_id].values():
|
|
182
209
|
self._collect_module_result(module_id)
|
|
210
|
+
self._reporter.update_db_by_doc()
|
|
183
211
|
|
|
184
212
|
# Fixture
|
|
185
213
|
|
|
@@ -242,3 +270,47 @@ class HardpyPlugin(object):
|
|
|
242
270
|
index = report.find("\nE")
|
|
243
271
|
return report[:index]
|
|
244
272
|
return None
|
|
273
|
+
|
|
274
|
+
def _handle_dependency(self, node_info: NodeInfo):
|
|
275
|
+
dependency = self._dependencies.get(
|
|
276
|
+
TestDependencyInfo(
|
|
277
|
+
node_info.module_id,
|
|
278
|
+
node_info.case_id,
|
|
279
|
+
)
|
|
280
|
+
)
|
|
281
|
+
if dependency and self._is_dependency_failed(dependency):
|
|
282
|
+
self._log.debug(f"Skipping test due to dependency: {dependency}")
|
|
283
|
+
self._results[node_info.module_id][node_info.case_id] = TestStatus.SKIPPED
|
|
284
|
+
skip(f"Test {node_info.module_id}::{node_info.case_id} is skipped")
|
|
285
|
+
|
|
286
|
+
def _is_dependency_failed(self, dependency) -> bool:
|
|
287
|
+
if isinstance(dependency, TestDependencyInfo):
|
|
288
|
+
incorrect_status = {
|
|
289
|
+
TestStatus.FAILED,
|
|
290
|
+
TestStatus.SKIPPED,
|
|
291
|
+
TestStatus.ERROR,
|
|
292
|
+
}
|
|
293
|
+
module_id, case_id = dependency
|
|
294
|
+
if case_id is not None:
|
|
295
|
+
return self._results[module_id][case_id] in incorrect_status
|
|
296
|
+
return any(
|
|
297
|
+
status in incorrect_status
|
|
298
|
+
for status in set(self._results[module_id].values())
|
|
299
|
+
)
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
def _add_dependency(self, node_info, nodes):
|
|
303
|
+
dependency = node_info.dependency
|
|
304
|
+
if dependency is None or dependency == "":
|
|
305
|
+
return
|
|
306
|
+
module_id, case_id = dependency
|
|
307
|
+
if module_id not in nodes:
|
|
308
|
+
error_message = f"Error: Module dependency '{dependency}' not found."
|
|
309
|
+
exit(error_message, 1)
|
|
310
|
+
elif case_id not in nodes[module_id] and case_id is not None:
|
|
311
|
+
error_message = f"Error: Case dependency '{dependency}' not found."
|
|
312
|
+
exit(error_message, 1)
|
|
313
|
+
|
|
314
|
+
self._dependencies[
|
|
315
|
+
TestDependencyInfo(node_info.module_id, node_info.case_id)
|
|
316
|
+
] = dependency
|
|
@@ -52,7 +52,8 @@ def set_dut_info(info: dict):
|
|
|
52
52
|
reporter = RunnerReporter()
|
|
53
53
|
for dut_key, dut_value in info.items():
|
|
54
54
|
key = reporter.generate_key(DF.DUT, DF.INFO, dut_key)
|
|
55
|
-
reporter.
|
|
55
|
+
reporter.set_doc_value(key, dut_value)
|
|
56
|
+
reporter.update_db_by_doc()
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
def set_dut_serial_number(serial_number: str):
|
|
@@ -68,7 +69,8 @@ def set_dut_serial_number(serial_number: str):
|
|
|
68
69
|
key = reporter.generate_key(DF.DUT, DF.SERIAL_NUMBER)
|
|
69
70
|
if reporter.get_field(key):
|
|
70
71
|
raise DuplicateSerialNumberError
|
|
71
|
-
reporter.
|
|
72
|
+
reporter.set_doc_value(key, serial_number)
|
|
73
|
+
reporter.update_db_by_doc()
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
def set_stand_info(info: dict):
|
|
@@ -80,7 +82,8 @@ def set_stand_info(info: dict):
|
|
|
80
82
|
reporter = RunnerReporter()
|
|
81
83
|
for stand_key, stand_value in info.items():
|
|
82
84
|
key = reporter.generate_key(DF.TEST_STAND, stand_key)
|
|
83
|
-
reporter.
|
|
85
|
+
reporter.set_doc_value(key, stand_value)
|
|
86
|
+
reporter.update_db_by_doc()
|
|
84
87
|
|
|
85
88
|
|
|
86
89
|
def set_message(msg: str, msg_key: Optional[str] = None) -> None:
|
|
@@ -111,7 +114,8 @@ def set_message(msg: str, msg_key: Optional[str] = None) -> None:
|
|
|
111
114
|
|
|
112
115
|
msgs[msg_key] = msg
|
|
113
116
|
|
|
114
|
-
reporter.
|
|
117
|
+
reporter.set_doc_value(key, msgs)
|
|
118
|
+
reporter.update_db_by_doc()
|
|
115
119
|
|
|
116
120
|
|
|
117
121
|
def set_case_artifact(data: dict):
|
|
@@ -134,7 +138,8 @@ def set_case_artifact(data: dict):
|
|
|
134
138
|
DF.ARTIFACT,
|
|
135
139
|
stand_key,
|
|
136
140
|
)
|
|
137
|
-
reporter.
|
|
141
|
+
reporter.set_doc_value(key, stand_value, runstore_only=True)
|
|
142
|
+
reporter.update_db_by_doc()
|
|
138
143
|
|
|
139
144
|
|
|
140
145
|
def set_module_artifact(data: dict):
|
|
@@ -155,7 +160,8 @@ def set_module_artifact(data: dict):
|
|
|
155
160
|
DF.ARTIFACT,
|
|
156
161
|
artifact_key,
|
|
157
162
|
)
|
|
158
|
-
reporter.
|
|
163
|
+
reporter.set_doc_value(key, artifact_value, runstore_only=True)
|
|
164
|
+
reporter.update_db_by_doc()
|
|
159
165
|
|
|
160
166
|
|
|
161
167
|
def set_run_artifact(data: dict):
|
|
@@ -173,7 +179,8 @@ def set_run_artifact(data: dict):
|
|
|
173
179
|
DF.ARTIFACT,
|
|
174
180
|
artifact_key,
|
|
175
181
|
)
|
|
176
|
-
reporter.
|
|
182
|
+
reporter.set_doc_value(key, artifact_value, runstore_only=True)
|
|
183
|
+
reporter.update_db_by_doc()
|
|
177
184
|
|
|
178
185
|
|
|
179
186
|
def set_driver_info(drivers: dict) -> None:
|
|
@@ -192,7 +199,8 @@ def set_driver_info(drivers: dict) -> None:
|
|
|
192
199
|
DF.DRIVERS,
|
|
193
200
|
driver_name,
|
|
194
201
|
)
|
|
195
|
-
reporter.
|
|
202
|
+
reporter.set_doc_value(key, driver_data)
|
|
203
|
+
reporter.update_db_by_doc()
|
|
196
204
|
|
|
197
205
|
|
|
198
206
|
def _get_current_test() -> CurrentTestInfo:
|
|
@@ -46,6 +46,7 @@ class PyTestWrapper(object):
|
|
|
46
46
|
self.config.db_user,
|
|
47
47
|
"--hardpy-dbpw",
|
|
48
48
|
self.config.db_pswd,
|
|
49
|
+
"--hardpy-pt",
|
|
49
50
|
],
|
|
50
51
|
cwd=self.config.tests_dir.absolute(),
|
|
51
52
|
)
|
|
@@ -63,6 +64,7 @@ class PyTestWrapper(object):
|
|
|
63
64
|
self.config.db_user,
|
|
64
65
|
"--hardpy-dbpw",
|
|
65
66
|
self.config.db_pswd,
|
|
67
|
+
"--hardpy-pt",
|
|
66
68
|
],
|
|
67
69
|
cwd=self.config.tests_dir.absolute(),
|
|
68
70
|
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
|
@@ -107,6 +109,7 @@ class PyTestWrapper(object):
|
|
|
107
109
|
self.config.db_user,
|
|
108
110
|
"--hardpy-dbpw",
|
|
109
111
|
self.config.db_pswd,
|
|
112
|
+
"--hardpy-pt",
|
|
110
113
|
],
|
|
111
114
|
cwd=self.config.tests_dir.absolute(),
|
|
112
115
|
)
|
|
@@ -14,21 +14,39 @@ class BaseReporter(object):
|
|
|
14
14
|
self._runstore = RunStore()
|
|
15
15
|
self._log = getLogger(__name__)
|
|
16
16
|
|
|
17
|
-
def
|
|
18
|
-
|
|
17
|
+
def set_doc_value(
|
|
18
|
+
self, key: str, value, runstore_only=False, statestore_only=False
|
|
19
|
+
):
|
|
20
|
+
"""Set value to the document.
|
|
21
|
+
|
|
22
|
+
Update a document without writing to the database.
|
|
19
23
|
|
|
20
24
|
Args:
|
|
21
|
-
key (str):
|
|
22
|
-
value
|
|
23
|
-
|
|
24
|
-
be written to
|
|
25
|
-
|
|
26
|
-
be written to
|
|
25
|
+
key (str): document key
|
|
26
|
+
value: document value
|
|
27
|
+
runstore_only (bool, optional): indicates whether data should
|
|
28
|
+
be written to RunStore. Defaults to True.
|
|
29
|
+
statestore_only (bool, optional): indicates whether data should
|
|
30
|
+
be written to StateStore. Defaults to True.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValueError: if both runstore_only and statestore_only are True
|
|
27
34
|
"""
|
|
28
|
-
if
|
|
29
|
-
|
|
30
|
-
if
|
|
31
|
-
self._runstore.
|
|
35
|
+
if runstore_only and statestore_only:
|
|
36
|
+
raise ValueError("Both runstore_only and statestore_only cannot be True")
|
|
37
|
+
if runstore_only:
|
|
38
|
+
self._runstore.update_doc(key, value)
|
|
39
|
+
return
|
|
40
|
+
if statestore_only:
|
|
41
|
+
self._statestore.update_doc(key, value)
|
|
42
|
+
return
|
|
43
|
+
self._runstore.update_doc(key, value)
|
|
44
|
+
self._statestore.update_doc(key, value)
|
|
45
|
+
|
|
46
|
+
def update_db_by_doc(self):
|
|
47
|
+
"""Update database by current document."""
|
|
48
|
+
self._statestore.update_db()
|
|
49
|
+
self._runstore.update_db()
|
|
32
50
|
|
|
33
51
|
def generate_key(self, *args) -> str:
|
|
34
52
|
"""Generate key for database.
|
|
@@ -25,23 +25,23 @@ class HookReporter(BaseReporter):
|
|
|
25
25
|
Args:
|
|
26
26
|
doc_name (str): test run name
|
|
27
27
|
"""
|
|
28
|
-
self.
|
|
29
|
-
self.
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
32
|
-
self.
|
|
33
|
-
self.
|
|
34
|
-
self.
|
|
35
|
-
self.
|
|
28
|
+
self.set_doc_value(DF.NAME, doc_name)
|
|
29
|
+
self.set_doc_value(DF.STATUS, TestStatus.READY)
|
|
30
|
+
self.set_doc_value(DF.START_TIME, None)
|
|
31
|
+
self.set_doc_value(DF.TIMEZONE, None)
|
|
32
|
+
self.set_doc_value(DF.STOP_TIME, None)
|
|
33
|
+
self.set_doc_value(DF.PROGRESS, 0)
|
|
34
|
+
self.set_doc_value(DF.DRIVERS, {})
|
|
35
|
+
self.set_doc_value(DF.ARTIFACT, {}, runstore_only=True)
|
|
36
36
|
|
|
37
37
|
def start(self):
|
|
38
38
|
"""Start test."""
|
|
39
39
|
self._log.debug("Starting test run.")
|
|
40
40
|
start_time = int(time())
|
|
41
|
-
self.
|
|
42
|
-
self.
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
41
|
+
self.set_doc_value(DF.START_TIME, start_time)
|
|
42
|
+
self.set_doc_value(DF.STATUS, TestStatus.RUN)
|
|
43
|
+
self.set_doc_value(DF.TIMEZONE, tzname) # noqa: WPS432
|
|
44
|
+
self.set_doc_value(DF.PROGRESS, 0)
|
|
45
45
|
|
|
46
46
|
def finish(self, status: RunStatus):
|
|
47
47
|
"""Finish test.
|
|
@@ -50,15 +50,11 @@ class HookReporter(BaseReporter):
|
|
|
50
50
|
"""
|
|
51
51
|
self._log.debug("Finishing test run.")
|
|
52
52
|
stop_time = int(time())
|
|
53
|
-
self.
|
|
54
|
-
self.
|
|
55
|
-
|
|
56
|
-
if self._statestore.get_document():
|
|
57
|
-
self._log.debug("Report StateStore has been successfully validated.")
|
|
58
|
-
|
|
59
|
-
if self._runstore.get_document():
|
|
60
|
-
self._log.debug("Report RunStore has been successfully validated.")
|
|
53
|
+
self.set_doc_value(DF.STOP_TIME, stop_time)
|
|
54
|
+
self.set_doc_value(DF.STATUS, status)
|
|
61
55
|
|
|
56
|
+
def compact_all(self):
|
|
57
|
+
"""Compact all databases"""
|
|
62
58
|
self._statestore.compact()
|
|
63
59
|
self._runstore.compact()
|
|
64
60
|
|
|
@@ -68,7 +64,7 @@ class HookReporter(BaseReporter):
|
|
|
68
64
|
Args:
|
|
69
65
|
progress (int): test progress
|
|
70
66
|
"""
|
|
71
|
-
self.
|
|
67
|
+
self.set_doc_value(DF.PROGRESS, progress)
|
|
72
68
|
|
|
73
69
|
def set_assertion_msg(self, module_id: str, case_id: str, msg: str | None):
|
|
74
70
|
"""Set case assertion message.
|
|
@@ -81,7 +77,7 @@ class HookReporter(BaseReporter):
|
|
|
81
77
|
key = self.generate_key(
|
|
82
78
|
DF.MODULES, module_id, DF.CASES, case_id, DF.ASSERTION_MSG
|
|
83
79
|
)
|
|
84
|
-
self.
|
|
80
|
+
self.set_doc_value(key, msg)
|
|
85
81
|
|
|
86
82
|
def add_case(self, node_info: NodeInfo):
|
|
87
83
|
"""Add test case to document.
|
|
@@ -90,18 +86,15 @@ class HookReporter(BaseReporter):
|
|
|
90
86
|
node_info (NodeInfo): node info
|
|
91
87
|
"""
|
|
92
88
|
key = DF.MODULES
|
|
89
|
+
|
|
93
90
|
item_statestore = self._statestore.get_field(key)
|
|
94
91
|
item_runstore = self._runstore.get_field(key)
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
item_runstore,
|
|
99
|
-
node_info,
|
|
100
|
-
is_use_artifact=True,
|
|
101
|
-
)
|
|
93
|
+
self._init_case(item_statestore, node_info)
|
|
94
|
+
self._init_case(item_runstore, node_info, is_use_artifact=True)
|
|
102
95
|
|
|
103
|
-
self.
|
|
104
|
-
self.
|
|
96
|
+
self.set_doc_value(key, item_statestore, statestore_only=True)
|
|
97
|
+
self.set_doc_value(key, item_runstore, runstore_only=True)
|
|
105
98
|
|
|
106
99
|
def set_case_status(self, module_id: str, case_id: str, status: TestStatus):
|
|
107
100
|
"""Set test case status.
|
|
@@ -112,7 +105,7 @@ class HookReporter(BaseReporter):
|
|
|
112
105
|
status (TestStatus): test case status
|
|
113
106
|
"""
|
|
114
107
|
key = self.generate_key(DF.MODULES, module_id, DF.CASES, case_id, DF.STATUS)
|
|
115
|
-
self.
|
|
108
|
+
self.set_doc_value(key, status)
|
|
116
109
|
|
|
117
110
|
def set_case_start_time(self, module_id: str, case_id: str):
|
|
118
111
|
"""Set test case start_time.
|
|
@@ -142,7 +135,7 @@ class HookReporter(BaseReporter):
|
|
|
142
135
|
status (TestStatus): test module status
|
|
143
136
|
"""
|
|
144
137
|
key = self.generate_key(DF.MODULES, module_id, DF.STATUS)
|
|
145
|
-
self.
|
|
138
|
+
self.set_doc_value(key, status)
|
|
146
139
|
|
|
147
140
|
def set_module_start_time(self, module_id: str):
|
|
148
141
|
"""Set test module status.
|
|
@@ -175,16 +168,16 @@ class HookReporter(BaseReporter):
|
|
|
175
168
|
rm_outdated_nodes = self._remove_outdate_node(old_modules, modules_copy, nodes)
|
|
176
169
|
updated_case_order = self._update_case_order(rm_outdated_nodes, nodes)
|
|
177
170
|
updated_module_order = self._update_module_order(updated_case_order)
|
|
178
|
-
self.
|
|
171
|
+
self.set_doc_value(key, updated_module_order, statestore_only=True)
|
|
179
172
|
|
|
180
173
|
def _set_time(self, key: str):
|
|
181
174
|
current_time = self._statestore.get_field(key)
|
|
182
175
|
if current_time is None:
|
|
183
|
-
self.
|
|
176
|
+
self.set_doc_value(key, int(time()))
|
|
184
177
|
|
|
185
178
|
def _init_case(
|
|
186
179
|
self, item: dict, node_info: NodeInfo, is_use_artifact: bool = False
|
|
187
|
-
)
|
|
180
|
+
):
|
|
188
181
|
module_default = { # noqa: WPS204
|
|
189
182
|
DF.STATUS: TestStatus.READY,
|
|
190
183
|
DF.NAME: self._get_module_name(node_info),
|
|
@@ -216,8 +209,6 @@ class HookReporter(BaseReporter):
|
|
|
216
209
|
case_default[DF.ARTIFACT] = {}
|
|
217
210
|
item[node_info.module_id][DF.CASES][node_info.case_id] = case_default
|
|
218
211
|
|
|
219
|
-
return item
|
|
220
|
-
|
|
221
212
|
def _remove_outdate_node(
|
|
222
213
|
self, old_modules: dict, new_modules: dict, nodes: dict
|
|
223
214
|
) -> dict:
|
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
# Copyright (c) 2024 Everypin
|
|
2
2
|
# GNU General Public License v3.0 (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
3
3
|
|
|
4
|
+
import re
|
|
4
5
|
from logging import getLogger
|
|
5
6
|
from pathlib import Path
|
|
7
|
+
from typing import NamedTuple
|
|
6
8
|
|
|
7
9
|
from pytest import Item, Mark
|
|
8
10
|
|
|
9
11
|
|
|
12
|
+
class TestDependencyInfo(NamedTuple):
|
|
13
|
+
"""Test info."""
|
|
14
|
+
|
|
15
|
+
def __repr__(self) -> str:
|
|
16
|
+
return f"Dependency: {self.module_id}::{self.case_id}"
|
|
17
|
+
|
|
18
|
+
module_id: str
|
|
19
|
+
case_id: str | None
|
|
20
|
+
|
|
21
|
+
|
|
10
22
|
class NodeInfo(object):
|
|
11
23
|
"""Test node info."""
|
|
12
24
|
|
|
@@ -22,25 +34,59 @@ class NodeInfo(object):
|
|
|
22
34
|
item.parent.own_markers,
|
|
23
35
|
"module_name",
|
|
24
36
|
)
|
|
37
|
+
|
|
38
|
+
self._dependency = self._get_dependency_info(
|
|
39
|
+
item.own_markers + item.parent.own_markers
|
|
40
|
+
)
|
|
41
|
+
|
|
25
42
|
self._module_id = Path(item.parent.nodeid).stem
|
|
26
43
|
self._case_id = item.name
|
|
27
44
|
|
|
28
45
|
@property
|
|
29
46
|
def module_id(self):
|
|
47
|
+
"""Get module id.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str: module id
|
|
51
|
+
"""
|
|
30
52
|
return self._module_id
|
|
31
53
|
|
|
32
54
|
@property
|
|
33
55
|
def case_id(self):
|
|
56
|
+
"""Get case id.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
str: case id
|
|
60
|
+
"""
|
|
34
61
|
return self._case_id
|
|
35
62
|
|
|
36
63
|
@property
|
|
37
64
|
def module_name(self):
|
|
65
|
+
"""Get module name.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
str: module name
|
|
69
|
+
"""
|
|
38
70
|
return self._module_name
|
|
39
71
|
|
|
40
72
|
@property
|
|
41
73
|
def case_name(self):
|
|
74
|
+
"""Get case name.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
str: case name
|
|
78
|
+
"""
|
|
42
79
|
return self._case_name
|
|
43
80
|
|
|
81
|
+
@property
|
|
82
|
+
def dependency(self):
|
|
83
|
+
"""Get dependency information.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
TestDependencyInfo | str: Parsed dependency information.
|
|
87
|
+
"""
|
|
88
|
+
return self._dependency
|
|
89
|
+
|
|
44
90
|
def _get_human_name(self, markers: list[Mark], marker_name: str) -> str:
|
|
45
91
|
"""Get human name from markers.
|
|
46
92
|
|
|
@@ -57,3 +103,23 @@ class NodeInfo(object):
|
|
|
57
103
|
return marker.args[0]
|
|
58
104
|
|
|
59
105
|
return ""
|
|
106
|
+
|
|
107
|
+
def _get_dependency_info(self, markers: list[Mark]) -> TestDependencyInfo | str:
|
|
108
|
+
"""Extract and parse dependency information.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
markers (list[Mark]): item markers list
|
|
112
|
+
marker_name (str): marker name
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
TestDependencyInfo | str | None: Parsed dependency information.
|
|
116
|
+
"""
|
|
117
|
+
dependency_value = self._get_human_name(markers, "dependency")
|
|
118
|
+
dependency_data = re.search(r"(\w+)::(\w+)", dependency_value)
|
|
119
|
+
if dependency_data:
|
|
120
|
+
return TestDependencyInfo(*dependency_data.groups())
|
|
121
|
+
elif re.search(r"^\w+$", dependency_value):
|
|
122
|
+
return TestDependencyInfo(dependency_value, None)
|
|
123
|
+
elif dependency_data is None and dependency_value == "":
|
|
124
|
+
return ""
|
|
125
|
+
raise ValueError
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: hardpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: HardPy library for device testing
|
|
5
5
|
Project-URL: repository, https://github.com/everypindevices/hardpy
|
|
6
6
|
Author: Everypin
|
|
@@ -36,7 +36,7 @@ Requires-Dist: wemake-python-styleguide; extra == 'dev'
|
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
37
37
|
|
|
38
38
|
<h1 align="center">
|
|
39
|
-
<img src="
|
|
39
|
+
<img src="https://everypinio.github.io/hardpy/img/logo512.png" alt="HardPy" style="width:200px;">
|
|
40
40
|
</h1>
|
|
41
41
|
|
|
42
42
|
<h1 align="center">
|
|
@@ -47,6 +47,16 @@ Description-Content-Type: text/markdown
|
|
|
47
47
|
HardPy is a python library for creating a test bench for devices.
|
|
48
48
|
</p>
|
|
49
49
|
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
**Documentation**: <a href=https://everypinio.github.io/hardpy/ target="_blank">https://everypinio.github.io/hardpy/</a>
|
|
53
|
+
|
|
54
|
+
**Source Code**: <a href=https://github.com/everypinio/hardpy target="_blank">https://github.com/everypinio/hardpy</a>
|
|
55
|
+
|
|
56
|
+
**PyPi**: <a href=https://pypi.org/project/hardpy/ target="_blank">https://pypi.org/project/hardpy/</a>
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
50
60
|
## Overview
|
|
51
61
|
|
|
52
62
|
HardPy allows you to:
|
|
@@ -69,7 +79,12 @@ Find examples of using the **HardPy** in the `examples` folder.
|
|
|
69
79
|
|
|
70
80
|
#### CouchDB
|
|
71
81
|
|
|
82
|
+
This is a simple instruction for Linux.
|
|
83
|
+
For Windows, follow the instructions from the
|
|
84
|
+
[documentation](https://everypinio.github.io/hardpy/documentation/database/#couchdb-instance).
|
|
85
|
+
|
|
72
86
|
Launch CouchDB with Docker.
|
|
87
|
+
The Docker version must be 24.0.0 or higher.
|
|
73
88
|
Create `couchdb.ini` file:
|
|
74
89
|
|
|
75
90
|
```ini
|
|
@@ -86,21 +101,18 @@ headers = accept, authorization, content-type, origin, referer, x-csrf-token
|
|
|
86
101
|
Run the Docker container from folder with couchdb.ini file:
|
|
87
102
|
|
|
88
103
|
```bash
|
|
89
|
-
docker run --name couchdb -p 5984:5984 -e COUCHDB_USER=dev -e COUCHDB_PASSWORD=dev -v ./couchdb.ini:/opt/couchdb/etc/local.ini couchdb:3.3
|
|
104
|
+
docker run --rm --name couchdb -p 5984:5984 -e COUCHDB_USER=dev -e COUCHDB_PASSWORD=dev -v ./couchdb.ini:/opt/couchdb/etc/local.ini couchdb:3.3
|
|
90
105
|
```
|
|
91
106
|
|
|
92
|
-
|
|
107
|
+
Command for Windows:
|
|
93
108
|
|
|
94
|
-
|
|
109
|
+
```bash
|
|
110
|
+
docker run --rm --name couchdb -p 5984:5984 -e COUCHDB_USER=dev -e COUCHDB_PASSWORD=dev -v .\couchdb.ini:/opt/couchdb/etc/local.ini couchdb:3.3.2
|
|
111
|
+
```
|
|
95
112
|
|
|
96
|
-
|
|
97
|
-
# conftest.py
|
|
98
|
-
import pytest
|
|
99
|
-
import hardpy
|
|
113
|
+
#### Test steps
|
|
100
114
|
|
|
101
|
-
|
|
102
|
-
config.pluginmanager.register(hardpy.HardpyPlugin())
|
|
103
|
-
```
|
|
115
|
+
Add simple test to `tests` folder
|
|
104
116
|
|
|
105
117
|
```python
|
|
106
118
|
# test_1.py
|
|
@@ -114,16 +126,16 @@ def test_one():
|
|
|
114
126
|
Launch `hardpy-panel` from tests folder or launch `hardpy-panel tests` and open page http://localhost:8000/ in browser.
|
|
115
127
|
|
|
116
128
|
<h1 align="center">
|
|
117
|
-
<img src="
|
|
129
|
+
<img src="https://everypinio.github.io/hardpy/img/hardpy_operator_panel_hello_hardpy.png"
|
|
118
130
|
alt="hardpy operator panel" style="width:600px;">
|
|
119
131
|
</h1>
|
|
120
132
|
|
|
121
133
|
#### Test report
|
|
122
134
|
|
|
123
135
|
The last test report is stored in **runstore** database, document - **current**.
|
|
124
|
-
You can view the CouchDB instance through Fauxton web interface: http://
|
|
136
|
+
You can view the CouchDB instance through Fauxton web interface: http://localhost:5984/_utils
|
|
125
137
|
|
|
126
138
|
<h1 align="center">
|
|
127
|
-
<img src="
|
|
139
|
+
<img src="https://everypinio.github.io/hardpy/img/runstore_hello_hardpy.png"
|
|
128
140
|
alt="hardpy runstore" style="width:600px;">
|
|
129
141
|
</h1>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
hardpy/__init__.py,sha256=
|
|
1
|
+
hardpy/__init__.py,sha256=RwEQwrdXAtjf2UKOTyXiOUvMKOpXCy7TrJR03V4HY98,864
|
|
2
2
|
hardpy/hardpy_panel/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
hardpy/hardpy_panel/api.py,sha256=
|
|
4
|
-
hardpy/hardpy_panel/runner.py,sha256=
|
|
3
|
+
hardpy/hardpy_panel/api.py,sha256=ool8MvCH9SFGHooOloboListg1Mas4FciMQsyRjhWGE,1567
|
|
4
|
+
hardpy/hardpy_panel/runner.py,sha256=J8c2Pzemwr1Xzs0SOIqLqUNNf_4Jb5BJN_fo4pU3Za4,1866
|
|
5
5
|
hardpy/hardpy_panel/frontend/dist/asset-manifest.json,sha256=Z6KDx9WMNghh4d_7w52kTSdEec-lup56JR6JEP3WY94,2824
|
|
6
6
|
hardpy/hardpy_panel/frontend/dist/favicon.ico,sha256=sgIk5PKUKEKBDpkSrc8dJgjpObp0iF82Mec0GpfKId4,15406
|
|
7
7
|
hardpy/hardpy_panel/frontend/dist/index.html,sha256=u1IJG5LkBLmaPgL-dYJ4rIfmxzf0xNhmmOupezuobgg,656
|
|
@@ -36,20 +36,20 @@ hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.afbadb627d43b7
|
|
|
36
36
|
hardpy/hardpy_panel/frontend/dist/static/media/blueprint-icons-20.e857f5a5132b8bfa71a1.woff,sha256=mQZTxE1PyyAL16VWuASOvXlZFwuI4aCPvbrhfgpdIdU,55356
|
|
37
37
|
hardpy/hardpy_panel/frontend/dist/static/media/logo_smol.5b16f92447a4a9e80331.png,sha256=E4K7drvhJCg9HcTpRihOXZhVJVBZ7-W97Se-3tDb46o,14485
|
|
38
38
|
hardpy/pytest_hardpy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
|
-
hardpy/pytest_hardpy/plugin.py,sha256=
|
|
40
|
-
hardpy/pytest_hardpy/pytest_call.py,sha256=
|
|
41
|
-
hardpy/pytest_hardpy/pytest_wrapper.py,sha256=
|
|
39
|
+
hardpy/pytest_hardpy/plugin.py,sha256=lL3ZNVVSlS7VzpVDiF_SWbnbFBbK9vXtD6JZwqBoObA,10973
|
|
40
|
+
hardpy/pytest_hardpy/pytest_call.py,sha256=CrbAw_W6A1HU06xIcylivgNKVUzGoo0B0ikNqflHbDo,6171
|
|
41
|
+
hardpy/pytest_hardpy/pytest_wrapper.py,sha256=bC8ROwAEjo3TpXpNtHXUS-C_fpIyeCPtgfGBSZ82aI8,3554
|
|
42
42
|
hardpy/pytest_hardpy/db/__init__.py,sha256=MxDufncz0zgRAxrndvPXXW4NrU7rRP7MzIrR7S5Cwwo,558
|
|
43
43
|
hardpy/pytest_hardpy/db/base_connector.py,sha256=7KUgPY-GmAo8MFN4OFpG5y3WH1xjohRnpeQ1gxQF1tg,751
|
|
44
44
|
hardpy/pytest_hardpy/db/base_server.py,sha256=uBnq5zGkzEIq_EGzLw0C8kfDEDvQyN52Y6L41KKL9FQ,397
|
|
45
|
-
hardpy/pytest_hardpy/db/base_store.py,sha256=
|
|
45
|
+
hardpy/pytest_hardpy/db/base_store.py,sha256=DiYaBOwufEOdtDpo9dUb3ZaZ7-c1FInAWjLpUXSEFHA,2668
|
|
46
46
|
hardpy/pytest_hardpy/db/const.py,sha256=ffYW54TP0aNF5LhW3g_2G5kVuvqAMWfuJqNDzWZg2nI,618
|
|
47
47
|
hardpy/pytest_hardpy/db/runstore.py,sha256=50amoTIO7OTqd5Ks1_7uTzqjCldLpTapkxbIQOgj1sQ,1023
|
|
48
48
|
hardpy/pytest_hardpy/db/schema.py,sha256=iIclTudP0tauTWLYiEW9MMlvBfuWOteA7eRzDU5gKwI,6859
|
|
49
49
|
hardpy/pytest_hardpy/db/statestore.py,sha256=1BUfA4oqG4vx7z5v_uUYi_Un6YA769JeuShxDicrl9Q,636
|
|
50
50
|
hardpy/pytest_hardpy/reporter/__init__.py,sha256=RONapygH3c_FyXokAlyCVJXGV2cV_jCYDxLymvvA1uE,322
|
|
51
|
-
hardpy/pytest_hardpy/reporter/base.py,sha256=
|
|
52
|
-
hardpy/pytest_hardpy/reporter/hook_reporter.py,sha256=
|
|
51
|
+
hardpy/pytest_hardpy/reporter/base.py,sha256=M-lwli64ty9FW8HlGEpUyoFsZv48tyNgzPjCWVUrATY,1941
|
|
52
|
+
hardpy/pytest_hardpy/reporter/hook_reporter.py,sha256=TE6IzQVH10ce8JXWtep80kgyVOicU02C75FDpV6NfSQ,9895
|
|
53
53
|
hardpy/pytest_hardpy/reporter/runner_reporter.py,sha256=NXkBIoERqmLI-GYtHavmOWC5t6NIpcAE-NECrUKIAJs,827
|
|
54
54
|
hardpy/pytest_hardpy/result/__init__.py,sha256=NMeCGx3yh8ds9VpaUpuNFDxbwgYFq3e-o7W6rYIv8uI,346
|
|
55
55
|
hardpy/pytest_hardpy/result/couchdb_config.py,sha256=QZryfA2QoHIjzbVT3OAD76DCNppCghtRWdZMZ5v7KhY,611
|
|
@@ -61,11 +61,11 @@ hardpy/pytest_hardpy/utils/__init__.py,sha256=IeOr27pgzvMolQtEXJxODJKNdQAFww8Ejj
|
|
|
61
61
|
hardpy/pytest_hardpy/utils/config_data.py,sha256=F8khHsvkEsJjDnoHeLjI0rgsAfETN7nSlEP2snf2kio,990
|
|
62
62
|
hardpy/pytest_hardpy/utils/const.py,sha256=rjW1Rzhe2vCr8GeQqeN_pafepGDYhjhY4u1VfTOVI6U,625
|
|
63
63
|
hardpy/pytest_hardpy/utils/exception.py,sha256=5GnVkOchSPDEXaOXaruO0YzKXoY7b3Y5mVU5-51ZKRg,457
|
|
64
|
-
hardpy/pytest_hardpy/utils/node_info.py,sha256=
|
|
64
|
+
hardpy/pytest_hardpy/utils/node_info.py,sha256=VnEbhKBNAL5xpuFtJTCg90TmkjkFCQA59F5W2RcOlx4,3157
|
|
65
65
|
hardpy/pytest_hardpy/utils/progress_calculator.py,sha256=r0qb3p6_yDIyLeCshF3Ceo5pCzd3BoTahL4rCD2oMNw,1041
|
|
66
66
|
hardpy/pytest_hardpy/utils/singleton.py,sha256=C8cgRDydnG2b5dcN1LCLw4aM-AUMAvJc1W39mTkNWlQ,614
|
|
67
|
-
hardpy-0.
|
|
68
|
-
hardpy-0.
|
|
69
|
-
hardpy-0.
|
|
70
|
-
hardpy-0.
|
|
71
|
-
hardpy-0.
|
|
67
|
+
hardpy-0.3.0.dist-info/METADATA,sha256=lWSZtZSy7b_PISotSgTw3lyM1X2uJPj0dVmOfI2A9N8,4096
|
|
68
|
+
hardpy-0.3.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
|
69
|
+
hardpy-0.3.0.dist-info/entry_points.txt,sha256=q73g5GfznSUpjkayi0SV4uaAtrf7D-7rmDoWoEZmZe0,120
|
|
70
|
+
hardpy-0.3.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
71
|
+
hardpy-0.3.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|