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.
Files changed (198) hide show
  1. {cooptools-1.77 → cooptools-1.78}/PKG-INFO +1 -1
  2. cooptools-1.78/cooptools/expertise/__init__.py +6 -0
  3. cooptools-1.78/cooptools/expertise/expertise_state.py +58 -0
  4. cooptools-1.78/cooptools/persistence/__init__.py +36 -0
  5. cooptools-1.78/cooptools/persistence/enum_state.py +81 -0
  6. cooptools-1.78/cooptools/persistence/stateful.py +148 -0
  7. {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/PKG-INFO +1 -1
  8. {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/SOURCES.txt +6 -0
  9. {cooptools-1.77 → cooptools-1.78}/setup.py +1 -1
  10. cooptools-1.78/tests/test_expertise_state.py +120 -0
  11. cooptools-1.78/tests/test_persistence.py +176 -0
  12. cooptools-1.77/cooptools/expertise/__init__.py +0 -2
  13. {cooptools-1.77 → cooptools-1.78}/MANIFEST.in +0 -0
  14. {cooptools-1.77 → cooptools-1.78}/README.md +0 -0
  15. {cooptools-1.77 → cooptools-1.78}/cooptools/__init__.py +0 -0
  16. {cooptools-1.77 → cooptools-1.78}/cooptools/anchor.py +0 -0
  17. {cooptools-1.77 → cooptools-1.78}/cooptools/asyncable.py +0 -0
  18. {cooptools-1.77 → cooptools-1.78}/cooptools/cli/CliAtomicUserInteraction.py +0 -0
  19. {cooptools-1.77 → cooptools-1.78}/cooptools/cli/CliMenu.py +0 -0
  20. {cooptools-1.77 → cooptools-1.78}/cooptools/cli/__init__.py +0 -0
  21. {cooptools-1.77 → cooptools-1.78}/cooptools/cli/fileContentReturn.py +0 -0
  22. {cooptools-1.77 → cooptools-1.78}/cooptools/cnxn_info.py +0 -0
  23. {cooptools-1.77 → cooptools-1.78}/cooptools/colors.py +0 -0
  24. {cooptools-1.77 → cooptools-1.78}/cooptools/commandDesignPattern/__init__.py +0 -0
  25. {cooptools-1.77 → cooptools-1.78}/cooptools/commandDesignPattern/commandController.py +0 -0
  26. {cooptools-1.77 → cooptools-1.78}/cooptools/commandDesignPattern/commandProtocol.py +0 -0
  27. {cooptools-1.77 → cooptools-1.78}/cooptools/commandDesignPattern/exceptions.py +0 -0
  28. {cooptools-1.77 → cooptools-1.78}/cooptools/common.py +0 -0
  29. {cooptools-1.77 → cooptools-1.78}/cooptools/config.py +0 -0
  30. {cooptools-1.77 → cooptools-1.78}/cooptools/coopDataclass.py +0 -0
  31. {cooptools-1.77 → cooptools-1.78}/cooptools/coopEnum.py +0 -0
  32. {cooptools-1.77 → cooptools-1.78}/cooptools/coopthreading.py +0 -0
  33. {cooptools-1.77 → cooptools-1.78}/cooptools/currency.py +0 -0
  34. {cooptools-1.77 → cooptools-1.78}/cooptools/dataRefresher/__init__.py +0 -0
  35. {cooptools-1.77 → cooptools-1.78}/cooptools/dataRefresher/dataHub.py +0 -0
  36. {cooptools-1.77 → cooptools-1.78}/cooptools/dataRefresher/dataRefresher.py +0 -0
  37. {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/__init__.py +0 -0
  38. {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/dataProcessor.py +0 -0
  39. {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/dataStoreProtocol.py +0 -0
  40. {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/dbConnectionURI.py +0 -0
  41. {cooptools-1.77 → cooptools-1.78}/cooptools/dataStore/inMemoryDataStore.py +0 -0
  42. {cooptools-1.77 → cooptools-1.78}/cooptools/date_utils.py +0 -0
  43. {cooptools-1.77 → cooptools-1.78}/cooptools/decay.py +0 -0
  44. {cooptools-1.77 → cooptools-1.78}/cooptools/decor.py +0 -0
  45. {cooptools-1.77 → cooptools-1.78}/cooptools/dictPolicies.py +0 -0
  46. {cooptools-1.77 → cooptools-1.78}/cooptools/exceptions.py +0 -0
  47. {cooptools-1.77 → cooptools-1.78}/cooptools/expertise/expertiseArgs.py +0 -0
  48. {cooptools-1.77 → cooptools-1.78}/cooptools/expertise/expertiseSchedules.py +0 -0
  49. {cooptools-1.77 → cooptools-1.78}/cooptools/finance/__init__.py +0 -0
  50. {cooptools-1.77 → cooptools-1.78}/cooptools/finance/dicounted_cashflow_analysis.py +0 -0
  51. {cooptools-1.77 → cooptools-1.78}/cooptools/finance/fund_projection.py +0 -0
  52. {cooptools-1.77 → cooptools-1.78}/cooptools/finance/fund_projection_renderer.py +0 -0
  53. {cooptools-1.77 → cooptools-1.78}/cooptools/finance/futureValueProjector.py +0 -0
  54. {cooptools-1.77 → cooptools-1.78}/cooptools/finance/growth_projections.py +0 -0
  55. {cooptools-1.77 → cooptools-1.78}/cooptools/finance/reports.py +0 -0
  56. {cooptools-1.77 → cooptools-1.78}/cooptools/finance/utils.py +0 -0
  57. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/__init__.py +0 -0
  58. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/circle_utils.py +0 -0
  59. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/common.py +0 -0
  60. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/curve_utils.py +0 -0
  61. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/curves.py +0 -0
  62. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/line_utils.py +0 -0
  63. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/polygon_utils.py +0 -0
  64. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/rect_utils.py +0 -0
  65. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/triangle_utils.py +0 -0
  66. {cooptools-1.77 → cooptools-1.78}/cooptools/geometry_utils/vector_utils.py +0 -0
  67. {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/__init__.py +0 -0
  68. {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/astar_results_viewer.py +0 -0
  69. {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/draw.py +0 -0
  70. {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/flow_field.py +0 -0
  71. {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/graph.py +0 -0
  72. {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/graph_dcs.py +0 -0
  73. {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/graph_definitions.py +0 -0
  74. {cooptools-1.77 → cooptools-1.78}/cooptools/graphs/utils.py +0 -0
  75. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/__init__.py +0 -0
  76. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/actionItemTracker/__init__.py +0 -0
  77. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/actionItemTracker/ai_tracker.py +0 -0
  78. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/actionItemTracker/dcs.py +0 -0
  79. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/discreteEventSimulator/__init__.py +0 -0
  80. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/discreteEventSimulator/discreteEventSimulator.py +0 -0
  81. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/jsonIO.py +0 -0
  82. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/metrics.py +0 -0
  83. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/triggerActionSystem/__init__.py +0 -0
  84. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/triggerActionSystem/signal.py +0 -0
  85. {cooptools-1.77 → cooptools-1.78}/cooptools/ideas/triggerActionSystem/trigger.py +0 -0
  86. {cooptools-1.77 → cooptools-1.78}/cooptools/loggingHelpers.py +0 -0
  87. {cooptools-1.77 → cooptools-1.78}/cooptools/marchingSquares.py +0 -0
  88. {cooptools-1.77 → cooptools-1.78}/cooptools/materialHandling/__init__.py +0 -0
  89. {cooptools-1.77 → cooptools-1.78}/cooptools/materialHandling/cli.py +0 -0
  90. {cooptools-1.77 → cooptools-1.78}/cooptools/materialHandling/dcs.py +0 -0
  91. {cooptools-1.77 → cooptools-1.78}/cooptools/materialHandling/selectionCriteria.py +0 -0
  92. {cooptools-1.77 → cooptools-1.78}/cooptools/matrixManipulation.py +0 -0
  93. {cooptools-1.77 → cooptools-1.78}/cooptools/os_manip.py +0 -0
  94. {cooptools-1.77 → cooptools-1.78}/cooptools/pandasHelpers.py +0 -0
  95. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/__init__.py +0 -0
  96. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/catmullRomFollower.py +0 -0
  97. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/demo/__init__.py +0 -0
  98. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/demo/server.py +0 -0
  99. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/kinematicGoalSeeker.py +0 -0
  100. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/motion_profile_utils.py +0 -0
  101. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/orbit_fuzzer.py +0 -0
  102. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/path_utils.py +0 -0
  103. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/straightLineFollower.py +0 -0
  104. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/trajectory_provider.py +0 -0
  105. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/waypoint.py +0 -0
  106. {cooptools-1.77 → cooptools-1.78}/cooptools/pathing/waypointFollower.py +0 -0
  107. {cooptools-1.77 → cooptools-1.78}/cooptools/physics/__init__.py +0 -0
  108. {cooptools-1.77 → cooptools-1.78}/cooptools/physics/kinematic.py +0 -0
  109. {cooptools-1.77 → cooptools-1.78}/cooptools/physics/kinematic_utils.py +0 -0
  110. {cooptools-1.77 → cooptools-1.78}/cooptools/physics/movement.py +0 -0
  111. {cooptools-1.77 → cooptools-1.78}/cooptools/physics/trajectory.py +0 -0
  112. {cooptools-1.77 → cooptools-1.78}/cooptools/plotting.py +0 -0
  113. {cooptools-1.77 → cooptools-1.78}/cooptools/printing.py +0 -0
  114. {cooptools-1.77 → cooptools-1.78}/cooptools/protocols.py +0 -0
  115. {cooptools-1.77 → cooptools-1.78}/cooptools/qualifiers/__init__.py +0 -0
  116. {cooptools-1.77 → cooptools-1.78}/cooptools/qualifiers/cli.py +0 -0
  117. {cooptools-1.77 → cooptools-1.78}/cooptools/qualifiers/qualifier.py +0 -0
  118. {cooptools-1.77 → cooptools-1.78}/cooptools/randoms.py +0 -0
  119. {cooptools-1.77 → cooptools-1.78}/cooptools/register.py +0 -0
  120. {cooptools-1.77 → cooptools-1.78}/cooptools/reservation/__init__.py +0 -0
  121. {cooptools-1.77 → cooptools-1.78}/cooptools/reservation/dcs.py +0 -0
  122. {cooptools-1.77 → cooptools-1.78}/cooptools/reservation/enums.py +0 -0
  123. {cooptools-1.77 → cooptools-1.78}/cooptools/reservation/reservationmanager.py +0 -0
  124. {cooptools-1.77 → cooptools-1.78}/cooptools/retry.py +0 -0
  125. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/__init__.py +0 -0
  126. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/__init__.py +0 -0
  127. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/gridState.py +0 -0
  128. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/grid_base.py +0 -0
  129. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/hexGrid.py +0 -0
  130. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/grids/rectGrid.py +0 -0
  131. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/hex_utils.py +0 -0
  132. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/sect_utils.py +0 -0
  133. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/sectorTree/__init__.py +0 -0
  134. {cooptools-1.77 → cooptools-1.78}/cooptools/sectors/sectorTree/sectorTree.py +0 -0
  135. {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/__init__.py +0 -0
  136. {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/activityReport.py +0 -0
  137. {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/controlChart/__init__.py +0 -0
  138. {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/controlChart/controlChart.py +0 -0
  139. {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/controlChart/plotting.py +0 -0
  140. {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/oee/__init__.py +0 -0
  141. {cooptools-1.77 → cooptools-1.78}/cooptools/statistics/oee/oeeHub.py +0 -0
  142. {cooptools-1.77 → cooptools-1.78}/cooptools/taskProcessing/__init__.py +0 -0
  143. {cooptools-1.77 → cooptools-1.78}/cooptools/taskProcessing/dcs.py +0 -0
  144. {cooptools-1.77 → cooptools-1.78}/cooptools/taskProcessing/taskProcessor.py +0 -0
  145. {cooptools-1.77 → cooptools-1.78}/cooptools/timeTracker/__init__.py +0 -0
  146. {cooptools-1.77 → cooptools-1.78}/cooptools/timeTracker/decay.py +0 -0
  147. {cooptools-1.77 → cooptools-1.78}/cooptools/timeTracker/timeTracker.py +0 -0
  148. {cooptools-1.77 → cooptools-1.78}/cooptools/timeWindow.py +0 -0
  149. {cooptools-1.77 → cooptools-1.78}/cooptools/toggles.py +0 -0
  150. {cooptools-1.77 → cooptools-1.78}/cooptools/transform.py +0 -0
  151. {cooptools-1.77 → cooptools-1.78}/cooptools/trends.py +0 -0
  152. {cooptools-1.77 → cooptools-1.78}/cooptools/typeProviders.py +0 -0
  153. {cooptools-1.77 → cooptools-1.78}/cooptools/typevalidation.py +0 -0
  154. {cooptools-1.77 → cooptools-1.78}/cooptools/version.py +0 -0
  155. {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/dependency_links.txt +0 -0
  156. {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/not-zip-safe +0 -0
  157. {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/requires.txt +0 -0
  158. {cooptools-1.77 → cooptools-1.78}/cooptools.egg-info/top_level.txt +0 -0
  159. {cooptools-1.77 → cooptools-1.78}/requirements.txt +0 -0
  160. {cooptools-1.77 → cooptools-1.78}/setup.cfg +0 -0
  161. {cooptools-1.77 → cooptools-1.78}/tests/test_CliAtomicUserInteraction.py +0 -0
  162. {cooptools-1.77 → cooptools-1.78}/tests/test_asyncable.py +0 -0
  163. {cooptools-1.77 → cooptools-1.78}/tests/test_catmullRomFollower.py +0 -0
  164. {cooptools-1.77 → cooptools-1.78}/tests/test_cdp_bugs.py +0 -0
  165. {cooptools-1.77 → cooptools-1.78}/tests/test_commandDesignPattern.py +0 -0
  166. {cooptools-1.77 → cooptools-1.78}/tests/test_common.py +0 -0
  167. {cooptools-1.77 → cooptools-1.78}/tests/test_config.py +0 -0
  168. {cooptools-1.77 → cooptools-1.78}/tests/test_coopEnum.py +0 -0
  169. {cooptools-1.77 → cooptools-1.78}/tests/test_currency.py +0 -0
  170. {cooptools-1.77 → cooptools-1.78}/tests/test_curves.py +0 -0
  171. {cooptools-1.77 → cooptools-1.78}/tests/test_date_utils.py +0 -0
  172. {cooptools-1.77 → cooptools-1.78}/tests/test_flow_field.py +0 -0
  173. {cooptools-1.77 → cooptools-1.78}/tests/test_flow_field_perf.py +0 -0
  174. {cooptools-1.77 → cooptools-1.78}/tests/test_fund_projection.py +0 -0
  175. {cooptools-1.77 → cooptools-1.78}/tests/test_graph_node_classification.py +0 -0
  176. {cooptools-1.77 → cooptools-1.78}/tests/test_kinematicGoalSeeker.py +0 -0
  177. {cooptools-1.77 → cooptools-1.78}/tests/test_kinematic_utils.py +0 -0
  178. {cooptools-1.77 → cooptools-1.78}/tests/test_os_manip.py +0 -0
  179. {cooptools-1.77 → cooptools-1.78}/tests/test_pandas.py +0 -0
  180. {cooptools-1.77 → cooptools-1.78}/tests/test_path_utils.py +0 -0
  181. {cooptools-1.77 → cooptools-1.78}/tests/test_printing.py +0 -0
  182. {cooptools-1.77 → cooptools-1.78}/tests/test_qualifiers.py +0 -0
  183. {cooptools-1.77 → cooptools-1.78}/tests/test_sectors.py +0 -0
  184. {cooptools-1.77 → cooptools-1.78}/tests/test_sectors_hex.py +0 -0
  185. {cooptools-1.77 → cooptools-1.78}/tests/test_sectors_sectorTree.py +0 -0
  186. {cooptools-1.77 → cooptools-1.78}/tests/test_statistics.py +0 -0
  187. {cooptools-1.77 → cooptools-1.78}/tests/test_straightLineFollower.py +0 -0
  188. {cooptools-1.77 → cooptools-1.78}/tests/test_target_seek.py +0 -0
  189. {cooptools-1.77 → cooptools-1.78}/tests/test_tasks.py +0 -0
  190. {cooptools-1.77 → cooptools-1.78}/tests/test_toggles.py +0 -0
  191. {cooptools-1.77 → cooptools-1.78}/tests/test_trajectory.py +0 -0
  192. {cooptools-1.77 → cooptools-1.78}/tests/test_trends.py +0 -0
  193. {cooptools-1.77 → cooptools-1.78}/tests/test_typevalidation.py +0 -0
  194. {cooptools-1.77 → cooptools-1.78}/tests/test_vector_utils.py +0 -0
  195. {cooptools-1.77 → cooptools-1.78}/tests/test_version.py +0 -0
  196. {cooptools-1.77 → cooptools-1.78}/tests/test_waypointFollower.py +0 -0
  197. {cooptools-1.77 → cooptools-1.78}/tests/tests_graph.py +0 -0
  198. {cooptools-1.77 → cooptools-1.78}/tests/tests_gridsystem.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.77
3
+ Version: 1.78
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -0,0 +1,6 @@
1
+ from cooptools.expertise.expertiseArgs import *
2
+ from cooptools.expertise.expertiseSchedules import *
3
+ from cooptools.expertise.expertise_state import (
4
+ expertise_from_state,
5
+ expertise_to_state,
6
+ )
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cooptools
3
- Version: 1.77
3
+ Version: 1.78
4
4
  Summary: Generic Tooling
5
5
  Home-page: https://github.com/tylertjburns/cooptools
6
6
  Author: tburns
@@ -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
@@ -7,7 +7,7 @@ with open('requirements.txt') as f:
7
7
  requirements = f.read().splitlines()
8
8
 
9
9
  setuptools.setup(name='cooptools',
10
- version='1.77',
10
+ version='1.78',
11
11
  description='Generic Tooling',
12
12
  url='https://github.com/tylertjburns/cooptools',
13
13
  author='tburns',
@@ -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()