cooptools 1.77__tar.gz → 1.78__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.
- {cooptools-1.77 → cooptools-1.78}/PKG-INFO +1 -1
- cooptools-1.78/cooptools/expertise/__init__.py +6 -0
- cooptools-1.78/cooptools/expertise/expertise_state.py +58 -0
- cooptools-1.78/cooptools/persistence/__init__.py +36 -0
- cooptools-1.78/cooptools/persistence/enum_state.py +81 -0
- cooptools-1.78/cooptools/persistence/stateful.py +148 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/PKG-INFO +1 -1
- {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/SOURCES.txt +6 -0
- {cooptools-1.77 → cooptools-1.78}/setup.py +1 -1
- cooptools-1.78/tests/test_expertise_state.py +120 -0
- cooptools-1.78/tests/test_persistence.py +176 -0
- cooptools-1.77/cooptools/expertise/__init__.py +0 -2
- {cooptools-1.77 → cooptools-1.78}/MANIFEST.in +0 -0
- {cooptools-1.77 → cooptools-1.78}/README.md +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/anchor.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/asyncable.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/cli/CliMenu.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/cli/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/cli/fileContentReturn.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/cnxn_info.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/colors.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/commandDesignPattern/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/commandDesignPattern/commandController.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/commandDesignPattern/exceptions.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/common.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/config.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/coopDataclass.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/coopEnum.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/coopthreading.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/currency.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dataRefresher/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dataRefresher/dataHub.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dataRefresher/dataRefresher.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/dataProcessor.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/dataStoreProtocol.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/dbConnectionURI.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/inMemoryDataStore.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/date_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/decay.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/decor.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/dictPolicies.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/exceptions.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/expertise/expertiseArgs.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/expertise/expertiseSchedules.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/finance/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/finance/fund_projection.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/finance/fund_projection_renderer.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/finance/futureValueProjector.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/finance/growth_projections.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/finance/reports.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/finance/utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/circle_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/common.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/curve_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/curves.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/line_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/polygon_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/rect_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/triangle_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/vector_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/astar_results_viewer.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/draw.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/flow_field.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/graph.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/graph_dcs.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/graph_definitions.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/jsonIO.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/metrics.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/loggingHelpers.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/marchingSquares.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/materialHandling/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/materialHandling/cli.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/materialHandling/dcs.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/materialHandling/selectionCriteria.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/matrixManipulation.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/os_manip.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pandasHelpers.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/catmullRomFollower.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/demo/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/demo/server.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/kinematicGoalSeeker.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/motion_profile_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/orbit_fuzzer.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/path_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/straightLineFollower.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/trajectory_provider.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/waypoint.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/waypointFollower.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/physics/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/physics/kinematic.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/physics/kinematic_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/physics/movement.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/physics/trajectory.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/plotting.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/printing.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/protocols.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/qualifiers/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/qualifiers/cli.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/qualifiers/qualifier.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/randoms.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/register.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/reservation/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/reservation/dcs.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/reservation/enums.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/reservation/reservationmanager.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/retry.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/gridState.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/grid_base.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/hexGrid.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/rectGrid.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/hex_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/sect_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/sectorTree/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/sectorTree/sectorTree.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/activityReport.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/controlChart/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/controlChart/controlChart.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/controlChart/plotting.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/oee/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/oee/oeeHub.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/taskProcessing/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/taskProcessing/dcs.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/taskProcessing/taskProcessor.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/timeTracker/__init__.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/timeTracker/decay.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/timeTracker/timeTracker.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/timeWindow.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/toggles.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/transform.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/trends.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/typeProviders.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/typevalidation.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools/version.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/dependency_links.txt +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/not-zip-safe +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/requires.txt +0 -0
- {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/top_level.txt +0 -0
- {cooptools-1.77 → cooptools-1.78}/requirements.txt +0 -0
- {cooptools-1.77 → cooptools-1.78}/setup.cfg +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_CliAtomicUserInteraction.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_asyncable.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_catmullRomFollower.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_cdp_bugs.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_commandDesignPattern.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_common.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_config.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_coopEnum.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_currency.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_curves.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_date_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_flow_field.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_flow_field_perf.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_fund_projection.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_graph_node_classification.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_kinematicGoalSeeker.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_kinematic_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_os_manip.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_pandas.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_path_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_printing.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_qualifiers.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_sectors.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_sectors_hex.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_sectors_sectorTree.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_statistics.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_straightLineFollower.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_target_seek.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_tasks.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_toggles.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_trajectory.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_trends.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_typevalidation.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_vector_utils.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_version.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/test_waypointFollower.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/tests_graph.py +0 -0
- {cooptools-1.77 → cooptools-1.78}/tests/tests_gridsystem.py +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Expertise across the JSON boundary.
|
|
2
|
+
|
|
3
|
+
Beside the calculator it serializes rather than in a persistence package, because
|
|
4
|
+
what these two functions know about is *expertise* -- which accumulator holds the
|
|
5
|
+
truth and which views are derived -- and only incidentally that the destination is
|
|
6
|
+
JSON. Grouped by mechanism they would sit next to enum conversion, which they have
|
|
7
|
+
nothing in common with; grouped by subject they sit next to the thing that breaks
|
|
8
|
+
them when it changes.
|
|
9
|
+
|
|
10
|
+
Functions over `ExpertiseCalculator` rather than methods on it, so that reading
|
|
11
|
+
and writing its state is available without widening the calculator's own API.
|
|
12
|
+
|
|
13
|
+
Deliberately asymmetric, and worth knowing why:
|
|
14
|
+
|
|
15
|
+
- Reading has to reach `_expertise_args`, because the calculator exposes only
|
|
16
|
+
derived views (`PercExpert`, `CurrentTimeReductionPerc`) and neither can be
|
|
17
|
+
inverted back into the accumulator that produced it.
|
|
18
|
+
- Writing does not, because `schedule` is public and the increments are public,
|
|
19
|
+
so a fresh calculator can be built and wound forward. That is also what makes
|
|
20
|
+
applying twice give the same answer as applying once -- incrementing an
|
|
21
|
+
existing calculator would accumulate instead of replace.
|
|
22
|
+
|
|
23
|
+
The read is the half that would break if the calculator's internals moved, which
|
|
24
|
+
is the argument for these eventually becoming methods on it.
|
|
25
|
+
"""
|
|
26
|
+
import logging
|
|
27
|
+
from typing import Any, Dict
|
|
28
|
+
|
|
29
|
+
from cooptools.expertise.expertiseSchedules import ExpertiseCalculator
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def expertise_to_state(calculator: ExpertiseCalculator) -> Dict[str, Any]:
|
|
35
|
+
"""The accumulators behind an expertise calculator.
|
|
36
|
+
|
|
37
|
+
The schedule is not written -- it is definition, rebuilt from tuning by
|
|
38
|
+
whatever constructs the calculator's owner.
|
|
39
|
+
"""
|
|
40
|
+
args = calculator._expertise_args
|
|
41
|
+
return {'n_runs': args.n_runs,
|
|
42
|
+
'accumulated_s': args.accumulated_s,
|
|
43
|
+
'exp': args.exp}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def expertise_from_state(state: Dict[str, Any],
|
|
47
|
+
calculator: ExpertiseCalculator) -> ExpertiseCalculator:
|
|
48
|
+
"""A calculator on `calculator`'s schedule, wound to the saved accumulators.
|
|
49
|
+
|
|
50
|
+
Returns a new instance rather than mutating: the caller reassigns, which is
|
|
51
|
+
what makes this replace rather than accumulate.
|
|
52
|
+
"""
|
|
53
|
+
restored = ExpertiseCalculator(schedule=calculator.schedule)
|
|
54
|
+
saved = state or {}
|
|
55
|
+
restored.increment_n_runs(saved.get('n_runs', 0))
|
|
56
|
+
restored.increment_s_producting(saved.get('accumulated_s', 0))
|
|
57
|
+
restored.increment_exp(saved.get('exp', 0))
|
|
58
|
+
return restored
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Handing an object's state out and taking it back, without a base class.
|
|
2
|
+
|
|
3
|
+
`Stateful` is a structural `typing.Protocol`, so a class conforms by having
|
|
4
|
+
`to_state`/`apply_state` and importing nothing to do it. That is what lets a type
|
|
5
|
+
in one package be persisted by another with no dependency edge pointing back --
|
|
6
|
+
and it is why this belongs in a shared library rather than in whichever consumer
|
|
7
|
+
happened to need it first.
|
|
8
|
+
|
|
9
|
+
Alongside it is the conversion every state dict of any size ends up needing:
|
|
10
|
+
enums as dict keys, because JSON keys must be strings.
|
|
11
|
+
|
|
12
|
+
Conversions for a *particular* type live with that type instead --
|
|
13
|
+
`cooptools.expertise.expertise_state` is the worked example. What those functions
|
|
14
|
+
know is which of an expertise calculator's fields holds the truth and which are
|
|
15
|
+
derived; that they happen to end up as JSON is incidental, and grouping them here
|
|
16
|
+
would put them beside enum conversion, which they have nothing in common with.
|
|
17
|
+
|
|
18
|
+
What is deliberately *not* here: anything about where state is written. A file
|
|
19
|
+
format, a directory of saves, a version envelope around a whole document -- those
|
|
20
|
+
are decisions about an application, not about an object handing over its state.
|
|
21
|
+
"""
|
|
22
|
+
from cooptools.persistence.stateful import (
|
|
23
|
+
StateDict,
|
|
24
|
+
StateError,
|
|
25
|
+
StateVersionError,
|
|
26
|
+
Stateful,
|
|
27
|
+
VERSION_KEY,
|
|
28
|
+
check_version,
|
|
29
|
+
version_of,
|
|
30
|
+
)
|
|
31
|
+
from cooptools.persistence.enum_state import (
|
|
32
|
+
enum_from_state,
|
|
33
|
+
enum_keyed_from_state,
|
|
34
|
+
enum_keyed_to_state,
|
|
35
|
+
enum_to_state,
|
|
36
|
+
)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Enums across the JSON boundary.
|
|
2
|
+
|
|
3
|
+
Most state blocks of any size carry at least one enum-keyed
|
|
4
|
+
quantity map -- what a stockpile holds, what a villager carries, what a recipe
|
|
5
|
+
costs -- and JSON has no enum and no non-string key. Left to each call site, that
|
|
6
|
+
translation gets written seven ways: by `.value` in one place and `.name` in
|
|
7
|
+
another, crashing on an unknown member here and silently dropping it there.
|
|
8
|
+
|
|
9
|
+
**Names, not values.** A member's `name` is the thing the rest of the codebase
|
|
10
|
+
already treats as its stable identity (log lines, config keys, file keys). Its
|
|
11
|
+
`value` is frequently `auto()`, which renumbers the moment somebody inserts a
|
|
12
|
+
member -- so a file written by value would read back as a *different* resource
|
|
13
|
+
after an unrelated edit, with nothing to report it.
|
|
14
|
+
"""
|
|
15
|
+
import logging
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Dict, Mapping, Optional, Type, TypeVar
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
E = TypeVar('E', bound=Enum)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def enum_to_state(value: Optional[Enum]) -> Optional[str]:
|
|
25
|
+
"""One enum member as its name, passing None through."""
|
|
26
|
+
return value.name if value is not None else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def enum_from_state(name: Optional[str], enum_cls: Type[E], *,
|
|
30
|
+
owner: str = None) -> Optional[E]:
|
|
31
|
+
"""One enum member back from its name.
|
|
32
|
+
|
|
33
|
+
An unknown name yields None with a warning rather than raising. A member
|
|
34
|
+
removed from the code between the write and the read is a real, expected event
|
|
35
|
+
during development, and it should cost the field rather than the file --
|
|
36
|
+
every caller already handles None for these (a region with no building, a
|
|
37
|
+
site with no target tier).
|
|
38
|
+
"""
|
|
39
|
+
if name is None:
|
|
40
|
+
return None
|
|
41
|
+
try:
|
|
42
|
+
return enum_cls[name]
|
|
43
|
+
except KeyError:
|
|
44
|
+
logger.warning(f"{owner or enum_cls.__name__}: '{name}' is not a member of "
|
|
45
|
+
f"{enum_cls.__name__} any more -- dropped")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def enum_keyed_to_state(quantities: Optional[Mapping[Enum, float]]) -> Dict[str, float]:
|
|
50
|
+
"""An enum-keyed quantity map as a name-keyed one.
|
|
51
|
+
|
|
52
|
+
Written in the enum's own declaration order rather than the mapping's, so two
|
|
53
|
+
writes of the same state are byte-identical -- which is what lets a round-trip
|
|
54
|
+
test compare dicts, and lets two files diff usefully.
|
|
55
|
+
"""
|
|
56
|
+
if not quantities:
|
|
57
|
+
return {}
|
|
58
|
+
members = sorted(quantities.keys(), key=lambda member: list(type(member)).index(member))
|
|
59
|
+
return {member.name: quantities[member] for member in members}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def enum_keyed_from_state(quantities: Optional[Mapping[str, float]],
|
|
63
|
+
enum_cls: Type[E], *,
|
|
64
|
+
owner: str = None) -> Dict[E, float]:
|
|
65
|
+
"""A name-keyed quantity map back to an enum-keyed one.
|
|
66
|
+
|
|
67
|
+
Unknown names are dropped with a warning, for the reason `enum_from_state`
|
|
68
|
+
gives -- but here the loss is quantified in the log, since goods vanishing
|
|
69
|
+
from a stockpile is the kind of thing that otherwise gets noticed as a
|
|
70
|
+
balance mystery three sessions later.
|
|
71
|
+
"""
|
|
72
|
+
if not quantities:
|
|
73
|
+
return {}
|
|
74
|
+
result: Dict[E, float] = {}
|
|
75
|
+
for name, qty in quantities.items():
|
|
76
|
+
try:
|
|
77
|
+
result[enum_cls[name]] = qty
|
|
78
|
+
except KeyError:
|
|
79
|
+
logger.warning(f"{owner or enum_cls.__name__}: dropping {qty:g} of '{name}', "
|
|
80
|
+
f"which is not a member of {enum_cls.__name__} any more")
|
|
81
|
+
return result
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""The contract every persistable thing implements: hand out your state, and
|
|
2
|
+
take it back.
|
|
3
|
+
|
|
4
|
+
Deliberately a `typing.Protocol` rather than a base class, because conformance is
|
|
5
|
+
*structural*: a class satisfies this by having the two methods, without importing
|
|
6
|
+
anything from here. That is what lets a class in one package be persisted by
|
|
7
|
+
another with no dependency edge pointing back -- which is the whole reason this
|
|
8
|
+
is a library type rather than one belonging to whoever is doing the persisting.
|
|
9
|
+
|
|
10
|
+
## State is not definition
|
|
11
|
+
|
|
12
|
+
The split this protocol exists to enforce: **state is what use changed;
|
|
13
|
+
definition is what built the object.** A production station is built from its
|
|
14
|
+
recipe -- inputs, outputs, a timer callback, a schedule -- and *holds* the
|
|
15
|
+
contents of its stores and how far its current run has got. Only the second half
|
|
16
|
+
belongs in a state dict. The first half is reconstructed by whatever built the
|
|
17
|
+
object the first time, from the same source it used then.
|
|
18
|
+
|
|
19
|
+
Getting this line wrong in either direction is expensive. Persisting definition
|
|
20
|
+
means a restored object silently pins yesterday's configuration and stops
|
|
21
|
+
tracking the file it should be reading. Persisting too little means the object
|
|
22
|
+
comes back subtly wrong, usually in a way nothing notices for a while.
|
|
23
|
+
|
|
24
|
+
## Why `apply_state` mutates instead of `from_state` constructing
|
|
25
|
+
|
|
26
|
+
Almost nothing worth persisting can be rebuilt from a dict alone. The objects
|
|
27
|
+
that carry interesting state are usually constructed with injected collaborators
|
|
28
|
+
-- providers, callbacks, the graph they read -- none of which survive a round trip
|
|
29
|
+
through JSON. A `from_state` classmethod would force every one of them to grow a
|
|
30
|
+
second construction path whose only caller is the loader.
|
|
31
|
+
|
|
32
|
+
`apply_state` matches how a restore actually goes instead: the object graph is
|
|
33
|
+
rebuilt the ordinary way, so everything exists with its collaborators already
|
|
34
|
+
wired, and the saved state is then poured into objects that are otherwise ready to
|
|
35
|
+
run.
|
|
36
|
+
|
|
37
|
+
A leaf that genuinely owns nothing may additionally offer a `from_state`
|
|
38
|
+
classmethod as a convenience. The protocol does not ask for one.
|
|
39
|
+
|
|
40
|
+
## Versioning
|
|
41
|
+
|
|
42
|
+
Each block carries its own `STATE_VERSION`, stamped under `VERSION_KEY`, and each
|
|
43
|
+
`apply_state` is responsible for reading older shapes of its own block. Versioning
|
|
44
|
+
per class rather than once for the whole file is what stops a new field on one
|
|
45
|
+
class invalidating every file in existence -- and it keeps a migration next to the
|
|
46
|
+
invariant it is migrating, which is the same reason the state lives on the object
|
|
47
|
+
rather than in a central writer.
|
|
48
|
+
"""
|
|
49
|
+
import logging
|
|
50
|
+
from typing import Any, ClassVar, Dict, Protocol, runtime_checkable
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
# A JSON-safe mapping: dicts, lists, strings, numbers, bools and None, and
|
|
55
|
+
# nothing else. Enum keys are written as `.name` (see enum_state.py), and any
|
|
56
|
+
# identity-bearing type -- a uuid, a graph Node -- is written as whatever stable
|
|
57
|
+
# name addresses it, never as the object.
|
|
58
|
+
StateDict = Dict[str, Any]
|
|
59
|
+
|
|
60
|
+
# Where a block's own version lives. Short because it appears once per block, and
|
|
61
|
+
# a file of any size holds a great many blocks.
|
|
62
|
+
VERSION_KEY = "v"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class StateError(ValueError):
|
|
66
|
+
"""A state dict could not be read."""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class StateVersionError(StateError):
|
|
70
|
+
"""A state dict is a version this build does not understand."""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@runtime_checkable
|
|
74
|
+
class Stateful(Protocol):
|
|
75
|
+
"""Hands out its own state, and takes it back.
|
|
76
|
+
|
|
77
|
+
Two things to know about checking conformance against this at runtime:
|
|
78
|
+
|
|
79
|
+
- **`isinstance` works and covers all three members**, `STATE_VERSION`
|
|
80
|
+
included, so a class that has the methods but never declared a version is
|
|
81
|
+
correctly refused.
|
|
82
|
+
- **`issubclass` raises `TypeError`** on every Python version, because this
|
|
83
|
+
protocol has a non-method member. Check an instance, never a class.
|
|
84
|
+
|
|
85
|
+
And what `isinstance` does *not* buy: `runtime_checkable` compares member
|
|
86
|
+
presence, never signatures. It catches a conformer that dropped or renamed a
|
|
87
|
+
member -- the realistic failure for a class in another package that never
|
|
88
|
+
imports this -- and says nothing about one that changed an argument.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
STATE_VERSION: ClassVar[int]
|
|
92
|
+
|
|
93
|
+
def to_state(self) -> StateDict:
|
|
94
|
+
"""This object's play-state, as JSON-safe primitives.
|
|
95
|
+
|
|
96
|
+
Must include `VERSION_KEY`. Must not include anything reconstructed by
|
|
97
|
+
whatever builds this object -- see the module docstring on state vs.
|
|
98
|
+
definition.
|
|
99
|
+
"""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
def apply_state(self, state: StateDict) -> None:
|
|
103
|
+
"""Adopts `state`, replacing whatever this object currently holds.
|
|
104
|
+
|
|
105
|
+
Called on an object that is fully constructed and wired to its
|
|
106
|
+
collaborators. Implementations replace rather than merge: applying a state
|
|
107
|
+
twice must leave the same result as applying it once, and a field absent
|
|
108
|
+
from `state` takes its documented default rather than whatever happened to
|
|
109
|
+
be there.
|
|
110
|
+
"""
|
|
111
|
+
...
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def version_of(state: StateDict) -> int:
|
|
115
|
+
"""The version stamped on a state block.
|
|
116
|
+
|
|
117
|
+
Raises rather than defaulting: an unstamped block is either corrupt or predates
|
|
118
|
+
versioning, and both are cases where guessing a version reads the rest of the
|
|
119
|
+
block under rules it was not written to.
|
|
120
|
+
"""
|
|
121
|
+
if VERSION_KEY not in state:
|
|
122
|
+
raise StateVersionError(
|
|
123
|
+
f"State block carries no '{VERSION_KEY}' -- it is corrupt, or was written "
|
|
124
|
+
f"before this block was versioned")
|
|
125
|
+
version = state[VERSION_KEY]
|
|
126
|
+
if not isinstance(version, int) or isinstance(version, bool):
|
|
127
|
+
raise StateVersionError(
|
|
128
|
+
f"State block's '{VERSION_KEY}' is {version!r}, which is not a version number")
|
|
129
|
+
return version
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def check_version(state: StateDict, *, owner: str, supported: int) -> int:
|
|
133
|
+
"""Returns the block's version, refusing one this build cannot read.
|
|
134
|
+
|
|
135
|
+
`owner` names the class in the error, because a load failure often reaches a
|
|
136
|
+
user as a single line, and "which part of this file is too new" is the whole of
|
|
137
|
+
what they can act on.
|
|
138
|
+
|
|
139
|
+
A version *below* `supported` is returned rather than refused -- that is the
|
|
140
|
+
case `apply_state` migrates, and this function has no way to know whether it
|
|
141
|
+
can.
|
|
142
|
+
"""
|
|
143
|
+
version = version_of(state)
|
|
144
|
+
if version > supported:
|
|
145
|
+
raise StateVersionError(
|
|
146
|
+
f"{owner} state is version {version}, but this build understands up to "
|
|
147
|
+
f"{supported} -- it was written by a newer build")
|
|
148
|
+
return version
|
|
@@ -61,6 +61,7 @@ cooptools/dataStore/inMemoryDataStore.py
|
|
|
61
61
|
cooptools/expertise/__init__.py
|
|
62
62
|
cooptools/expertise/expertiseArgs.py
|
|
63
63
|
cooptools/expertise/expertiseSchedules.py
|
|
64
|
+
cooptools/expertise/expertise_state.py
|
|
64
65
|
cooptools/finance/__init__.py
|
|
65
66
|
cooptools/finance/dicounted_cashflow_analysis.py
|
|
66
67
|
cooptools/finance/fund_projection.py
|
|
@@ -114,6 +115,9 @@ cooptools/pathing/waypoint.py
|
|
|
114
115
|
cooptools/pathing/waypointFollower.py
|
|
115
116
|
cooptools/pathing/demo/__init__.py
|
|
116
117
|
cooptools/pathing/demo/server.py
|
|
118
|
+
cooptools/persistence/__init__.py
|
|
119
|
+
cooptools/persistence/enum_state.py
|
|
120
|
+
cooptools/persistence/stateful.py
|
|
117
121
|
cooptools/physics/__init__.py
|
|
118
122
|
cooptools/physics/kinematic.py
|
|
119
123
|
cooptools/physics/kinematic_utils.py
|
|
@@ -160,6 +164,7 @@ tests/test_coopEnum.py
|
|
|
160
164
|
tests/test_currency.py
|
|
161
165
|
tests/test_curves.py
|
|
162
166
|
tests/test_date_utils.py
|
|
167
|
+
tests/test_expertise_state.py
|
|
163
168
|
tests/test_flow_field.py
|
|
164
169
|
tests/test_flow_field_perf.py
|
|
165
170
|
tests/test_fund_projection.py
|
|
@@ -169,6 +174,7 @@ tests/test_kinematic_utils.py
|
|
|
169
174
|
tests/test_os_manip.py
|
|
170
175
|
tests/test_pandas.py
|
|
171
176
|
tests/test_path_utils.py
|
|
177
|
+
tests/test_persistence.py
|
|
172
178
|
tests/test_printing.py
|
|
173
179
|
tests/test_qualifiers.py
|
|
174
180
|
tests/test_sectors.py
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Coverage for expertise across the JSON boundary.
|
|
2
|
+
|
|
3
|
+
The pair was moved here from a consumer with no tests of its own, so these are
|
|
4
|
+
new. What matters about it is not the round trip -- three integers survive a dict,
|
|
5
|
+
which would be surprising if it failed -- but the two properties that make it
|
|
6
|
+
usable as *state*:
|
|
7
|
+
|
|
8
|
+
* **What is written is the accumulators, not the schedule.** The schedule is
|
|
9
|
+
definition, rebuilt from tuning by whatever constructs the calculator's owner.
|
|
10
|
+
Writing it would pin yesterday's tuning into every file.
|
|
11
|
+
* **Applying twice gives the same answer as applying once.** The functions are
|
|
12
|
+
deliberately asymmetric for this: reading reaches the private args because the
|
|
13
|
+
calculator exposes only derived views that cannot be inverted, while writing
|
|
14
|
+
builds a *fresh* calculator and winds it forward. Incrementing an existing one
|
|
15
|
+
would accumulate, and a state applied twice would report double the
|
|
16
|
+
experience.
|
|
17
|
+
|
|
18
|
+
Runs with:
|
|
19
|
+
python -m unittest tests.test_expertise_state
|
|
20
|
+
"""
|
|
21
|
+
import unittest
|
|
22
|
+
|
|
23
|
+
from cooptools.expertise import expertise_from_state, expertise_to_state
|
|
24
|
+
from cooptools.expertise.expertiseSchedules import (ByRunsExpertiseSchedule,
|
|
25
|
+
ExpertiseCalculator)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _calculator(runs_until_expert: int = 8,
|
|
29
|
+
max_time_reduction_perc: float = 0.3) -> ExpertiseCalculator:
|
|
30
|
+
return ExpertiseCalculator(schedule=ByRunsExpertiseSchedule(
|
|
31
|
+
runs_until_expert=runs_until_expert,
|
|
32
|
+
max_time_reduction_perc=max_time_reduction_perc))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ExpertiseStateTests(unittest.TestCase):
|
|
36
|
+
def test__a_fresh_calculator_writes_zeroes(self):
|
|
37
|
+
self.assertEqual({'n_runs': 0, 'accumulated_s': 0, 'exp': 0},
|
|
38
|
+
expertise_to_state(_calculator()))
|
|
39
|
+
|
|
40
|
+
def test__the_accumulators_round_trip(self):
|
|
41
|
+
source = _calculator()
|
|
42
|
+
source.increment_n_runs(5)
|
|
43
|
+
source.increment_s_producting(12.5)
|
|
44
|
+
source.increment_exp(3)
|
|
45
|
+
|
|
46
|
+
restored = expertise_from_state(expertise_to_state(source), _calculator())
|
|
47
|
+
self.assertEqual(expertise_to_state(source), expertise_to_state(restored))
|
|
48
|
+
|
|
49
|
+
def test__the_derived_views_come_back_with_them(self):
|
|
50
|
+
"""The accumulators are what is stored precisely because the views follow
|
|
51
|
+
from them -- so the views are the check that the right thing was stored."""
|
|
52
|
+
source = _calculator()
|
|
53
|
+
source.increment_n_runs(4)
|
|
54
|
+
|
|
55
|
+
restored = expertise_from_state(expertise_to_state(source), _calculator())
|
|
56
|
+
self.assertEqual(source.PercExpert, restored.PercExpert)
|
|
57
|
+
self.assertEqual(source.CurrentTimeReductionPerc,
|
|
58
|
+
restored.CurrentTimeReductionPerc)
|
|
59
|
+
|
|
60
|
+
def test__the_schedule_is_not_written(self):
|
|
61
|
+
"""It is definition. Writing it would pin the tuning a file was made
|
|
62
|
+
under into every later read of that file."""
|
|
63
|
+
state = expertise_to_state(_calculator(runs_until_expert=8))
|
|
64
|
+
self.assertEqual({'n_runs', 'accumulated_s', 'exp'}, set(state))
|
|
65
|
+
|
|
66
|
+
def test__the_schedule_comes_from_the_target_not_the_state(self):
|
|
67
|
+
"""So retuning reaches an object restored from an older file, rather than
|
|
68
|
+
the file holding it back."""
|
|
69
|
+
source = _calculator(runs_until_expert=8)
|
|
70
|
+
source.increment_n_runs(4)
|
|
71
|
+
|
|
72
|
+
retuned = _calculator(runs_until_expert=4)
|
|
73
|
+
restored = expertise_from_state(expertise_to_state(source), retuned)
|
|
74
|
+
|
|
75
|
+
self.assertEqual(retuned.schedule, restored.schedule)
|
|
76
|
+
# Four runs is half-expert on the old schedule and fully expert on the
|
|
77
|
+
# new one, from the same stored accumulators.
|
|
78
|
+
self.assertGreater(restored.PercExpert, source.PercExpert)
|
|
79
|
+
|
|
80
|
+
def test__applying_twice_equals_applying_once(self):
|
|
81
|
+
"""The asymmetry exists for this. A version that incremented an existing
|
|
82
|
+
calculator would report double the experience on a second apply."""
|
|
83
|
+
source = _calculator()
|
|
84
|
+
source.increment_n_runs(6)
|
|
85
|
+
source.increment_s_producting(9.0)
|
|
86
|
+
state = expertise_to_state(source)
|
|
87
|
+
|
|
88
|
+
once = expertise_from_state(state, _calculator())
|
|
89
|
+
twice = expertise_from_state(state, once)
|
|
90
|
+
self.assertEqual(expertise_to_state(once), expertise_to_state(twice))
|
|
91
|
+
|
|
92
|
+
def test__applying_does_not_disturb_the_target(self):
|
|
93
|
+
"""It returns a new calculator, so the caller reassigns -- which is what
|
|
94
|
+
makes it replace rather than accumulate."""
|
|
95
|
+
target = _calculator()
|
|
96
|
+
target.increment_n_runs(2)
|
|
97
|
+
|
|
98
|
+
source = _calculator()
|
|
99
|
+
source.increment_n_runs(7)
|
|
100
|
+
expertise_from_state(expertise_to_state(source), target)
|
|
101
|
+
|
|
102
|
+
self.assertEqual(2, expertise_to_state(target)['n_runs'])
|
|
103
|
+
|
|
104
|
+
def test__an_absent_state_restores_a_fresh_calculator(self):
|
|
105
|
+
"""A file written before this was recorded should leave the object at its
|
|
106
|
+
documented default rather than failing to load."""
|
|
107
|
+
for empty in (None, {}):
|
|
108
|
+
with self.subTest(state=empty):
|
|
109
|
+
restored = expertise_from_state(empty, _calculator())
|
|
110
|
+
self.assertEqual({'n_runs': 0, 'accumulated_s': 0, 'exp': 0},
|
|
111
|
+
expertise_to_state(restored))
|
|
112
|
+
|
|
113
|
+
def test__a_partial_state_takes_defaults_for_what_is_missing(self):
|
|
114
|
+
restored = expertise_from_state({'n_runs': 3}, _calculator())
|
|
115
|
+
self.assertEqual({'n_runs': 3, 'accumulated_s': 0, 'exp': 0},
|
|
116
|
+
expertise_to_state(restored))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
if __name__ == '__main__':
|
|
120
|
+
unittest.main()
|