mdq-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 (304) hide show
  1. mdq_models-0.1.0/.gitattributes +12 -0
  2. mdq_models-0.1.0/.github/dependabot.yml +14 -0
  3. mdq_models-0.1.0/.github/workflows/dependabot_automerge.yml +18 -0
  4. mdq_models-0.1.0/.github/workflows/go-lint.yml +28 -0
  5. mdq_models-0.1.0/.github/workflows/go-test.yml +30 -0
  6. mdq_models-0.1.0/.github/workflows/packaging-test.yml +40 -0
  7. mdq_models-0.1.0/.github/workflows/proto.yml +63 -0
  8. mdq_models-0.1.0/.github/workflows/python-coverage.yml +23 -0
  9. mdq_models-0.1.0/.github/workflows/python-formatting.yml +28 -0
  10. mdq_models-0.1.0/.github/workflows/python-lint.yml +36 -0
  11. mdq_models-0.1.0/.github/workflows/python-publish.yml +58 -0
  12. mdq_models-0.1.0/.github/workflows/python-test.yml +26 -0
  13. mdq_models-0.1.0/.gitignore +30 -0
  14. mdq_models-0.1.0/.golangci.yml +19 -0
  15. mdq_models-0.1.0/LICENSE +21 -0
  16. mdq_models-0.1.0/PKG-INFO +284 -0
  17. mdq_models-0.1.0/README.md +255 -0
  18. mdq_models-0.1.0/buf.yaml +9 -0
  19. mdq_models-0.1.0/doc.go +11 -0
  20. mdq_models-0.1.0/docs/superpowers/plans/2026-10-30-v1-models.md +289 -0
  21. mdq_models-0.1.0/domain-specific-terms.txt +18 -0
  22. mdq_models-0.1.0/errors.go +43 -0
  23. mdq_models-0.1.0/example_test.go +78 -0
  24. mdq_models-0.1.0/export_test.go +20 -0
  25. mdq_models-0.1.0/fixtures_test.go +532 -0
  26. mdq_models-0.1.0/go.mod +7 -0
  27. mdq_models-0.1.0/go.sum +4 -0
  28. mdq_models-0.1.0/golden_test.go +75 -0
  29. mdq_models-0.1.0/messwert.go +209 -0
  30. mdq_models-0.1.0/models_test.go +461 -0
  31. mdq_models-0.1.0/object.go +192 -0
  32. mdq_models-0.1.0/proto/mdq/v1/enums.proto +65 -0
  33. mdq_models-0.1.0/proto/mdq/v1/messwert.proto +39 -0
  34. mdq_models-0.1.0/proto/mdq/v1/zaehlerstand.proto +23 -0
  35. mdq_models-0.1.0/proto_contract_test.go +316 -0
  36. mdq_models-0.1.0/pyproject.toml +115 -0
  37. mdq_models-0.1.0/scalars.go +400 -0
  38. mdq_models-0.1.0/src/mdq_models/__init__.py +34 -0
  39. mdq_models-0.1.0/src/mdq_models/_base.py +137 -0
  40. mdq_models-0.1.0/src/mdq_models/_version.py +1 -0
  41. mdq_models-0.1.0/src/mdq_models/enums.py +71 -0
  42. mdq_models-0.1.0/src/mdq_models/malo.py +66 -0
  43. mdq_models-0.1.0/src/mdq_models/messwert.py +46 -0
  44. mdq_models-0.1.0/src/mdq_models/polars.py +235 -0
  45. mdq_models-0.1.0/src/mdq_models/py.typed +0 -0
  46. mdq_models-0.1.0/src/mdq_models/scalars.py +132 -0
  47. mdq_models-0.1.0/src/mdq_models/zaehlerstand.py +17 -0
  48. mdq_models-0.1.0/testdata/README.md +75 -0
  49. mdq_models-0.1.0/testdata/herkunft_status.json +142 -0
  50. mdq_models-0.1.0/testdata/invalid_reasons.json +194 -0
  51. mdq_models-0.1.0/testdata/malo_ids.json +121 -0
  52. mdq_models-0.1.0/testdata/messwert/invalid/duplicate_key.json +11 -0
  53. mdq_models-0.1.0/testdata/messwert/invalid/ersatzwert_with_herkunft_null.json +10 -0
  54. mdq_models-0.1.0/testdata/messwert/invalid/ersatzwert_with_value_null.json +10 -0
  55. mdq_models-0.1.0/testdata/messwert/invalid/fehlend_with_herkunft_fremd.json +10 -0
  56. mdq_models-0.1.0/testdata/messwert/invalid/fehlend_with_herkunft_mqa.json +10 -0
  57. mdq_models-0.1.0/testdata/messwert/invalid/fehlend_with_value.json +10 -0
  58. mdq_models-0.1.0/testdata/messwert/invalid/herkunft_capitalised.json +10 -0
  59. mdq_models-0.1.0/testdata/messwert/invalid/herkunft_empty_string.json +10 -0
  60. mdq_models-0.1.0/testdata/messwert/invalid/herkunft_unknown_value.json +10 -0
  61. mdq_models-0.1.0/testdata/messwert/invalid/herkunft_upper_case.json +10 -0
  62. mdq_models-0.1.0/testdata/messwert/invalid/invalid_utf8_bytes.json +1 -0
  63. mdq_models-0.1.0/testdata/messwert/invalid/malo_10_digits.json +10 -0
  64. mdq_models-0.1.0/testdata/messwert/invalid/malo_12_digits.json +10 -0
  65. mdq_models-0.1.0/testdata/messwert/invalid/malo_arabic_indic_digits.json +10 -0
  66. mdq_models-0.1.0/testdata/messwert/invalid/malo_empty.json +10 -0
  67. mdq_models-0.1.0/testdata/messwert/invalid/malo_leading_zero.json +10 -0
  68. mdq_models-0.1.0/testdata/messwert/invalid/malo_letters.json +10 -0
  69. mdq_models-0.1.0/testdata/messwert/invalid/malo_non_ascii_digits.json +10 -0
  70. mdq_models-0.1.0/testdata/messwert/invalid/malo_null.json +10 -0
  71. mdq_models-0.1.0/testdata/messwert/invalid/malo_number.json +10 -0
  72. mdq_models-0.1.0/testdata/messwert/invalid/malo_with_space.json +10 -0
  73. mdq_models-0.1.0/testdata/messwert/invalid/malo_wrong_check_digit.json +10 -0
  74. mdq_models-0.1.0/testdata/messwert/invalid/missing_herkunft.json +9 -0
  75. mdq_models-0.1.0/testdata/messwert/invalid/missing_herkunft_instead_of_null.json +9 -0
  76. mdq_models-0.1.0/testdata/messwert/invalid/missing_malo.json +9 -0
  77. mdq_models-0.1.0/testdata/messwert/invalid/missing_profiltyp.json +9 -0
  78. mdq_models-0.1.0/testdata/messwert/invalid/missing_resolution.json +9 -0
  79. mdq_models-0.1.0/testdata/messwert/invalid/missing_richtung.json +9 -0
  80. mdq_models-0.1.0/testdata/messwert/invalid/missing_status.json +9 -0
  81. mdq_models-0.1.0/testdata/messwert/invalid/missing_timestamp.json +9 -0
  82. mdq_models-0.1.0/testdata/messwert/invalid/missing_value.json +9 -0
  83. mdq_models-0.1.0/testdata/messwert/invalid/missing_value_instead_of_null.json +9 -0
  84. mdq_models-0.1.0/testdata/messwert/invalid/profiltyp_capitalised.json +10 -0
  85. mdq_models-0.1.0/testdata/messwert/invalid/profiltyp_empty_string.json +10 -0
  86. mdq_models-0.1.0/testdata/messwert/invalid/profiltyp_null.json +10 -0
  87. mdq_models-0.1.0/testdata/messwert/invalid/profiltyp_unknown_value.json +10 -0
  88. mdq_models-0.1.0/testdata/messwert/invalid/profiltyp_upper_case.json +10 -0
  89. mdq_models-0.1.0/testdata/messwert/invalid/resolution_15.json +10 -0
  90. mdq_models-0.1.0/testdata/messwert/invalid/resolution_empty.json +10 -0
  91. mdq_models-0.1.0/testdata/messwert/invalid/resolution_lower_case.json +10 -0
  92. mdq_models-0.1.0/testdata/messwert/invalid/resolution_mixed_case.json +10 -0
  93. mdq_models-0.1.0/testdata/messwert/invalid/resolution_null.json +10 -0
  94. mdq_models-0.1.0/testdata/messwert/invalid/resolution_number.json +10 -0
  95. mdq_models-0.1.0/testdata/messwert/invalid/resolution_other_iso_form.json +10 -0
  96. mdq_models-0.1.0/testdata/messwert/invalid/resolution_pt1h.json +10 -0
  97. mdq_models-0.1.0/testdata/messwert/invalid/resolution_pt5m.json +10 -0
  98. mdq_models-0.1.0/testdata/messwert/invalid/richtung_capitalised.json +10 -0
  99. mdq_models-0.1.0/testdata/messwert/invalid/richtung_empty_string.json +10 -0
  100. mdq_models-0.1.0/testdata/messwert/invalid/richtung_null.json +10 -0
  101. mdq_models-0.1.0/testdata/messwert/invalid/richtung_unknown_value.json +10 -0
  102. mdq_models-0.1.0/testdata/messwert/invalid/richtung_upper_case.json +10 -0
  103. mdq_models-0.1.0/testdata/messwert/invalid/status_capitalised.json +10 -0
  104. mdq_models-0.1.0/testdata/messwert/invalid/status_empty_string.json +10 -0
  105. mdq_models-0.1.0/testdata/messwert/invalid/status_gemessen_retired.json +10 -0
  106. mdq_models-0.1.0/testdata/messwert/invalid/status_null.json +10 -0
  107. mdq_models-0.1.0/testdata/messwert/invalid/status_number.json +10 -0
  108. mdq_models-0.1.0/testdata/messwert/invalid/status_unknown_value.json +10 -0
  109. mdq_models-0.1.0/testdata/messwert/invalid/status_upper_case.json +10 -0
  110. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_1900_not_a_leap_year.json +10 -0
  111. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_date_only.json +10 -0
  112. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_empty.json +10 -0
  113. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_fractional_seconds.json +10 -0
  114. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_full_width_digits.json +10 -0
  115. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_hour_24.json +10 -0
  116. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_impossible_date.json +10 -0
  117. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_leap_second.json +10 -0
  118. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_lower_case_t.json +10 -0
  119. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_lower_case_z.json +10 -0
  120. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_not_a_leap_day.json +10 -0
  121. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_null.json +10 -0
  122. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_number.json +10 -0
  123. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_off_raster_minute_07.json +10 -0
  124. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_off_raster_minute_16.json +10 -0
  125. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_off_raster_seconds_30.json +10 -0
  126. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_offset_plus_00_00.json +10 -0
  127. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_offset_plus_01_00.json +10 -0
  128. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_space_instead_of_t.json +10 -0
  129. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_trailing_newline.json +10 -0
  130. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_without_offset.json +10 -0
  131. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_without_seconds.json +10 -0
  132. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_year_1899.json +10 -0
  133. mdq_models-0.1.0/testdata/messwert/invalid/timestamp_zero_fractional_seconds.json +10 -0
  134. mdq_models-0.1.0/testdata/messwert/invalid/top_level_array.json +1 -0
  135. mdq_models-0.1.0/testdata/messwert/invalid/top_level_null.json +1 -0
  136. mdq_models-0.1.0/testdata/messwert/invalid/top_level_string.json +1 -0
  137. mdq_models-0.1.0/testdata/messwert/invalid/unknown_key.json +11 -0
  138. mdq_models-0.1.0/testdata/messwert/invalid/utf16_encoded.json +0 -0
  139. mdq_models-0.1.0/testdata/messwert/invalid/utf8_bom.json +1 -0
  140. mdq_models-0.1.0/testdata/messwert/invalid/value_arabic_indic_digit.json +10 -0
  141. mdq_models-0.1.0/testdata/messwert/invalid/value_comma.json +10 -0
  142. mdq_models-0.1.0/testdata/messwert/invalid/value_empty.json +10 -0
  143. mdq_models-0.1.0/testdata/messwert/invalid/value_exponent.json +10 -0
  144. mdq_models-0.1.0/testdata/messwert/invalid/value_huge_json_number.json +10 -0
  145. mdq_models-0.1.0/testdata/messwert/invalid/value_infinity.json +10 -0
  146. mdq_models-0.1.0/testdata/messwert/invalid/value_json_boolean.json +10 -0
  147. mdq_models-0.1.0/testdata/messwert/invalid/value_json_integer.json +10 -0
  148. mdq_models-0.1.0/testdata/messwert/invalid/value_json_number.json +10 -0
  149. mdq_models-0.1.0/testdata/messwert/invalid/value_leading_space.json +10 -0
  150. mdq_models-0.1.0/testdata/messwert/invalid/value_leading_zero.json +10 -0
  151. mdq_models-0.1.0/testdata/messwert/invalid/value_leading_zeros.json +10 -0
  152. mdq_models-0.1.0/testdata/messwert/invalid/value_more_than_12_integer_digits.json +10 -0
  153. mdq_models-0.1.0/testdata/messwert/invalid/value_nan.json +10 -0
  154. mdq_models-0.1.0/testdata/messwert/invalid/value_negative_infinity.json +10 -0
  155. mdq_models-0.1.0/testdata/messwert/invalid/value_negative_more_than_12_integer_digits.json +10 -0
  156. mdq_models-0.1.0/testdata/messwert/invalid/value_negative_zero.json +10 -0
  157. mdq_models-0.1.0/testdata/messwert/invalid/value_no_integer_digit.json +10 -0
  158. mdq_models-0.1.0/testdata/messwert/invalid/value_plus_sign.json +10 -0
  159. mdq_models-0.1.0/testdata/messwert/invalid/value_seven_decimals.json +10 -0
  160. mdq_models-0.1.0/testdata/messwert/invalid/value_thirteen_integer_digits.json +10 -0
  161. mdq_models-0.1.0/testdata/messwert/invalid/value_trailing_newline.json +10 -0
  162. mdq_models-0.1.0/testdata/messwert/invalid/value_trailing_point.json +10 -0
  163. mdq_models-0.1.0/testdata/messwert/invalid/value_trailing_zero.json +10 -0
  164. mdq_models-0.1.0/testdata/messwert/invalid/value_trailing_zero_fraction.json +10 -0
  165. mdq_models-0.1.0/testdata/messwert/invalid/value_two_points.json +10 -0
  166. mdq_models-0.1.0/testdata/messwert/invalid/value_underscore_separator.json +10 -0
  167. mdq_models-0.1.0/testdata/messwert/invalid/value_upper_case_exponent.json +10 -0
  168. mdq_models-0.1.0/testdata/messwert/invalid/value_zero_with_trailing_zero.json +10 -0
  169. mdq_models-0.1.0/testdata/messwert/invalid/vorlaeufig_with_herkunft_null.json +10 -0
  170. mdq_models-0.1.0/testdata/messwert/invalid/vorlaeufig_with_value_null.json +10 -0
  171. mdq_models-0.1.0/testdata/messwert/invalid/wahr_with_herkunft_mqa.json +10 -0
  172. mdq_models-0.1.0/testdata/messwert/invalid/wahr_with_herkunft_null.json +10 -0
  173. mdq_models-0.1.0/testdata/messwert/invalid/wahr_with_value_null.json +10 -0
  174. mdq_models-0.1.0/testdata/messwert/valid/dst_autumn_change_day_2026_10_25.json +10 -0
  175. mdq_models-0.1.0/testdata/messwert/valid/dst_autumn_change_day_2026_10_25_first_quarter_hour.json +10 -0
  176. mdq_models-0.1.0/testdata/messwert/valid/dst_autumn_change_day_2026_10_25_last_quarter_hour.json +10 -0
  177. mdq_models-0.1.0/testdata/messwert/valid/dst_autumn_change_day_repeated_hour.json +10 -0
  178. mdq_models-0.1.0/testdata/messwert/valid/dst_spring_change_day_2026_03_29.json +10 -0
  179. mdq_models-0.1.0/testdata/messwert/valid/dst_spring_change_day_2026_03_29_first_quarter_hour.json +10 -0
  180. mdq_models-0.1.0/testdata/messwert/valid/dst_spring_change_day_2026_03_29_last_quarter_hour.json +10 -0
  181. mdq_models-0.1.0/testdata/messwert/valid/dst_spring_change_day_first_summer_time_slot.json +10 -0
  182. mdq_models-0.1.0/testdata/messwert/valid/ersatzwert_fremd.json +10 -0
  183. mdq_models-0.1.0/testdata/messwert/valid/ersatzwert_mqa.json +10 -0
  184. mdq_models-0.1.0/testdata/messwert/valid/fehlend_value_and_herkunft_null.json +10 -0
  185. mdq_models-0.1.0/testdata/messwert/valid/key_order_reversed.json +10 -0
  186. mdq_models-0.1.0/testdata/messwert/valid/malo_52291008369.json +10 -0
  187. mdq_models-0.1.0/testdata/messwert/valid/malo_80167773998.json +10 -0
  188. mdq_models-0.1.0/testdata/messwert/valid/profiltyp_gewerbe.json +10 -0
  189. mdq_models-0.1.0/testdata/messwert/valid/profiltyp_haushalt.json +10 -0
  190. mdq_models-0.1.0/testdata/messwert/valid/profiltyp_industrie.json +10 -0
  191. mdq_models-0.1.0/testdata/messwert/valid/profiltyp_unbekannt.json +10 -0
  192. mdq_models-0.1.0/testdata/messwert/valid/richtung_bezug.json +10 -0
  193. mdq_models-0.1.0/testdata/messwert/valid/richtung_einspeisung.json +10 -0
  194. mdq_models-0.1.0/testdata/messwert/valid/timestamp_leap_day.json +10 -0
  195. mdq_models-0.1.0/testdata/messwert/valid/timestamp_leap_day_2000_02_29.json +10 -0
  196. mdq_models-0.1.0/testdata/messwert/valid/timestamp_lower_year_bound_1900.json +10 -0
  197. mdq_models-0.1.0/testdata/messwert/valid/timestamp_upper_year_bound_9999.json +10 -0
  198. mdq_models-0.1.0/testdata/messwert/valid/value_boundary_max_12_integer_digits.json +10 -0
  199. mdq_models-0.1.0/testdata/messwert/valid/value_fractional.json +10 -0
  200. mdq_models-0.1.0/testdata/messwert/valid/value_large.json +10 -0
  201. mdq_models-0.1.0/testdata/messwert/valid/value_negative.json +10 -0
  202. mdq_models-0.1.0/testdata/messwert/valid/value_negative_boundary_max_12_integer_digits.json +10 -0
  203. mdq_models-0.1.0/testdata/messwert/valid/value_negative_fraction.json +10 -0
  204. mdq_models-0.1.0/testdata/messwert/valid/value_one_million.json +10 -0
  205. mdq_models-0.1.0/testdata/messwert/valid/value_smallest_fraction.json +10 -0
  206. mdq_models-0.1.0/testdata/messwert/valid/value_zero.json +10 -0
  207. mdq_models-0.1.0/testdata/messwert/valid/vorlaeufig_fremd.json +10 -0
  208. mdq_models-0.1.0/testdata/messwert/valid/vorlaeufig_mqa.json +10 -0
  209. mdq_models-0.1.0/testdata/messwert/valid/wahr_fremd.json +10 -0
  210. mdq_models-0.1.0/testdata/proto_enums.json +82 -0
  211. mdq_models-0.1.0/testdata/proto_fields.json +17 -0
  212. mdq_models-0.1.0/testdata/serialized.golden +56 -0
  213. mdq_models-0.1.0/testdata/zaehlerstand/invalid/duplicate_key.json +6 -0
  214. mdq_models-0.1.0/testdata/zaehlerstand/invalid/invalid_utf8_bytes.json +1 -0
  215. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_10_digits.json +5 -0
  216. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_12_digits.json +5 -0
  217. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_arabic_indic_digits.json +5 -0
  218. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_empty.json +5 -0
  219. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_leading_zero.json +5 -0
  220. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_letters.json +5 -0
  221. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_non_ascii_digits.json +5 -0
  222. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_null.json +5 -0
  223. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_number.json +5 -0
  224. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_with_space.json +5 -0
  225. mdq_models-0.1.0/testdata/zaehlerstand/invalid/malo_wrong_check_digit.json +5 -0
  226. mdq_models-0.1.0/testdata/zaehlerstand/invalid/missing_malo.json +4 -0
  227. mdq_models-0.1.0/testdata/zaehlerstand/invalid/missing_timestamp.json +4 -0
  228. mdq_models-0.1.0/testdata/zaehlerstand/invalid/missing_value.json +4 -0
  229. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_1900_not_a_leap_year.json +5 -0
  230. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_date_only.json +5 -0
  231. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_empty.json +5 -0
  232. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_fractional_seconds.json +5 -0
  233. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_full_width_digits.json +5 -0
  234. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_hour_24.json +5 -0
  235. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_impossible_date.json +5 -0
  236. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_leap_second.json +5 -0
  237. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_lower_case_t.json +5 -0
  238. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_lower_case_z.json +5 -0
  239. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_not_a_leap_day.json +5 -0
  240. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_null.json +5 -0
  241. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_number.json +5 -0
  242. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_offset_plus_00_00.json +5 -0
  243. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_offset_plus_00_00_off_raster.json +5 -0
  244. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_offset_plus_01_00.json +5 -0
  245. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_space_instead_of_t.json +5 -0
  246. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_trailing_newline.json +5 -0
  247. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_without_offset.json +5 -0
  248. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_without_seconds.json +5 -0
  249. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_year_1899.json +5 -0
  250. mdq_models-0.1.0/testdata/zaehlerstand/invalid/timestamp_zero_fractional_seconds.json +5 -0
  251. mdq_models-0.1.0/testdata/zaehlerstand/invalid/unknown_key.json +6 -0
  252. mdq_models-0.1.0/testdata/zaehlerstand/invalid/unknown_key_resolution.json +6 -0
  253. mdq_models-0.1.0/testdata/zaehlerstand/invalid/utf16_encoded.json +0 -0
  254. mdq_models-0.1.0/testdata/zaehlerstand/invalid/utf8_bom.json +1 -0
  255. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_arabic_indic_digit.json +5 -0
  256. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_comma.json +5 -0
  257. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_empty.json +5 -0
  258. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_exponent.json +5 -0
  259. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_huge_json_number.json +5 -0
  260. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_infinity.json +5 -0
  261. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_json_integer.json +5 -0
  262. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_json_number.json +5 -0
  263. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_leading_space.json +5 -0
  264. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_leading_zero.json +5 -0
  265. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_leading_zeros.json +5 -0
  266. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_more_than_12_integer_digits.json +5 -0
  267. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_nan.json +5 -0
  268. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_negative_infinity.json +5 -0
  269. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_negative_zero.json +5 -0
  270. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_no_integer_digit.json +5 -0
  271. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_null.json +5 -0
  272. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_plus_sign.json +5 -0
  273. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_seven_decimals.json +5 -0
  274. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_thirteen_integer_digits.json +5 -0
  275. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_trailing_newline.json +5 -0
  276. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_trailing_point.json +5 -0
  277. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_trailing_zero.json +5 -0
  278. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_trailing_zero_fraction.json +5 -0
  279. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_two_points.json +5 -0
  280. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_underscore_separator.json +5 -0
  281. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_upper_case_exponent.json +5 -0
  282. mdq_models-0.1.0/testdata/zaehlerstand/invalid/value_zero_with_trailing_zero.json +5 -0
  283. mdq_models-0.1.0/testdata/zaehlerstand/valid/basic.json +5 -0
  284. mdq_models-0.1.0/testdata/zaehlerstand/valid/dst_autumn_change_day_2026_10_25.json +5 -0
  285. mdq_models-0.1.0/testdata/zaehlerstand/valid/dst_spring_change_day_2026_03_29.json +5 -0
  286. mdq_models-0.1.0/testdata/zaehlerstand/valid/key_order_reversed.json +5 -0
  287. mdq_models-0.1.0/testdata/zaehlerstand/valid/malo_other_valid_id.json +5 -0
  288. mdq_models-0.1.0/testdata/zaehlerstand/valid/timestamp_any_second.json +5 -0
  289. mdq_models-0.1.0/testdata/zaehlerstand/valid/timestamp_leap_day_2000_02_29.json +5 -0
  290. mdq_models-0.1.0/testdata/zaehlerstand/valid/timestamp_lower_year_bound_1900.json +5 -0
  291. mdq_models-0.1.0/testdata/zaehlerstand/valid/timestamp_midnight.json +5 -0
  292. mdq_models-0.1.0/testdata/zaehlerstand/valid/timestamp_on_raster.json +5 -0
  293. mdq_models-0.1.0/testdata/zaehlerstand/valid/timestamp_upper_year_bound_9999.json +5 -0
  294. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_boundary_max_12_integer_digits.json +5 -0
  295. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_fractional.json +5 -0
  296. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_large.json +5 -0
  297. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_negative.json +5 -0
  298. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_negative_boundary_max_12_integer_digits.json +5 -0
  299. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_negative_fraction.json +5 -0
  300. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_one_million.json +5 -0
  301. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_smallest_fraction.json +5 -0
  302. mdq_models-0.1.0/testdata/zaehlerstand/valid/value_zero.json +5 -0
  303. mdq_models-0.1.0/uv.lock +787 -0
  304. mdq_models-0.1.0/zaehlerstand.go +94 -0
