zeitreiher-models 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (380) hide show
  1. zeitreiher_models-0.1.0/.gitattributes +9 -0
  2. zeitreiher_models-0.1.0/.github/dependabot.yml +14 -0
  3. zeitreiher_models-0.1.0/.github/workflows/dependabot_automerge.yml +18 -0
  4. zeitreiher_models-0.1.0/.github/workflows/go-lint.yml +28 -0
  5. zeitreiher_models-0.1.0/.github/workflows/go-test.yml +26 -0
  6. zeitreiher_models-0.1.0/.github/workflows/packaging-test.yml +20 -0
  7. zeitreiher_models-0.1.0/.github/workflows/proto.yml +27 -0
  8. zeitreiher_models-0.1.0/.github/workflows/python-coverage.yml +23 -0
  9. zeitreiher_models-0.1.0/.github/workflows/python-formatting.yml +28 -0
  10. zeitreiher_models-0.1.0/.github/workflows/python-lint.yml +35 -0
  11. zeitreiher_models-0.1.0/.github/workflows/python-publish.yml +52 -0
  12. zeitreiher_models-0.1.0/.github/workflows/python-test.yml +24 -0
  13. zeitreiher_models-0.1.0/.gitignore +30 -0
  14. zeitreiher_models-0.1.0/LICENSE +21 -0
  15. zeitreiher_models-0.1.0/PKG-INFO +235 -0
  16. zeitreiher_models-0.1.0/README.md +209 -0
  17. zeitreiher_models-0.1.0/buf.yaml +13 -0
  18. zeitreiher_models-0.1.0/doc.go +24 -0
  19. zeitreiher_models-0.1.0/docs/superpowers/plans/2026-10-27-m1a-models.md +495 -0
  20. zeitreiher_models-0.1.0/domain-specific-terms.txt +5 -0
  21. zeitreiher_models-0.1.0/errors.go +244 -0
  22. zeitreiher_models-0.1.0/fields.go +214 -0
  23. zeitreiher_models-0.1.0/fields_test.go +55 -0
  24. zeitreiher_models-0.1.0/fixtures_test.go +328 -0
  25. zeitreiher_models-0.1.0/go.mod +7 -0
  26. zeitreiher_models-0.1.0/go.sum +4 -0
  27. zeitreiher_models-0.1.0/golden_test.go +56 -0
  28. zeitreiher_models-0.1.0/models_test.go +342 -0
  29. zeitreiher_models-0.1.0/ndjson.go +125 -0
  30. zeitreiher_models-0.1.0/proto/zeitreiher/v1/query.proto +153 -0
  31. zeitreiher_models-0.1.0/proto/zeitreiher/v1/results.proto +59 -0
  32. zeitreiher_models-0.1.0/proto/zeitreiher/v1/selector.proto +20 -0
  33. zeitreiher_models-0.1.0/proto/zeitreiher/v1/series_service.proto +44 -0
  34. zeitreiher_models-0.1.0/proto/zeitreiher/v1/system.proto +19 -0
  35. zeitreiher_models-0.1.0/pyproject.toml +100 -0
  36. zeitreiher_models-0.1.0/records.go +531 -0
  37. zeitreiher_models-0.1.0/request.go +268 -0
  38. zeitreiher_models-0.1.0/results.go +342 -0
  39. zeitreiher_models-0.1.0/scalars.go +193 -0
  40. zeitreiher_models-0.1.0/selector.go +194 -0
  41. zeitreiher_models-0.1.0/src/_zeitreiher_models_version.py +1 -0
  42. zeitreiher_models-0.1.0/src/zeitreiher_models/__init__.py +99 -0
  43. zeitreiher_models-0.1.0/src/zeitreiher_models/_base.py +115 -0
  44. zeitreiher_models-0.1.0/src/zeitreiher_models/errors.py +106 -0
  45. zeitreiher_models-0.1.0/src/zeitreiher_models/ndjson.py +65 -0
  46. zeitreiher_models-0.1.0/src/zeitreiher_models/py.typed +0 -0
  47. zeitreiher_models-0.1.0/src/zeitreiher_models/records.py +136 -0
  48. zeitreiher_models-0.1.0/src/zeitreiher_models/requests.py +89 -0
  49. zeitreiher_models-0.1.0/src/zeitreiher_models/results.py +90 -0
  50. zeitreiher_models-0.1.0/src/zeitreiher_models/scalars.py +148 -0
  51. zeitreiher_models-0.1.0/src/zeitreiher_models/selectors.py +95 -0
  52. zeitreiher_models-0.1.0/src/zeitreiher_models/system.py +18 -0
  53. zeitreiher_models-0.1.0/src/zeitreiher_models/timestamps.py +131 -0
  54. zeitreiher_models-0.1.0/system.go +93 -0
  55. zeitreiher_models-0.1.0/testdata/error_codes.json +34 -0
  56. zeitreiher_models-0.1.0/testdata/error_envelope/invalid/code_not_snake_case.json +6 -0
  57. zeitreiher_models-0.1.0/testdata/error_envelope/invalid/message_not_string.json +6 -0
  58. zeitreiher_models-0.1.0/testdata/error_envelope/invalid/missing_message.json +5 -0
  59. zeitreiher_models-0.1.0/testdata/error_envelope/invalid/not_enveloped.json +4 -0
  60. zeitreiher_models-0.1.0/testdata/error_envelope/read_only/unknown_code.json +6 -0
  61. zeitreiher_models-0.1.0/testdata/error_envelope/valid/cursor_expired.json +6 -0
  62. zeitreiher_models-0.1.0/testdata/error_envelope/valid/cursor_invalid.json +6 -0
  63. zeitreiher_models-0.1.0/testdata/error_envelope/valid/cursor_mismatch.json +6 -0
  64. zeitreiher_models-0.1.0/testdata/error_envelope/valid/cursor_stale.json +6 -0
  65. zeitreiher_models-0.1.0/testdata/error_envelope/valid/forbidden.json +6 -0
  66. zeitreiher_models-0.1.0/testdata/error_envelope/valid/internal.json +6 -0
  67. zeitreiher_models-0.1.0/testdata/error_envelope/valid/invalid_request.json +6 -0
  68. zeitreiher_models-0.1.0/testdata/error_envelope/valid/method_not_allowed.json +6 -0
  69. zeitreiher_models-0.1.0/testdata/error_envelope/valid/not_found.json +6 -0
  70. zeitreiher_models-0.1.0/testdata/error_envelope/valid/request_too_large.json +6 -0
  71. zeitreiher_models-0.1.0/testdata/error_envelope/valid/unauthorized.json +6 -0
  72. zeitreiher_models-0.1.0/testdata/error_envelope/valid/upstream_timeout.json +6 -0
  73. zeitreiher_models-0.1.0/testdata/error_envelope/valid/upstream_unreachable.json +6 -0
  74. zeitreiher_models-0.1.0/testdata/error_envelope/valid/with_request_id.json +7 -0
  75. zeitreiher_models-0.1.0/testdata/invalid_reasons.json +1466 -0
  76. zeitreiher_models-0.1.0/testdata/proto_fields.json +114 -0
  77. zeitreiher_models-0.1.0/testdata/query_request/invalid/compact_offset.json +10 -0
  78. zeitreiher_models-0.1.0/testdata/query_request/invalid/cursor_empty.json +11 -0
  79. zeitreiher_models-0.1.0/testdata/query_request/invalid/custom_by_uppercase.json +10 -0
  80. zeitreiher_models-0.1.0/testdata/query_request/invalid/custom_param_invalid_utf8.json +13 -0
  81. zeitreiher_models-0.1.0/testdata/query_request/invalid/custom_param_lone_surrogate.json +13 -0
  82. zeitreiher_models-0.1.0/testdata/query_request/invalid/custom_params_empty_key.json +12 -0
  83. zeitreiher_models-0.1.0/testdata/query_request/invalid/custom_params_non_string.json +12 -0
  84. zeitreiher_models-0.1.0/testdata/query_request/invalid/custom_params_null_value.json +13 -0
  85. zeitreiher_models-0.1.0/testdata/query_request/invalid/custom_selector_reserved_by.json +10 -0
  86. zeitreiher_models-0.1.0/testdata/query_request/invalid/custom_selector_without_params.json +9 -0
  87. zeitreiher_models-0.1.0/testdata/query_request/invalid/date_only.json +10 -0
  88. zeitreiher_models-0.1.0/testdata/query_request/invalid/day_32.json +10 -0
  89. zeitreiher_models-0.1.0/testdata/query_request/invalid/empty_select.json +5 -0
  90. zeitreiher_models-0.1.0/testdata/query_request/invalid/fractional_seconds.json +10 -0
  91. zeitreiher_models-0.1.0/testdata/query_request/invalid/from_after_to.json +10 -0
  92. zeitreiher_models-0.1.0/testdata/query_request/invalid/from_equals_to.json +10 -0
  93. zeitreiher_models-0.1.0/testdata/query_request/invalid/from_number.json +10 -0
  94. zeitreiher_models-0.1.0/testdata/query_request/invalid/from_trailing_newline.json +10 -0
  95. zeitreiher_models-0.1.0/testdata/query_request/invalid/from_underscore_key.json +10 -0
  96. zeitreiher_models-0.1.0/testdata/query_request/invalid/from_unicode_digits.json +10 -0
  97. zeitreiher_models-0.1.0/testdata/query_request/invalid/hour_24.json +10 -0
  98. zeitreiher_models-0.1.0/testdata/query_request/invalid/interval_daily.json +11 -0
  99. zeitreiher_models-0.1.0/testdata/query_request/invalid/interval_empty.json +11 -0
  100. zeitreiher_models-0.1.0/testdata/query_request/invalid/interval_hours.json +11 -0
  101. zeitreiher_models-0.1.0/testdata/query_request/invalid/interval_unicode_digits.json +11 -0
  102. zeitreiher_models-0.1.0/testdata/query_request/invalid/leap_second.json +10 -0
  103. zeitreiher_models-0.1.0/testdata/query_request/invalid/lowercase_t.json +10 -0
  104. zeitreiher_models-0.1.0/testdata/query_request/invalid/malo_unicode_digits.json +10 -0
  105. zeitreiher_models-0.1.0/testdata/query_request/invalid/malo_wrong_length.json +10 -0
  106. zeitreiher_models-0.1.0/testdata/query_request/invalid/minus_0000_offset.json +10 -0
  107. zeitreiher_models-0.1.0/testdata/query_request/invalid/minute_60.json +10 -0
  108. zeitreiher_models-0.1.0/testdata/query_request/invalid/missing_from.json +9 -0
  109. zeitreiher_models-0.1.0/testdata/query_request/invalid/missing_select.json +4 -0
  110. zeitreiher_models-0.1.0/testdata/query_request/invalid/month_13.json +10 -0
  111. zeitreiher_models-0.1.0/testdata/query_request/invalid/naive_from.json +10 -0
  112. zeitreiher_models-0.1.0/testdata/query_request/invalid/naive_to.json +10 -0
  113. zeitreiher_models-0.1.0/testdata/query_request/invalid/null_to.json +10 -0
  114. zeitreiher_models-0.1.0/testdata/query_request/invalid/offset_hour_24.json +10 -0
  115. zeitreiher_models-0.1.0/testdata/query_request/invalid/offset_minute_60.json +10 -0
  116. zeitreiher_models-0.1.0/testdata/query_request/invalid/profile_all_zeros.json +10 -0
  117. zeitreiher_models-0.1.0/testdata/query_request/invalid/profile_number_not_string.json +10 -0
  118. zeitreiher_models-0.1.0/testdata/query_request/invalid/profile_selector_non_digits.json +10 -0
  119. zeitreiher_models-0.1.0/testdata/query_request/invalid/profile_too_long.json +10 -0
  120. zeitreiher_models-0.1.0/testdata/query_request/invalid/profile_trailing_newline.json +10 -0
  121. zeitreiher_models-0.1.0/testdata/query_request/invalid/profile_unicode_digits.json +10 -0
  122. zeitreiher_models-0.1.0/testdata/query_request/invalid/profile_zero.json +10 -0
  123. zeitreiher_models-0.1.0/testdata/query_request/invalid/selector_by_array.json +10 -0
  124. zeitreiher_models-0.1.0/testdata/query_request/invalid/selector_by_number.json +10 -0
  125. zeitreiher_models-0.1.0/testdata/query_request/invalid/selector_by_object.json +10 -0
  126. zeitreiher_models-0.1.0/testdata/query_request/invalid/selector_missing_by.json +9 -0
  127. zeitreiher_models-0.1.0/testdata/query_request/invalid/selector_not_object.json +7 -0
  128. zeitreiher_models-0.1.0/testdata/query_request/invalid/selector_unknown_field.json +11 -0
  129. zeitreiher_models-0.1.0/testdata/query_request/invalid/space_separator.json +10 -0
  130. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_control_char.json +11 -0
  131. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_degree.json +11 -0
  132. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_empty.json +11 -0
  133. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_four_chars.json +11 -0
  134. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_inner_space.json +11 -0
  135. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_kvarh.json +11 -0
  136. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_kwh_external.json +11 -0
  137. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_lowercase.json +11 -0
  138. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_micromolar.json +11 -0
  139. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_nbsp.json +11 -0
  140. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_one_char.json +11 -0
  141. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_padded.json +11 -0
  142. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_percent.json +11 -0
  143. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_six_chars.json +11 -0
  144. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_superscript_digit.json +11 -0
  145. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_symbol.json +11 -0
  146. zeitreiher_models-0.1.0/testdata/query_request/invalid/unit_too_long.json +11 -0
  147. zeitreiher_models-0.1.0/testdata/query_request/invalid/unknown_field.json +11 -0
  148. zeitreiher_models-0.1.0/testdata/query_request/invalid/uppercase_from_key.json +10 -0
  149. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_as_of_not_supported.json +13 -0
  150. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_current_not_empty.json +15 -0
  151. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_current_true.json +13 -0
  152. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_empty.json +11 -0
  153. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_null_sibling.json +14 -0
  154. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_number_float.json +13 -0
  155. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_number_null.json +13 -0
  156. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_number_string.json +13 -0
  157. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_number_zero.json +13 -0
  158. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_two_keys.json +14 -0
  159. zeitreiher_models-0.1.0/testdata/query_request/invalid/version_unknown_key.json +13 -0
  160. zeitreiher_models-0.1.0/testdata/query_request/invalid/year_0000.json +10 -0
  161. zeitreiher_models-0.1.0/testdata/query_request/invalid/year_0001.json +10 -0
  162. zeitreiher_models-0.1.0/testdata/query_request/invalid/year_10000_in_utc.json +10 -0
  163. zeitreiher_models-0.1.0/testdata/query_request/invalid/year_1899.json +10 -0
  164. zeitreiher_models-0.1.0/testdata/query_request/invalid/year_1899_in_utc.json +10 -0
  165. zeitreiher_models-0.1.0/testdata/query_request/valid/custom_selector.json +13 -0
  166. zeitreiher_models-0.1.0/testdata/query_request/valid/custom_selector_empty_params.json +10 -0
  167. zeitreiher_models-0.1.0/testdata/query_request/valid/custom_selector_params_key_order.json +15 -0
  168. zeitreiher_models-0.1.0/testdata/query_request/valid/dst_autumn_day.json +11 -0
  169. zeitreiher_models-0.1.0/testdata/query_request/valid/dst_spring_day.json +11 -0
  170. zeitreiher_models-0.1.0/testdata/query_request/valid/duplicate_key_last_wins.canonical.json +11 -0
  171. zeitreiher_models-0.1.0/testdata/query_request/valid/duplicate_key_last_wins.json +12 -0
  172. zeitreiher_models-0.1.0/testdata/query_request/valid/full_spec_example.canonical.json +19 -0
  173. zeitreiher_models-0.1.0/testdata/query_request/valid/full_spec_example.json +20 -0
  174. zeitreiher_models-0.1.0/testdata/query_request/valid/minimal.json +10 -0
  175. zeitreiher_models-0.1.0/testdata/query_request/valid/offset_mixed.json +10 -0
  176. zeitreiher_models-0.1.0/testdata/query_request/valid/offset_plus_0000.canonical.json +10 -0
  177. zeitreiher_models-0.1.0/testdata/query_request/valid/offset_plus_0000.json +10 -0
  178. zeitreiher_models-0.1.0/testdata/query_request/valid/offset_plus_0200.json +10 -0
  179. zeitreiher_models-0.1.0/testdata/query_request/valid/offset_z.json +10 -0
  180. zeitreiher_models-0.1.0/testdata/query_request/valid/profile_leading_zeros.json +10 -0
  181. zeitreiher_models-0.1.0/testdata/query_request/valid/three_selectors.json +21 -0
  182. zeitreiher_models-0.1.0/testdata/query_request/valid/unit_kvr.json +11 -0
  183. zeitreiher_models-0.1.0/testdata/query_request/valid/unit_kwh.json +11 -0
  184. zeitreiher_models-0.1.0/testdata/query_request/valid/unit_two_chars.json +11 -0
  185. zeitreiher_models-0.1.0/testdata/query_request/valid/unit_with_digit.json +11 -0
  186. zeitreiher_models-0.1.0/testdata/query_request/valid/version_current.json +13 -0
  187. zeitreiher_models-0.1.0/testdata/query_request/valid/version_number.json +13 -0
  188. zeitreiher_models-0.1.0/testdata/query_request/valid/with_cursor.json +11 -0
  189. zeitreiher_models-0.1.0/testdata/query_request/valid/year_1900.json +10 -0
  190. zeitreiher_models-0.1.0/testdata/query_request/valid/year_1900_in_utc.json +10 -0
  191. zeitreiher_models-0.1.0/testdata/query_request/valid/year_9999_in_utc.json +10 -0
  192. zeitreiher_models-0.1.0/testdata/records/invalid/end_count_float.json +7 -0
  193. zeitreiher_models-0.1.0/testdata/records/invalid/end_missing_next.json +6 -0
  194. zeitreiher_models-0.1.0/testdata/records/invalid/end_negative_values.json +7 -0
  195. zeitreiher_models-0.1.0/testdata/records/invalid/end_next_empty.json +7 -0
  196. zeitreiher_models-0.1.0/testdata/records/invalid/error_code_not_snake_case.json +10 -0
  197. zeitreiher_models-0.1.0/testdata/records/invalid/error_missing_code.json +9 -0
  198. zeitreiher_models-0.1.0/testdata/records/invalid/error_missing_select.json +6 -0
  199. zeitreiher_models-0.1.0/testdata/records/invalid/error_missing_selector.json +9 -0
  200. zeitreiher_models-0.1.0/testdata/records/invalid/error_profile_empty.json +10 -0
  201. zeitreiher_models-0.1.0/testdata/records/invalid/missing_type.json +10 -0
  202. zeitreiher_models-0.1.0/testdata/records/invalid/not_an_object.json +13 -0
  203. zeitreiher_models-0.1.0/testdata/records/invalid/series_attribute_not_string.json +31 -0
  204. zeitreiher_models-0.1.0/testdata/records/invalid/series_attribute_null.json +35 -0
  205. zeitreiher_models-0.1.0/testdata/records/invalid/series_from_offset_plus_0200.json +29 -0
  206. zeitreiher_models-0.1.0/testdata/records/invalid/series_interval_p1d.json +29 -0
  207. zeitreiher_models-0.1.0/testdata/records/invalid/series_interval_unicode_digits.json +29 -0
  208. zeitreiher_models-0.1.0/testdata/records/invalid/series_missing_attributes.json +28 -0
  209. zeitreiher_models-0.1.0/testdata/records/invalid/series_missing_select.json +25 -0
  210. zeitreiher_models-0.1.0/testdata/records/invalid/series_missing_versions.json +17 -0
  211. zeitreiher_models-0.1.0/testdata/records/invalid/series_naive_to.json +29 -0
  212. zeitreiher_models-0.1.0/testdata/records/invalid/series_profile_leading_zero.json +29 -0
  213. zeitreiher_models-0.1.0/testdata/records/invalid/series_profile_unicode_digits.json +29 -0
  214. zeitreiher_models-0.1.0/testdata/records/invalid/series_role_empty.json +29 -0
  215. zeitreiher_models-0.1.0/testdata/records/invalid/series_role_missing.json +28 -0
  216. zeitreiher_models-0.1.0/testdata/records/invalid/series_role_number.json +29 -0
  217. zeitreiher_models-0.1.0/testdata/records/invalid/series_select_by_array.json +29 -0
  218. zeitreiher_models-0.1.0/testdata/records/invalid/series_select_param_null.json +35 -0
  219. zeitreiher_models-0.1.0/testdata/records/invalid/series_select_profile_leading_zero.json +29 -0
  220. zeitreiher_models-0.1.0/testdata/records/invalid/series_selector_negative.json +29 -0
  221. zeitreiher_models-0.1.0/testdata/records/invalid/series_unit_empty.json +29 -0
  222. zeitreiher_models-0.1.0/testdata/records/invalid/series_unit_lowercase.json +29 -0
  223. zeitreiher_models-0.1.0/testdata/records/invalid/series_unit_missing.json +28 -0
  224. zeitreiher_models-0.1.0/testdata/records/invalid/series_unit_raw_control_char.json +29 -0
  225. zeitreiher_models-0.1.0/testdata/records/invalid/series_unit_raw_empty.json +29 -0
  226. zeitreiher_models-0.1.0/testdata/records/invalid/series_unit_raw_missing.json +28 -0
  227. zeitreiher_models-0.1.0/testdata/records/invalid/series_unit_raw_null.json +29 -0
  228. zeitreiher_models-0.1.0/testdata/records/invalid/series_unit_raw_too_long.json +29 -0
  229. zeitreiher_models-0.1.0/testdata/records/invalid/type_not_string.json +11 -0
  230. zeitreiher_models-0.1.0/testdata/records/invalid/unknown_type.json +11 -0
  231. zeitreiher_models-0.1.0/testdata/records/invalid/value_comma.json +11 -0
  232. zeitreiher_models-0.1.0/testdata/records/invalid/value_exponent.json +11 -0
  233. zeitreiher_models-0.1.0/testdata/records/invalid/value_fraction_trailing_zero.json +11 -0
  234. zeitreiher_models-0.1.0/testdata/records/invalid/value_from_after_to.json +11 -0
  235. zeitreiher_models-0.1.0/testdata/records/invalid/value_hour_24.json +11 -0
  236. zeitreiher_models-0.1.0/testdata/records/invalid/value_leading_zero.json +11 -0
  237. zeitreiher_models-0.1.0/testdata/records/invalid/value_missing_status.json +8 -0
  238. zeitreiher_models-0.1.0/testdata/records/invalid/value_negative_trailing_zero.json +11 -0
  239. zeitreiher_models-0.1.0/testdata/records/invalid/value_negative_zero.json +11 -0
  240. zeitreiher_models-0.1.0/testdata/records/invalid/value_negative_zero_unicode.json +11 -0
  241. zeitreiher_models-0.1.0/testdata/records/invalid/value_number_not_string.json +11 -0
  242. zeitreiher_models-0.1.0/testdata/records/invalid/value_offset_not_z.json +11 -0
  243. zeitreiher_models-0.1.0/testdata/records/invalid/value_offset_plus_0000.json +11 -0
  244. zeitreiher_models-0.1.0/testdata/records/invalid/value_plus_sign.json +11 -0
  245. zeitreiher_models-0.1.0/testdata/records/invalid/value_series_zero.json +11 -0
  246. zeitreiher_models-0.1.0/testdata/records/invalid/value_status_duplicate.json +12 -0
  247. zeitreiher_models-0.1.0/testdata/records/invalid/value_status_empty_code.json +11 -0
  248. zeitreiher_models-0.1.0/testdata/records/invalid/value_status_unsorted.json +12 -0
  249. zeitreiher_models-0.1.0/testdata/records/invalid/value_trailing_dot.json +11 -0
  250. zeitreiher_models-0.1.0/testdata/records/invalid/value_trailing_newline.json +11 -0
  251. zeitreiher_models-0.1.0/testdata/records/invalid/value_trailing_zero.json +11 -0
  252. zeitreiher_models-0.1.0/testdata/records/invalid/value_unicode_digits.json +11 -0
  253. zeitreiher_models-0.1.0/testdata/records/invalid/value_version_missing_key.json +10 -0
  254. zeitreiher_models-0.1.0/testdata/records/invalid/value_version_zero.json +11 -0
  255. zeitreiher_models-0.1.0/testdata/records/invalid/value_year_1899.json +11 -0
  256. zeitreiher_models-0.1.0/testdata/records/invalid/value_zero_fraction.json +11 -0
  257. zeitreiher_models-0.1.0/testdata/records/invalid/version_created_at_inconsistent.json +29 -0
  258. zeitreiher_models-0.1.0/testdata/records/invalid/version_created_at_null_but_known.json +29 -0
  259. zeitreiher_models-0.1.0/testdata/records/invalid/version_description_invalid_utf8.json +29 -0
  260. zeitreiher_models-0.1.0/testdata/records/invalid/version_description_lone_high_surrogate.json +29 -0
  261. zeitreiher_models-0.1.0/testdata/records/invalid/version_description_lone_low_surrogate.json +29 -0
  262. zeitreiher_models-0.1.0/testdata/records/invalid/version_description_reversed_surrogates.json +29 -0
  263. zeitreiher_models-0.1.0/testdata/records/invalid/version_missing_to_key.json +28 -0
  264. zeitreiher_models-0.1.0/testdata/records/invalid/version_number_zero.json +29 -0
  265. zeitreiher_models-0.1.0/testdata/records/invalid/version_to_before_from.json +29 -0
  266. zeitreiher_models-0.1.0/testdata/records/read_only/error_unknown_code.json +10 -0
  267. zeitreiher_models-0.1.0/testdata/records/valid/end_empty_page.json +7 -0
  268. zeitreiher_models-0.1.0/testdata/records/valid/end_spec_example.json +7 -0
  269. zeitreiher_models-0.1.0/testdata/records/valid/end_with_cursor.json +7 -0
  270. zeitreiher_models-0.1.0/testdata/records/valid/error_access_denied.json +10 -0
  271. zeitreiher_models-0.1.0/testdata/records/valid/error_ambiguous_local_time.json +10 -0
  272. zeitreiher_models-0.1.0/testdata/records/valid/error_assignment_unaligned.json +10 -0
  273. zeitreiher_models-0.1.0/testdata/records/valid/error_bad_selector.json +12 -0
  274. zeitreiher_models-0.1.0/testdata/records/valid/error_badi_failed.json +10 -0
  275. zeitreiher_models-0.1.0/testdata/records/valid/error_conversion_not_allowed.json +10 -0
  276. zeitreiher_models-0.1.0/testdata/records/valid/error_interval_size_unsupported.json +10 -0
  277. zeitreiher_models-0.1.0/testdata/records/valid/error_no_assignment.json +9 -0
  278. zeitreiher_models-0.1.0/testdata/records/valid/error_profile_not_found.json +10 -0
  279. zeitreiher_models-0.1.0/testdata/records/valid/error_profile_time_zone_missing.json +10 -0
  280. zeitreiher_models-0.1.0/testdata/records/valid/error_selector_not_found.json +9 -0
  281. zeitreiher_models-0.1.0/testdata/records/valid/error_unknown_selector.json +11 -0
  282. zeitreiher_models-0.1.0/testdata/records/valid/error_version_deleted.json +10 -0
  283. zeitreiher_models-0.1.0/testdata/records/valid/error_version_not_found.json +10 -0
  284. zeitreiher_models-0.1.0/testdata/records/valid/error_window_unaligned.json +10 -0
  285. zeitreiher_models-0.1.0/testdata/records/valid/from_underscore_ignored.canonical.json +11 -0
  286. zeitreiher_models-0.1.0/testdata/records/valid/from_underscore_ignored.json +12 -0
  287. zeitreiher_models-0.1.0/testdata/records/valid/series_custom_selector_attributes.json +35 -0
  288. zeitreiher_models-0.1.0/testdata/records/valid/series_escaping_and_key_order.json +41 -0
  289. zeitreiher_models-0.1.0/testdata/records/valid/series_no_versions.json +18 -0
  290. zeitreiher_models-0.1.0/testdata/records/valid/series_select_unknown_key_ignored.canonical.json +29 -0
  291. zeitreiher_models-0.1.0/testdata/records/valid/series_select_unknown_key_ignored.json +30 -0
  292. zeitreiher_models-0.1.0/testdata/records/valid/series_spec_example.json +29 -0
  293. zeitreiher_models-0.1.0/testdata/records/valid/series_unit_converted.json +29 -0
  294. zeitreiher_models-0.1.0/testdata/records/valid/series_unit_kvarh.json +29 -0
  295. zeitreiher_models-0.1.0/testdata/records/valid/series_unit_raw_free_text.json +29 -0
  296. zeitreiher_models-0.1.0/testdata/records/valid/series_unit_unmapped.json +29 -0
  297. zeitreiher_models-0.1.0/testdata/records/valid/series_version_closed.json +29 -0
  298. zeitreiher_models-0.1.0/testdata/records/valid/series_version_created_at_unknown.json +29 -0
  299. zeitreiher_models-0.1.0/testdata/records/valid/series_version_deleted.json +29 -0
  300. zeitreiher_models-0.1.0/testdata/records/valid/unknown_field_ignored.canonical.json +11 -0
  301. zeitreiher_models-0.1.0/testdata/records/valid/unknown_field_ignored.json +12 -0
  302. zeitreiher_models-0.1.0/testdata/records/valid/uppercase_key_ignored.canonical.json +11 -0
  303. zeitreiher_models-0.1.0/testdata/records/valid/uppercase_key_ignored.json +12 -0
  304. zeitreiher_models-0.1.0/testdata/records/valid/value_duplicate_key_last_wins.canonical.json +11 -0
  305. zeitreiher_models-0.1.0/testdata/records/valid/value_duplicate_key_last_wins.json +12 -0
  306. zeitreiher_models-0.1.0/testdata/records/valid/value_fraction_decimal.json +11 -0
  307. zeitreiher_models-0.1.0/testdata/records/valid/value_from_version.json +11 -0
  308. zeitreiher_models-0.1.0/testdata/records/valid/value_negative_decimal.json +11 -0
  309. zeitreiher_models-0.1.0/testdata/records/valid/value_no_status.json +9 -0
  310. zeitreiher_models-0.1.0/testdata/records/valid/value_spec_example.json +11 -0
  311. zeitreiher_models-0.1.0/testdata/records/valid/value_two_statuses.json +12 -0
  312. zeitreiher_models-0.1.0/testdata/records/valid/value_zero.json +11 -0
  313. zeitreiher_models-0.1.0/testdata/records/valid/version_description_surrogate_pair.json +29 -0
  314. zeitreiher_models-0.1.0/testdata/resolve_request/invalid/cursor_not_allowed.json +11 -0
  315. zeitreiher_models-0.1.0/testdata/resolve_request/invalid/custom_selector_without_params.json +9 -0
  316. zeitreiher_models-0.1.0/testdata/resolve_request/invalid/empty_select.json +5 -0
  317. zeitreiher_models-0.1.0/testdata/resolve_request/invalid/from_after_to.json +10 -0
  318. zeitreiher_models-0.1.0/testdata/resolve_request/invalid/naive_from.json +10 -0
  319. zeitreiher_models-0.1.0/testdata/resolve_request/invalid/selector_by_object.json +10 -0
  320. zeitreiher_models-0.1.0/testdata/resolve_request/invalid/unknown_field.json +11 -0
  321. zeitreiher_models-0.1.0/testdata/resolve_request/invalid/version_field_not_allowed.json +13 -0
  322. zeitreiher_models-0.1.0/testdata/resolve_request/valid/minimal.json +10 -0
  323. zeitreiher_models-0.1.0/testdata/resolve_request/valid/two_selectors.json +17 -0
  324. zeitreiher_models-0.1.0/testdata/resolve_response/invalid/assignment_from_without_to_with_error.json +20 -0
  325. zeitreiher_models-0.1.0/testdata/resolve_response/invalid/assignment_missing_to.json +17 -0
  326. zeitreiher_models-0.1.0/testdata/resolve_response/invalid/assignment_profile_leading_zero.json +18 -0
  327. zeitreiher_models-0.1.0/testdata/resolve_response/invalid/both_assignments_and_error.json +15 -0
  328. zeitreiher_models-0.1.0/testdata/resolve_response/invalid/error_without_code.json +12 -0
  329. zeitreiher_models-0.1.0/testdata/resolve_response/invalid/missing_results.json +1 -0
  330. zeitreiher_models-0.1.0/testdata/resolve_response/invalid/neither.json +11 -0
  331. zeitreiher_models-0.1.0/testdata/resolve_response/invalid/selector_negative.json +12 -0
  332. zeitreiher_models-0.1.0/testdata/resolve_response/valid/assignment_error_access_denied.json +24 -0
  333. zeitreiher_models-0.1.0/testdata/resolve_response/valid/assignment_error_with_window.json +21 -0
  334. zeitreiher_models-0.1.0/testdata/resolve_response/valid/empty_assignments.json +15 -0
  335. zeitreiher_models-0.1.0/testdata/resolve_response/valid/empty_results.json +3 -0
  336. zeitreiher_models-0.1.0/testdata/resolve_response/valid/spec_example.json +28 -0
  337. zeitreiher_models-0.1.0/testdata/resolve_response/valid/versions_key_ignored.canonical.json +18 -0
  338. zeitreiher_models-0.1.0/testdata/resolve_response/valid/versions_key_ignored.json +30 -0
  339. zeitreiher_models-0.1.0/testdata/serialized.golden +410 -0
  340. zeitreiher_models-0.1.0/testdata/streams/invalid/blank_line.ndjson +5 -0
  341. zeitreiher_models-0.1.0/testdata/streams/invalid/crlf.ndjson +4 -0
  342. zeitreiher_models-0.1.0/testdata/streams/invalid/empty.ndjson +0 -0
  343. zeitreiher_models-0.1.0/testdata/streams/invalid/end_not_last.ndjson +3 -0
  344. zeitreiher_models-0.1.0/testdata/streams/invalid/invalid_record.ndjson +3 -0
  345. zeitreiher_models-0.1.0/testdata/streams/invalid/missing_end.ndjson +3 -0
  346. zeitreiher_models-0.1.0/testdata/streams/invalid/pretty_printed_record.ndjson +21 -0
  347. zeitreiher_models-0.1.0/testdata/streams/invalid/two_ends.ndjson +5 -0
  348. zeitreiher_models-0.1.0/testdata/streams/valid/dst_autumn_day_pt15m.ndjson +102 -0
  349. zeitreiher_models-0.1.0/testdata/streams/valid/dst_spring_day_pt15m.ndjson +94 -0
  350. zeitreiher_models-0.1.0/testdata/streams/valid/gaps.ndjson +86 -0
  351. zeitreiher_models-0.1.0/testdata/streams/valid/no_trailing_newline.ndjson +3 -0
  352. zeitreiher_models-0.1.0/testdata/streams/valid/only_errors.ndjson +2 -0
  353. zeitreiher_models-0.1.0/testdata/streams/valid/paged_page1.ndjson +4 -0
  354. zeitreiher_models-0.1.0/testdata/streams/valid/paged_page2.ndjson +4 -0
  355. zeitreiher_models-0.1.0/testdata/streams/valid/spec_example.ndjson +6 -0
  356. zeitreiher_models-0.1.0/testdata/streams/valid/two_series_profile_change.ndjson +8 -0
  357. zeitreiher_models-0.1.0/testdata/streams/valid/version_number.ndjson +6 -0
  358. zeitreiher_models-0.1.0/testdata/system_info/invalid/empty_business_time_zone.json +13 -0
  359. zeitreiher_models-0.1.0/testdata/system_info/invalid/interval_p1d.json +12 -0
  360. zeitreiher_models-0.1.0/testdata/system_info/invalid/missing_sid.json +12 -0
  361. zeitreiher_models-0.1.0/testdata/system_info/valid/minimal.json +13 -0
  362. zeitreiher_models-0.1.0/testdata/versions_request/invalid/cursor_not_allowed.json +11 -0
  363. zeitreiher_models-0.1.0/testdata/versions_request/invalid/custom_selector_without_params.json +9 -0
  364. zeitreiher_models-0.1.0/testdata/versions_request/invalid/empty_select.json +5 -0
  365. zeitreiher_models-0.1.0/testdata/versions_request/invalid/from_after_to.json +10 -0
  366. zeitreiher_models-0.1.0/testdata/versions_request/invalid/naive_from.json +10 -0
  367. zeitreiher_models-0.1.0/testdata/versions_request/invalid/unknown_field.json +11 -0
  368. zeitreiher_models-0.1.0/testdata/versions_request/invalid/version_field_not_allowed.json +13 -0
  369. zeitreiher_models-0.1.0/testdata/versions_request/valid/minimal.json +10 -0
  370. zeitreiher_models-0.1.0/testdata/versions_request/valid/two_selectors.json +17 -0
  371. zeitreiher_models-0.1.0/testdata/versions_response/invalid/error_with_versions.json +31 -0
  372. zeitreiher_models-0.1.0/testdata/versions_response/invalid/missing_versions.json +18 -0
  373. zeitreiher_models-0.1.0/testdata/versions_response/invalid/version_number_zero.json +30 -0
  374. zeitreiher_models-0.1.0/testdata/versions_response/valid/assignment_error.json +19 -0
  375. zeitreiher_models-0.1.0/testdata/versions_response/valid/assignment_no_versions_empty_list.json +19 -0
  376. zeitreiher_models-0.1.0/testdata/versions_response/valid/deleted_and_created_at_unknown.json +40 -0
  377. zeitreiher_models-0.1.0/testdata/versions_response/valid/spec_example_with_versions.json +40 -0
  378. zeitreiher_models-0.1.0/timestamps.go +132 -0
  379. zeitreiher_models-0.1.0/timestamps_test.go +125 -0
  380. zeitreiher_models-0.1.0/uv.lock +745 -0