@@ -0,0 +1,12 @@
1
+ * text=auto eol=lf
2
+ *.go text eol=lf
3
+ *.py text eol=lf
4
+ *.json text eol=lf
5
+ *.proto text eol=lf
6
+ *.md text eol=lf
7
+ *.yml text eol=lf
8
+ *.golden text eol=lf
9
+ # the encoding fixtures contain a BOM, UTF-16 and invalid UTF-8: git must not touch their bytes
10
+ testdata/*/invalid/utf8_bom.json -text
11
+ testdata/*/invalid/utf16_encoded.json -text
12
+ testdata/*/invalid/invalid_utf8_bytes.json -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
+ # The linter runs on Go 1.27.x, the version named in go.mod.
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,30 @@
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
+ strategy:
15
+ matrix:
16
+ # Only supported Go releases; matching go.mod: 1.27.0 (1.23 is end of life)
17
+ go-version: ["1.27.x"]
18
+ steps:
19
+ - name: Install Go
20
+ uses: actions/setup-go@v7
21
+ with:
22
+ go-version: ${{ matrix.go-version }}
23
+ - name: Checkout code
24
+ uses: actions/checkout@v7
25
+ - name: Vet
26
+ run: go vet ./...
27
+ - name: Test
28
+ run: go test -race -covermode=atomic -coverprofile=coverage.out ./...
29
+ - name: Coverage
30
+ run: go tool cover -func=coverage.out | tail -1
@@ -0,0 +1,40 @@
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
21
+ - name: Check that the wheel has no top-level modules
22
+ run: |
23
+ unzip -Z1 dist/*.whl
24
+ if unzip -Z1 dist/*.whl | grep -E '\.py$' | grep -v '^mdq_models/'; then
25
+ echo "the wheel contains .py files outside mdq_models/" >&2
26
+ exit 1
27
+ fi
28
+ - name: Install the wheel into a fresh venv and import it
29
+ run: |
30
+ uv venv "$RUNNER_TEMP/wheel-venv"
31
+ uv pip install --python "$RUNNER_TEMP/wheel-venv" dist/*.whl
32
+ cd "$RUNNER_TEMP"
33
+ "$RUNNER_TEMP/wheel-venv/bin/python" -c "import mdq_models as m; assert m.__version__; print(m.__version__)"
34
+ - name: Install the wheel with the polars extra and import the Polars module
35
+ run: |
36
+ uv venv "$RUNNER_TEMP/wheel-venv-polars"
37
+ wheel=$(ls dist/*.whl)
38
+ uv pip install --python "$RUNNER_TEMP/wheel-venv-polars" "${wheel}[polars]"
39
+ cd "$RUNNER_TEMP"
40
+ "$RUNNER_TEMP/wheel-venv-polars/bin/python" -c "import mdq_models.polars as p; print(len(p.CANONICAL_COLUMNS))"
@@ -0,0 +1,63 @@
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
+ with:
17
+ fetch-depth: 0 # buf breaking compares against the PR base branch in the local clone
18
+ # `buf build` fails on a module without any .proto file ("had no .proto files"), so the buf
19
+ # steps are guarded instead of committing a placeholder: the job stays green (and the status
20
+ # check keeps existing) until the first .proto file lands, and enforces everything afterwards.
21
+ - name: Detect .proto files
22
+ id: detect
23
+ env:
24
+ BASE_REF: ${{ github.base_ref }}
25
+ run: |
26
+ if [ -n "$(find proto -name '*.proto' 2>/dev/null)" ]; then
27
+ echo "present=true" >> "$GITHUB_OUTPUT"
28
+ elif [ -n "$BASE_REF" ] && git ls-tree -r --name-only "origin/$BASE_REF" | grep -q '\.proto$'; then
29
+ echo "origin/$BASE_REF has .proto files but this PR has none: refusing to skip buf." >&2
30
+ exit 1
31
+ else
32
+ echo "present=false" >> "$GITHUB_OUTPUT"
33
+ echo "No .proto files yet, skipping buf."
34
+ fi
35
+ - name: Install Go
36
+ if: steps.detect.outputs.present == 'true'
37
+ uses: actions/setup-go@v7
38
+ with:
39
+ go-version: "1.27.x"
40
+ - name: Install buf
41
+ if: steps.detect.outputs.present == 'true'
42
+ run: go install github.com/bufbuild/buf/cmd/buf@v1.75.0
43
+ - name: buf build
44
+ if: steps.detect.outputs.present == 'true'
45
+ run: buf build
46
+ - name: buf lint
47
+ if: steps.detect.outputs.present == 'true'
48
+ run: buf lint
49
+ - name: buf format
50
+ if: steps.detect.outputs.present == 'true'
51
+ run: buf format --diff --exit-code
52
+ # Compare against the local clone (works without network access to the remote). Only on pull
53
+ # requests, and skipped while the base branch has no .proto files: there is nothing to compare to.
54
+ - name: buf breaking
55
+ if: steps.detect.outputs.present == 'true' && github.event_name == 'pull_request'
56
+ env:
57
+ BASE_REF: ${{ github.base_ref }}
58
+ run: |
59
+ if git ls-tree -r --name-only "origin/$BASE_REF" | grep -q '\.proto$'; then
60
+ buf breaking --against ".git#branch=origin/$BASE_REF"
61
+ else
62
+ echo "origin/$BASE_REF has no .proto files yet, skipping buf breaking"
63
+ fi
@@ -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 --locked --group coverage --extra polars
19
+ - name: Run Tests and Record Coverage
20
+ run: |
21
+ uv run --locked --extra polars coverage run -m pytest unittests
22
+ uv run --locked --extra polars coverage html --omit 'unittests/*,.venv/*'
23
+ uv run --locked --extra polars 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 --locked --group linting
27
+ - name: ${{ matrix.tool }}
28
+ run: uv run --locked ${{ matrix.command }}
@@ -0,0 +1,36 @@
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 --locked ruff check src/mdq_models unittests
18
+ - linter-env: type_check
19
+ extras: "--extra polars" # mypy needs polars to check mdq_models.polars
20
+ commands: |
21
+ uv run --locked --extra polars mypy --show-error-codes src/mdq_models --strict
22
+ uv run --locked --extra polars mypy --show-error-codes unittests --strict
23
+ - linter-env: spell_check
24
+ commands: |
25
+ uv run --locked codespell --ignore-words=domain-specific-terms.txt src
26
+ uv run --locked codespell --ignore-words=domain-specific-terms.txt unittests README.md doc.go
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+ - name: Install uv
30
+ uses: astral-sh/setup-uv@v10.3.2
31
+ with:
32
+ python-version: "3.15"
33
+ - name: Install Dependencies
34
+ run: uv sync --locked --group ${{ matrix.linter-env }} ${{ matrix.extras }}
35
+ - name: Run ${{ matrix.linter-env }}
36
+ run: ${{ matrix.commands }}
@@ -0,0 +1,58 @@
1
+ name: "Publish to PyPI"
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ tests:
12
+ if: startsWith(github.ref, 'refs/tags/v')
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ - name: Install uv
17
+ uses: astral-sh/setup-uv@v10.3.2
18
+ with:
19
+ python-version: "3.15"
20
+ - name: Install dependencies
21
+ run: uv sync --locked --group tests --group linting --group coverage --group type_check
22
+ - name: Run tests
23
+ run: uv run --locked pytest unittests
24
+ - name: Run tests with the polars extra
25
+ run: uv run --locked --extra polars pytest unittests
26
+ - name: Run linting
27
+ run: |
28
+ uv run --locked ruff check src/mdq_models unittests
29
+ - name: Run type_check
30
+ run: |
31
+ uv run --locked mypy --show-error-codes src/mdq_models --strict
32
+ uv run --locked mypy --show-error-codes unittests --strict
33
+ - name: Run coverage
34
+ run: |
35
+ uv run --locked coverage run -m pytest unittests
36
+ uv run --locked coverage report --fail-under 80 --omit 'unittests/*,.venv/*'
37
+
38
+ build-n-publish:
39
+ name: Build and publish Python distributions to PyPI
40
+ runs-on: ubuntu-latest
41
+ environment: release
42
+ permissions:
43
+ contents: read # job-level permissions replace the workflow-level ones; checkout needs this
44
+ id-token: write
45
+ needs: tests
46
+ steps:
47
+ - uses: actions/checkout@v7
48
+ with:
49
+ fetch-depth: 0 # hatch-vcs derives the package version from the git tag
50
+ - name: Install uv
51
+ uses: astral-sh/setup-uv@v10.3.2
52
+ with:
53
+ python-version: "3.15"
54
+ - name: Build wheel and source distributions
55
+ run: uv build
56
+ - name: Publish distribution to PyPI
57
+ if: startsWith(github.ref, 'refs/tags/v')
58
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,26 @@
1
+ name: "Python Tests"
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request: {}
7
+
8
+ jobs:
9
+ pytest:
10
+ name: "pytest (py${{ matrix.python-version }} ${{ matrix.extras || 'no extras' }})"
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
15
+ os: [ubuntu-latest]
16
+ extras: ["", "--extra polars"] # once without and once with the optional polars extra
17
+ steps:
18
+ - uses: actions/checkout@v7
19
+ - name: Install uv
20
+ uses: astral-sh/setup-uv@v10.3.2
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - name: Install Dependencies
24
+ run: uv sync --locked --group tests ${{ matrix.extras }}
25
+ - name: Run the Unit Tests
26
+ run: uv run --locked ${{ matrix.extras }} 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/mdq_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,19 @@
1
+ version: "2"
2
+ linters:
3
+ default: standard
4
+ enable:
5
+ - revive
6
+ settings:
7
+ revive:
8
+ enable-default-rules: false
9
+ rules:
10
+ - name: exported
11
+ arguments:
12
+ - checkPrivateReceivers
13
+ - sayRepetitiveInsteadOfStutters
14
+ - name: package-comments
15
+ exclusions:
16
+ rules:
17
+ - path: _test\.go
18
+ linters:
19
+ - revive
@@ -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.