@@ -0,0 +1,9 @@
1
+ * text=auto eol=lf
2
+ *.go text eol=lf
3
+ *.py text eol=lf
4
+ *.json text eol=lf
5
+ *.ndjson text eol=lf
6
+ *.proto text eol=lf
7
+ *.md text eol=lf
8
+ *.yml text eol=lf
9
+ testdata/streams/invalid/crlf.ndjson -text
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "gomod"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ - package-ecosystem: "uv"
8
+ directory: "/"
9
+ schedule:
10
+ interval: "weekly"
11
+ - package-ecosystem: "github-actions"
12
+ directory: "/"
13
+ schedule:
14
+ interval: "weekly"
@@ -0,0 +1,18 @@
1
+ name: Dependabot auto-approve / -merge
2
+ on: pull_request
3
+
4
+ jobs:
5
+ dependabot:
6
+ permissions:
7
+ contents: write
8
+ pull-requests: write
9
+ runs-on: ubuntu-latest
10
+ env:
11
+ PR_URL: ${{github.event.pull_request.html_url}}
12
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
13
+ if: ${{ github.actor == 'dependabot[bot]' }}
14
+ steps:
15
+ - name: Approve a PR
16
+ run: gh pr review --approve "$PR_URL"
17
+ - name: Enable auto-merge for Dependabot PRs
18
+ run: gh pr merge --auto --squash "$PR_URL"
@@ -0,0 +1,28 @@
1
+ name: "Go Lint"
2
+
3
+ on:
4
+ pull_request: {}
5
+
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ golangci:
11
+ runs-on: ubuntu-latest
12
+ name: lint
13
+ env:
14
+ # Build/run the linter with the toolchain the module targets (go 1.27.0).
15
+ GOTOOLCHAIN: go1.27.0
16
+ steps:
17
+ - name: Install Go
18
+ uses: actions/setup-go@v7
19
+ with:
20
+ go-version: "1.27.x"
21
+ - uses: actions/checkout@v7
22
+ - name: golangci-lint
23
+ uses: golangci/golangci-lint-action@v9
24
+ with:
25
+ version: v2.15.0
26
+ args: --enable dupl,gocyclo
27
+ - name: fmt
28
+ run: golangci-lint fmt --diff --enable gofmt
@@ -0,0 +1,26 @@
1
+ name: "Go Tests"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request: {}
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ test:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - name: Install Go
16
+ uses: actions/setup-go@v7
17
+ with:
18
+ go-version: "1.27.x"
19
+ - name: Checkout code
20
+ uses: actions/checkout@v7
21
+ - name: Vet
22
+ run: go vet ./...
23
+ - name: Test
24
+ run: go test -race -covermode=atomic -coverprofile=coverage.out ./...
25
+ - name: Coverage
26
+ run: go tool cover -func=coverage.out | tail -1
@@ -0,0 +1,20 @@
1
+ name: "Packaging Test"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request: {}
7
+
8
+ jobs:
9
+ check_packaging:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v7
13
+ - name: Install uv
14
+ uses: astral-sh/setup-uv@v10.3.2
15
+ with:
16
+ python-version: "3.15"
17
+ - name: Build distributions
18
+ run: uv build
19
+ - name: Check distributions
20
+ run: uv publish --dry-run
@@ -0,0 +1,27 @@
1
+ name: "Proto"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request: {}
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ buf:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ - name: Install Go
17
+ uses: actions/setup-go@v7
18
+ with:
19
+ go-version: "1.27.x"
20
+ - name: Install buf
21
+ run: go install github.com/bufbuild/buf/cmd/buf@v1.75.0
22
+ - name: buf build
23
+ run: buf build
24
+ - name: buf lint
25
+ run: buf lint
26
+ - name: buf format
27
+ run: buf format --diff --exit-code
@@ -0,0 +1,23 @@
1
+ name: "Python Coverage"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request: {}
7
+
8
+ jobs:
9
+ coverage:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v7
13
+ - name: Install uv
14
+ uses: astral-sh/setup-uv@v10.3.2
15
+ with:
16
+ python-version: "3.15"
17
+ - name: Install dependencies
18
+ run: uv sync --group coverage
19
+ - name: Run Tests and Record Coverage
20
+ run: |
21
+ uv run coverage run -m pytest unittests
22
+ uv run coverage html --omit 'unittests/*,.venv/*'
23
+ uv run coverage report --fail-under 80 --omit 'unittests/*,.venv/*'
@@ -0,0 +1,28 @@
1
+ name: "Python Formatting"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request: {}
7
+
8
+ jobs:
9
+ format:
10
+ name: "format (${{ matrix.tool }})"
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ include:
15
+ - tool: ruff-format
16
+ command: "ruff format --check ."
17
+ - tool: ruff-isort
18
+ command: "ruff check --select I ."
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v10.3.2
23
+ with:
24
+ python-version: "3.15"
25
+ - name: Install dependencies
26
+ run: uv sync --group linting
27
+ - name: ${{ matrix.tool }}
28
+ run: uv run ${{ matrix.command }}
@@ -0,0 +1,35 @@
1
+ name: "Python Lint"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request: {}
7
+
8
+ jobs:
9
+ pylint:
10
+ name: "Python Code Quality and Lint (${{ matrix.linter-env }})"
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ include:
15
+ - linter-env: linting
16
+ commands: |
17
+ uv run ruff check src/zeitreiher_models unittests
18
+ - linter-env: type_check
19
+ commands: |
20
+ uv run mypy --show-error-codes src/zeitreiher_models --strict
21
+ uv run mypy --show-error-codes unittests --strict
22
+ - linter-env: spell_check
23
+ commands: |
24
+ uv run codespell --ignore-words=domain-specific-terms.txt src
25
+ uv run codespell --ignore-words=domain-specific-terms.txt README.md
26
+ steps:
27
+ - uses: actions/checkout@v7
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@v10.3.2
30
+ with:
31
+ python-version: "3.15"
32
+ - name: Install Dependencies
33
+ run: uv sync --group ${{ matrix.linter-env }}
34
+ - name: Run ${{ matrix.linter-env }}
35
+ run: ${{ matrix.commands }}
@@ -0,0 +1,52 @@
1
+ name: "Publish to PyPI"
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ tests:
9
+ if: startsWith(github.ref, 'refs/tags/v')
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v7
13
+ - name: Install uv
14
+ uses: astral-sh/setup-uv@v10.3.2
15
+ with:
16
+ python-version: "3.15"
17
+ - name: Install dependencies
18
+ run: uv sync --group tests --group linting --group coverage --group type_check
19
+ - name: Run tests
20
+ run: uv run pytest unittests
21
+ - name: Run linting
22
+ run: |
23
+ uv run ruff check src/zeitreiher_models unittests
24
+ - name: Run type_check
25
+ run: |
26
+ uv run mypy --show-error-codes src/zeitreiher_models --strict
27
+ uv run mypy --show-error-codes unittests --strict
28
+ - name: Run coverage
29
+ run: |
30
+ uv run coverage run -m pytest unittests
31
+ uv run coverage report --fail-under 80 --omit 'unittests/*,.venv/*'
32
+
33
+ build-n-publish:
34
+ name: Build and publish Python distributions to PyPI
35
+ runs-on: ubuntu-latest
36
+ environment: release
37
+ permissions:
38
+ id-token: write
39
+ needs: tests
40
+ steps:
41
+ - uses: actions/checkout@v7
42
+ with:
43
+ fetch-depth: 0 # hatch-vcs derives the package version from the git tag
44
+ - name: Install uv
45
+ uses: astral-sh/setup-uv@v10.3.2
46
+ with:
47
+ python-version: "3.15"
48
+ - name: Build wheel and source distributions
49
+ run: uv build
50
+ - name: Publish distribution to PyPI
51
+ if: startsWith(github.ref, 'refs/tags/v')
52
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,24 @@
1
+ name: "Python Tests"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request: {}
7
+
8
+ jobs:
9
+ pytest:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
14
+ os: [ubuntu-latest]
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v10.3.2
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Install Dependencies
22
+ run: uv sync --group tests
23
+ - name: Run the Unit Tests
24
+ run: uv run pytest unittests
@@ -0,0 +1,30 @@
1
+ # Go
2
+ *.exe
3
+ *.exe~
4
+ *.dll
5
+ *.so
6
+ *.dylib
7
+ *.test
8
+ *.out
9
+
10
+ # Python
11
+ __pycache__/
12
+ *.py[cod]
13
+ *.egg-info/
14
+ dist/
15
+ build/
16
+ .tox/
17
+ .mypy_cache/
18
+ htmlcov/
19
+ .coverage
20
+ src/_zeitreiher_models_version.py
21
+
22
+ # IDE
23
+ .idea/
24
+ .vscode/
25
+ *.swp
26
+
27
+ # Env
28
+ .env
29
+ .venv/
30
+ coverage.out
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hochfrequenz Unternehmensberatung GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,235 @@
1
+ Metadata-Version: 2.5
2
+ Name: zeitreiher-models
3
+ Version: 0.1.0
4
+ Summary: Hand-written public data models of the Zeitreiher API (SAP IS-U EDM profile values)
5
+ Project-URL: Changelog, https://github.com/Hochfrequenz/zeitreiher-models/releases
6
+ Project-URL: Homepage, https://github.com/Hochfrequenz/zeitreiher-models
7
+ Author-email: Hochfrequenz Unternehmensberatung GmbH <info+github@hochfrequenz.de>
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Keywords: edm,is-u,lastgang,sap,time series,zeitreiher
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: pydantic>=2.11
25
+ Description-Content-Type: text/markdown
26
+
27
+ # zeitreiher-models
28
+
29
+ ![Go Tests](https://github.com/Hochfrequenz/zeitreiher-models/workflows/Go%20Tests/badge.svg)
30
+ ![Go Lint](https://github.com/Hochfrequenz/zeitreiher-models/workflows/Go%20Lint/badge.svg)
31
+ [![Go Reference](https://pkg.go.dev/badge/github.com/hochfrequenz/zeitreiher-models.svg)](https://pkg.go.dev/github.com/hochfrequenz/zeitreiher-models)
32
+ ![Python Tests](https://github.com/Hochfrequenz/zeitreiher-models/workflows/Python%20Tests/badge.svg)
33
+ ![Python Coverage](https://github.com/Hochfrequenz/zeitreiher-models/workflows/Python%20Coverage/badge.svg)
34
+ ![Python Lint](https://github.com/Hochfrequenz/zeitreiher-models/workflows/Python%20Lint/badge.svg)
35
+ ![Python Formatting](https://github.com/Hochfrequenz/zeitreiher-models/workflows/Python%20Formatting/badge.svg)
36
+ ![Proto](https://github.com/Hochfrequenz/zeitreiher-models/workflows/Proto/badge.svg)
37
+
38
+ Public data models of the **Zeitreiher API, version 1**: a read-only API over SAP IS-U EDM profile
39
+ values (Lastgänge) — profile metadata plus many `[from, to)` intervals with values.
40
+
41
+ This repository holds
42
+
43
+ - **Go** structs (package `zeitreihermodels`, module `github.com/hochfrequenz/zeitreiher-models`),
44
+ - **Python** frozen pydantic v2 models (package `zeitreiher_models`),
45
+ - the shared **fixtures** in [`testdata/`](testdata) that define the JSON contract, and
46
+ - the **gRPC contract** in [`proto/zeitreiher/v1`](proto/zeitreiher/v1).
47
+
48
+ Everything is **written by hand**. No code is generated and no generated code is committed: every
49
+ type, validator and (un)marshaller is ordinary, readable code. Both languages parse every valid
50
+ fixture, write it back and compare the result with the fixture, and reject every invalid fixture —
51
+ so Go and Python agree on the contract by construction.
52
+
53
+ ## The contract in short
54
+
55
+ | Topic | Rule |
56
+ |---|---|
57
+ | Intervals | Half-open everywhere: `[from, to)`; `from` must be before `to`. |
58
+ | Request timestamps | RFC 3339, whole seconds, **explicit offset** (`Z` or `±hh:mm`), ASCII digits only, hours 00–23. The instant must lie in the years 1900–9999 **in UTC**, so that it can be echoed in a response (`1900-01-01T00:00:00+01:00` is 1899 in UTC and rejected). Kept as sent (`+02:00` stays `+02:00`). A missing offset (`2026-01-01T00:00:00`) and the "unknown offset" `-00:00` are rejected. |
59
+ | Response timestamps | RFC 3339, whole seconds, always UTC (`Z`). Instants built in code must survive the round trip (no offsets with seconds, years 1900–9999). |
60
+ | Values | Already-scaled decimal **strings** in their **shortest canonical form** (`"160"`, `"0.022"`, `"25.25"`, `"-3.5"`), never JSON numbers. No exponent, no plus sign, no leading zeros, no trailing zeros after the decimal point (`"160.0"` is rejected: after parsing it equals `"160"`), no negative zero. |
61
+ | Profile numbers | In requests 1–18 digits, leading zeros allowed, not all zeros; in responses without leading zeros. |
62
+ | Units | `unit` is a **canonical unit code**: a UN/ECE Recommendation 20 code such as `KWH`, `MWH` or `KVR` (2 or 3 upper-case ASCII letters or digits, strict). Requests send canonical codes; the server maps them to the customer's SAP unit (an unknown code is a 400). In series records `unit` is always present and `null` when the SAP unit has no mapping; the series is delivered anyway. `unit_raw` (series records) is the SAP unit key (T006 `MSEHI`) as SAP delivers it, mapped or not, read leniently: any non-empty text of at most 30 characters (code points) without control characters (Unicode `Cc`). |
63
+ | Series role | Every `series` record carries `role`, the role of its profile assignment (SAP `EPROFASS` role, e.g. `"0001"`; any non-empty text), or `null` when the series was not resolved through a role (e.g. `by: profile`). For `by: malo` the roles to deliver are configurable (default: all). |
64
+ | Selectors | `{"by":"profile","profile":"…"}`, `{"by":"malo","malo":"<11 digits>"}`, or a customer resolver `{"by":"<snake_case name>","params":{"<key>":"<string>"}}`. |
65
+ | Version selector | Exactly one of `{"current":{}}` (default) or `{"number":N}`; no `null` members. v1 has no as-of selector: `{"as_of":…}` is rejected. `{"number":N}` returns the current values overlaid with the stored values of versions ≥ N (the lowest wins), only inside N's covered range (gaps outside). Each value record says its source: `"version":<n>` for a stored version row, `"version":null` for a current value. |
66
+ | Intervals (`interval`) | Sub-daily ISO 8601 durations in minutes: `PT15M`, `PT60M`, … Hours are written in minutes: `PT60M`, never `PT1H` (rejected, so that every interval has one spelling). |
67
+ | Text | Every string is valid Unicode and is written unchanged. Parsers reject invalid UTF-8 and unpaired `\uD800`–`\uDFFF` escapes. Go refuses to marshal a string that is not valid UTF-8 (instead of writing U+FFFD); Python rejects a string with a lone surrogate when the model is built (`ValidationError`). |
68
+ | Unknown keys | Requests **reject** them (a typo fails loudly). Responses **ignore** them (additive server changes do not break older clients). |
69
+ | Duplicate keys | Not rejected: when an object repeats a key, the **last** occurrence wins and earlier ones are ignored without validation (`{"unit":"K H","unit":"MWH"}` is read as `"MWH"`), in both languages and on both sides. Producers must not write duplicate keys; the models never do. Pinned by the `*duplicate_key_last_wins*` fixtures. |
70
+ | `null` | Request fields that are optional may be omitted or `null`. In responses, `series.role`, `series.unit`, `value.version`, `end.next`, `version.to` and `version.created_at` are always present and may be `null`; other optional response fields are omitted when unset. |
71
+ | Error codes | Request-level codes in the error envelope `{"error":{"code","message","request_id"?}}` — btpingo's vocabulary (`invalid_request`, `unauthorized`, `forbidden`, `not_found`, `method_not_allowed`, `request_too_large`, `upstream_unreachable`, `internal`), shared by the Hochfrequenz BTP services, plus `cursor_invalid`, `cursor_mismatch`, `cursor_expired`, `cursor_stale` and `upstream_timeout`; record-level codes in `error` records and resolve/versions results. The known codes are listed in [`testdata/error_codes.json`](testdata/error_codes.json). **Strict on write, lenient on read:** parsers accept unknown snake_case codes so that a newer server does not break older clients (Go `IsKnown()`, Python `RecordErrorCode | str`); writers refuse every code outside the public list (Go `MarshalJSON` returns an error, Python JSON serialization raises `PydanticSerializationError`; `model_dump()` to a dict keeps them). |
72
+ | NDJSON | One record per line, lines end with `\n` (the last one optionally), no `\r`, no blank lines. A response that does not end with exactly one `end` record was cut off and **must be treated as a failure**. The `end` record's `series`, `values` and `errors` count the records **on this page** only; a series record re-sent at the start of a continuation page counts again. |
73
+
74
+ The full design lives in the `zeitreiher-go` repository (`docs/superpowers/specs/2026-10-27-zeitreiher-design.md`).
75
+
76
+ ## Go
77
+
78
+ ```bash
79
+ go get github.com/hochfrequenz/zeitreiher-models
80
+ ```
81
+
82
+ ```go
83
+ import zrm "github.com/hochfrequenz/zeitreiher-models"
84
+
85
+ cet := time.FixedZone("CET", 3600)
86
+ req := zrm.QueryRequest{
87
+ Select: []zrm.Selector{zrm.MaLoSelector("87253749908"), zrm.ProfileSelector("50000000000056")},
88
+ From: time.Date(2026, 1, 1, 0, 0, 0, 0, cet),
89
+ To: time.Date(2026, 2, 1, 0, 0, 0, 0, cet),
90
+ Interval: "PT60M",
91
+ }
92
+ body, err := json.Marshal(req) // validates; an invalid request does not marshal
93
+
94
+ records, err := zrm.ReadStream(resp.Body) // errors.Is(err, zrm.ErrMissingEnd) for a cut-off stream
95
+ for _, rec := range records {
96
+ switch r := rec.(type) {
97
+ case *zrm.SeriesRecord:
98
+ case *zrm.ValueRecord: // r.Value is a zrm.Decimal (string)
99
+ case *zrm.ErrorRecord: // r.Code.IsKnown()
100
+ case *zrm.EndRecord: // r.Next != nil → request the next page with Cursor = *r.Next
101
+ }
102
+ }
103
+ ```
104
+
105
+ `ReadRecords(io.Reader)` yields records one by one (`iter.Seq2[Record, error]`) for streaming;
106
+ `WriteRecord` writes one NDJSON line. Every type validates itself in `UnmarshalJSON` and refuses
107
+ to marshal when it is invalid.
108
+
109
+ ## Python
110
+
111
+ ```bash
112
+ pip install zeitreiher-models # once published to PyPI
113
+ ```
114
+
115
+ ```python
116
+ from datetime import datetime, timedelta, timezone
117
+
118
+ from zeitreiher_models import MaLoSelector, QueryRequest, ValueRecord, iter_records, read_stream
119
+
120
+ cet = timezone(timedelta(hours=1))
121
+ request = QueryRequest(
122
+ select=[MaLoSelector(malo="87253749908")],
123
+ from_=datetime(2026, 1, 1, tzinfo=cet),
124
+ to=datetime(2026, 2, 1, tzinfo=cet),
125
+ interval="PT60M",
126
+ )
127
+ body = request.model_dump_json() # {"select":[…],"from":"2026-01-01T00:00:00+01:00",…}
128
+
129
+ for record in read_stream(response_bytes): # raises MissingEndError for a cut-off stream
130
+ if isinstance(record, ValueRecord):
131
+ print(record.from_, record.decimal) # decimal.Decimal, no float rounding
132
+ ```
133
+
134
+ `from` is a Python keyword, so the attribute is `from_`; the JSON key is always `from`.
135
+
136
+ ## Fixtures
137
+
138
+ ```
139
+ testdata/
140
+ error_codes.json the known request- and record-level codes
141
+ query_request/{valid,invalid}/ request bodies (DST days, +02:00 vs Z, naive timestamps, custom selectors, versions, …)
142
+ resolve_request/, versions_request/
143
+ records/{valid,invalid}/ single NDJSON records, one valid fixture per record-level error code
144
+ streams/{valid,invalid}/ whole NDJSON responses (DST days with 92/100 quarter hours, gaps, versions, paging, …)
145
+ resolve_response/, versions_response/
146
+ error_envelope/{valid,invalid}/ one valid fixture per request-level error code
147
+ records/read_only/, error_envelope/read_only/
148
+ unknown error codes: they parse, but writing them is refused
149
+ system_info/
150
+ ```
151
+
152
+ `X.canonical.json` next to `X.json` is the expected re-serialization when it differs from the input
153
+ (e.g. `+00:00` becomes `Z`, `"cursor": null` is omitted, unknown response keys are dropped).
154
+ Consumers in other languages (the Go service, future Rust/C#/TS models) should run the same fixtures.
155
+
156
+ `testdata/serialized.golden` pins the exact bytes both languages write for every valid fixture
157
+ (`<fixture>\t<JSON>` per line), so Go and Python output is byte-for-byte identical: compact JSON,
158
+ keys in model order, map keys (`params`, `attributes`) sorted, and Go's string escaping — `<`, `>`,
159
+ `&`, U+2028 and U+2029 are written as `<`, `>`, `&`, `
`, `
`; all other
160
+ non-ASCII characters are written as UTF-8. Python's `model_dump_json()` and `dump_record()` apply
161
+ that escaping; in Go, `json.Marshal`, `MarshalRecord` and a default `json.Encoder` produce it (an
162
+ encoder with `SetEscapeHTML(false)` would not).
163
+
164
+ Every invalid fixture has an entry in `testdata/invalid_reasons.json`, so that a fixture cannot pass by
165
+ being rejected for the wrong reason:
166
+
167
+ - `go`: a substring of the Go error (the field path and the failed rule);
168
+ - `python`: the pydantic error that must be among `ValidationError.errors()` — its `loc` (joined with
169
+ `.`), its `type` and, only for the error types whose messages the models write themselves
170
+ (`value_error`, `instant`, `unknown_key`, `version_selector`), `msg`, a substring of its message.
171
+ pydantic's own messages are not compared, so rewording them in a pydantic upgrade does not break
172
+ the fixtures.
173
+
174
+ ### Regenerating the pinned files
175
+
176
+ - **`serialized.golden`** — after an intended change to the output (or a new valid fixture), run
177
+ `go test -run Golden -update .`, review the diff, then `uv run pytest unittests` to confirm that
178
+ Python writes the same bytes. The file is only ever written by Go; Python compares against it.
179
+ - **`invalid_reasons.json`** — maintained by hand, not generated, because each entry states the
180
+ *intended* reason. For a new invalid fixture, the failing Go test prints the actual error
181
+ (`go test -run InvalidFixtures .`) and the failing Python test prints a ready-made `python` entry
182
+ (`uv run pytest unittests -k invalid_fixture`); check that both name the rule the fixture is about
183
+ before adding them. Keep the file sorted, two-space indented and ASCII-only
184
+ (`json.dumps(..., indent=2, sort_keys=True, ensure_ascii=True)`).
185
+ - **After a pydantic upgrade** — run `uv run pytest unittests`. A failing `invalid_fixture` test means
186
+ that pydantic changed an error's `loc` or `type` (messages are not compared), or that a fixture is
187
+ now rejected for a different reason; compare the printed `errors()` with the entry and update the
188
+ entry only if the fixture is still rejected for the intended reason. `serialized.golden` failures
189
+ after an upgrade mean that the output bytes changed and need a fix in the models, not a new golden
190
+ file.
191
+
192
+ ## gRPC
193
+
194
+ `proto/zeitreiher/v1/*.proto` (package `zeitreiher.v1`, service `SeriesService` with `Query`,
195
+ `Resolve`, `ListVersions`, `System`) is the published gRPC contract. Field names equal the JSON keys.
196
+ Request timestamps are RFC 3339 strings (so the explicit-offset rule holds for gRPC too); response
197
+ timestamps are `google.protobuf.Timestamp`. Conformance with the JSON contract is defined through
198
+ the hand-written models (fixture → model → proto → model → JSON), not through protojson. Generate
199
+ code in the consuming project, e.g. with buf managed mode.
200
+
201
+ A failed call (and a `Query` stream cut off before its `EndRecord`) ends with a gRPC status that
202
+ carries a `google.rpc.ErrorInfo` detail with `reason` = the public request-level code and
203
+ `domain` = `"zeitreiher.hochfrequenz.de"`; `series_service.proto` lists the status code for each
204
+ code. Clients switch on `reason`.
205
+
206
+ ## Versioning
207
+
208
+ `v1` is in the URL path (`/api/v1/…`), the proto package (`zeitreiher.v1`) and the models' major
209
+ version. Additive changes (new optional fields, new error codes) stay in v1; breaking changes need v2.
210
+
211
+ The Go package `zeitreihermodels` lives at the **repository root** (import path
212
+ `github.com/hochfrequenz/zeitreiher-models`); there is no `v1` sub-package. Its major version follows
213
+ Go module versioning: v1 is released as tags `v1.x.y` of this module path, and a breaking v2 becomes
214
+ the module `github.com/hochfrequenz/zeitreiher-models/v2` (tags `v2.x.y`). The Python package takes its
215
+ version from the same tags.
216
+
217
+ ## Development
218
+
219
+ ```bash
220
+ go test ./... # Go
221
+ golangci-lint run && golangci-lint fmt --diff
222
+ uv sync --group dev # Python
223
+ uv run pytest unittests
224
+ uv run ruff check src unittests && uv run ruff format --check .
225
+ uv run mypy --strict src/zeitreiher_models unittests
226
+ buf lint && buf build && buf format --diff --exit-code # buf v1.75.0
227
+ ```
228
+
229
+ The Go tests (`fixtures_test.go`) and the Python tests (`unittests/test_fixtures.py`) use the same
230
+ table of fixture kinds; a new fixture directory must be added to both. `unittests/test_proto_contract.py`
231
+ fails when a model and its proto message disagree on field names.
232
+
233
+ ## License
234
+
235
+ MIT