glean-parser 17.0.1__tar.gz → 17.2.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 (181) hide show
  1. glean_parser-17.0.1/docs/readme.md → glean_parser-17.2.0/PKG-INFO +30 -11
  2. {glean_parser-17.0.1 → glean_parser-17.2.0}/README.md +1 -11
  3. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/lint.py +28 -9
  4. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/metrics.py +56 -0
  5. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/parser.py +70 -30
  6. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/rust.py +2 -0
  7. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/schemas/metrics.2-0-0.schema.yaml +65 -11
  8. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/rust.jinja2 +15 -2
  9. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/util.py +9 -1
  10. glean_parser-17.2.0/pyproject.toml +76 -0
  11. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/all_metrics.yaml +15 -0
  12. glean_parser-17.2.0/tests/data/attribution.yaml +93 -0
  13. glean_parser-17.2.0/tests/data/bad_attribution.yamlx +36 -0
  14. glean_parser-17.2.0/tests/data/dual_labeled.yaml +96 -0
  15. glean_parser-17.2.0/tests/data/dual_labeled_invalid.yaml +72 -0
  16. glean_parser-17.2.0/tests/data/redefined_category.yamlx +32 -0
  17. glean_parser-17.2.0/tests/data/redefined_metric.yamlx +31 -0
  18. glean_parser-17.2.0/tests/data/redefined_ping.yamlx +30 -0
  19. glean_parser-17.2.0/tests/data/reserved_categories.yamlx +33 -0
  20. glean_parser-17.2.0/tests/data/same_name_different_category.yaml +32 -0
  21. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_lint.py +52 -4
  22. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_parser.py +106 -5
  23. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_rust.py +48 -0
  24. glean_parser-17.0.1/.circleci/config.yml +0 -271
  25. glean_parser-17.0.1/.editorconfig +0 -21
  26. glean_parser-17.0.1/.github/CODEOWNERS +0 -12
  27. glean_parser-17.0.1/.github/ISSUE_TEMPLATE.md +0 -15
  28. glean_parser-17.0.1/.github/dependabot.yml +0 -6
  29. glean_parser-17.0.1/.github/pull_request_template.md +0 -8
  30. glean_parser-17.0.1/.swiftlint.yml +0 -6
  31. glean_parser-17.0.1/CHANGELOG.md +0 -816
  32. glean_parser-17.0.1/CODE_OF_CONDUCT.md +0 -15
  33. glean_parser-17.0.1/CONTRIBUTING.md +0 -189
  34. glean_parser-17.0.1/MANIFEST.in +0 -14
  35. glean_parser-17.0.1/Makefile +0 -79
  36. glean_parser-17.0.1/PKG-INFO +0 -893
  37. glean_parser-17.0.1/docs/Makefile +0 -20
  38. glean_parser-17.0.1/docs/_static/glean.jpeg +0 -0
  39. glean_parser-17.0.1/docs/authors.md +0 -17
  40. glean_parser-17.0.1/docs/conf.py +0 -160
  41. glean_parser-17.0.1/docs/contributing.md +0 -189
  42. glean_parser-17.0.1/docs/history.md +0 -816
  43. glean_parser-17.0.1/docs/index.rst +0 -27
  44. glean_parser-17.0.1/docs/installation.md +0 -41
  45. glean_parser-17.0.1/docs/make.bat +0 -36
  46. glean_parser-17.0.1/docs/metrics-yaml.rst +0 -13
  47. glean_parser-17.0.1/docs/pings-yaml.rst +0 -13
  48. glean_parser-17.0.1/docs/tags-yaml.rst +0 -13
  49. glean_parser-17.0.1/glean_parser.egg-info/PKG-INFO +0 -893
  50. glean_parser-17.0.1/glean_parser.egg-info/SOURCES.txt +0 -170
  51. glean_parser-17.0.1/glean_parser.egg-info/dependency_links.txt +0 -1
  52. glean_parser-17.0.1/glean_parser.egg-info/entry_points.txt +0 -2
  53. glean_parser-17.0.1/glean_parser.egg-info/not-zip-safe +0 -1
  54. glean_parser-17.0.1/glean_parser.egg-info/requires.txt +0 -6
  55. glean_parser-17.0.1/glean_parser.egg-info/top_level.txt +0 -1
  56. glean_parser-17.0.1/pytest.ini +0 -7
  57. glean_parser-17.0.1/requirements_dev.txt +0 -13
  58. glean_parser-17.0.1/setup.cfg +0 -10
  59. glean_parser-17.0.1/setup.py +0 -77
  60. glean_parser-17.0.1/tools/extract_data_categories.py +0 -176
  61. {glean_parser-17.0.1 → glean_parser-17.2.0}/.gitignore +0 -0
  62. {glean_parser-17.0.1 → glean_parser-17.2.0}/AUTHORS.md +0 -0
  63. {glean_parser-17.0.1 → glean_parser-17.2.0}/LICENSE +0 -0
  64. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/__init__.py +0 -0
  65. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/__main__.py +0 -0
  66. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/coverage.py +0 -0
  67. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/data_review.py +0 -0
  68. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/go_server.py +0 -0
  69. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/javascript.py +0 -0
  70. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/javascript_server.py +0 -0
  71. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/kotlin.py +0 -0
  72. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/markdown.py +0 -0
  73. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/pings.py +0 -0
  74. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/python_server.py +0 -0
  75. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/ruby_server.py +0 -0
  76. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/rust_server.py +0 -0
  77. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/schemas/metrics.1-0-0.schema.yaml +0 -0
  78. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/schemas/pings.1-0-0.schema.yaml +0 -0
  79. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/schemas/pings.2-0-0.schema.yaml +0 -0
  80. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/schemas/tags.1-0-0.schema.yaml +0 -0
  81. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/swift.py +0 -0
  82. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/tags.py +0 -0
  83. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/data_review.jinja2 +0 -0
  84. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/go_server.jinja2 +0 -0
  85. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/javascript.buildinfo.jinja2 +0 -0
  86. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/javascript.jinja2 +0 -0
  87. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/javascript_server.jinja2 +0 -0
  88. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/kotlin.buildinfo.jinja2 +0 -0
  89. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/kotlin.jinja2 +0 -0
  90. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/markdown.jinja2 +0 -0
  91. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/python_server.jinja2 +0 -0
  92. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/qmldir.jinja2 +0 -0
  93. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/ruby_server.jinja2 +0 -0
  94. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/rust_server.jinja2 +0 -0
  95. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/templates/swift.jinja2 +0 -0
  96. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/translate.py +0 -0
  97. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/translation_options.py +0 -0
  98. {glean_parser-17.0.1 → glean_parser-17.2.0}/glean_parser/validate_ping.py +0 -0
  99. {glean_parser-17.0.1 → glean_parser-17.2.0}/server_telemetry/sdk-metrics-compat.yaml +0 -0
  100. {glean_parser-17.0.1 → glean_parser-17.2.0}/server_telemetry/server-side-pings.yaml +0 -0
  101. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/conftest.py +0 -0
  102. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/all_pings.yaml +0 -0
  103. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/bad_ping.yamlx +0 -0
  104. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/core.yaml +0 -0
  105. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/duplicate_labeled.yaml +0 -0
  106. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/duplicate_send_in_ping.yaml +0 -0
  107. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/empty.yaml +0 -0
  108. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/event_key_ordering.yaml +0 -0
  109. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/events_with_types.yaml +0 -0
  110. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/fxa-server-metrics.yaml +0 -0
  111. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/fxa-server-pings.yaml +0 -0
  112. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/go_server_custom_ping_only_metrics.yaml +0 -0
  113. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/go_server_custom_ping_only_pings.yaml +0 -0
  114. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/go_server_events_and_custom_ping_metrics.yaml +0 -0
  115. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/go_server_events_and_custom_ping_pings.yaml +0 -0
  116. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/go_server_events_only_metrics.yaml +0 -0
  117. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/go_server_metrics_unsupported.yaml +0 -0
  118. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/invalid-ping-names.yaml +0 -0
  119. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/invalid.yamlx +0 -0
  120. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/jwe.yaml +0 -0
  121. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/metric-with-tags.yaml +0 -0
  122. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/mixed-expirations.yaml +0 -0
  123. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/name_too_similar.yaml +0 -0
  124. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/object.yaml +0 -0
  125. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/old_event_api.yamlx +0 -0
  126. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/ordering.yaml +0 -0
  127. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/pings.yaml +0 -0
  128. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/rate.yaml +0 -0
  129. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/ruby_server_metrics_unsupported.yaml +0 -0
  130. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/ruby_server_pings_unsupported.yaml +0 -0
  131. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/rust_server_custom_ping_only_metrics.yaml +0 -0
  132. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/rust_server_custom_ping_only_pings.yaml +0 -0
  133. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/rust_server_events_and_custom_ping_metrics.yaml +0 -0
  134. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/rust_server_events_and_custom_ping_pings.yaml +0 -0
  135. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/rust_server_events_only_metrics.yaml +0 -0
  136. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/rust_server_metrics_unsupported.yaml +0 -0
  137. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/schema-violation.yaml +0 -0
  138. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/send_if_empty_with_metrics.yaml +0 -0
  139. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_custom_ping_only_compare.go +0 -0
  140. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_custom_ping_only_compare.rs +0 -0
  141. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_events_and_custom_ping_compare.go +0 -0
  142. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_events_and_custom_ping_compare.rs +0 -0
  143. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_events_compare.rb +0 -0
  144. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_events_only_compare.go +0 -0
  145. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_events_only_compare.rs +0 -0
  146. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_metrics_no_events_no_pings.yaml +0 -0
  147. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_metrics_with_event.yaml +0 -0
  148. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/server_pings.yaml +0 -0
  149. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/single_labeled.yaml +0 -0
  150. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/smaller.yaml +0 -0
  151. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/tags.yaml +0 -0
  152. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/telemetry_mirror.yaml +0 -0
  153. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/text.yaml +0 -0
  154. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/text_invalid.yaml +0 -0
  155. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/unknown_ping_used.yaml +0 -0
  156. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/wrong_key.yamlx +0 -0
  157. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/data/yaml_nits.yamlx +0 -0
  158. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/detekt.yml +0 -0
  159. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test-go/test.go.tmpl +0 -0
  160. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test-js/package.json +0 -0
  161. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test-js/test.js.tmpl +0 -0
  162. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test-py/test.py +0 -0
  163. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test-rb/test.rb.tmpl +0 -0
  164. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test-rs/test.rs.tmpl +0 -0
  165. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_cli.py +0 -0
  166. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_go_server.py +0 -0
  167. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_javascript.py +0 -0
  168. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_javascript_server.py +0 -0
  169. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_kotlin.py +0 -0
  170. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_markdown.py +0 -0
  171. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_metrics.py +0 -0
  172. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_pings.py +0 -0
  173. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_python_server.py +0 -0
  174. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_ruby_server.py +0 -0
  175. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_rust_server.py +0 -0
  176. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_swift.py +0 -0
  177. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_tags.py +0 -0
  178. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_translate.py +0 -0
  179. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_utils.py +0 -0
  180. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/test_validate_ping.py +0 -0
  181. {glean_parser-17.0.1 → glean_parser-17.2.0}/tests/util.py +0 -0
@@ -1,3 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: glean-parser
3
+ Version: 17.2.0
4
+ Summary: Parser tools for Mozilla's Glean telemetry
5
+ Project-URL: Homepage, https://mozilla.github.io/glean
6
+ Project-URL: Repository, https://github.com/mozilla/glean_parser
7
+ Project-URL: Changelog, https://github.com/mozilla/glean_parser/blob/main/CHANGELOG.md
8
+ Author-email: The Glean Team <glean-team@mozilla.com>
9
+ License-File: AUTHORS.md
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Natural Language :: English
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Requires-Python: >=3.8
22
+ Requires-Dist: click>=7
23
+ Requires-Dist: diskcache>=4
24
+ Requires-Dist: jinja2>=2.10.1
25
+ Requires-Dist: jsonschema>=3.0.2
26
+ Requires-Dist: platformdirs>=2.4.0
27
+ Requires-Dist: pyyaml>=5.3.1
28
+ Description-Content-Type: text/markdown
29
+
1
30
  # Glean Parser
2
31
 
3
32
  Parser tools for Mozilla's Glean telemetry.
@@ -16,17 +45,7 @@ code for various integrations, linting and coverage testing.
16
45
 
17
46
  ## Requirements
18
47
 
19
- - Python 3.8 (or later)
20
-
21
- The following library requirements are installed automatically when
22
- `glean_parser` is installed by `pip`.
23
-
24
- - Click
25
- - diskcache
26
- - Jinja2
27
- - jsonschema
28
- - platformdirs
29
- - PyYAML
48
+ - Python 3.8 (or later)
30
49
 
31
50
  ## Usage
32
51
 
@@ -16,17 +16,7 @@ code for various integrations, linting and coverage testing.
16
16
 
17
17
  ## Requirements
18
18
 
19
- - Python 3.8 (or later)
20
-
21
- The following library requirements are installed automatically when
22
- `glean_parser` is installed by `pip`.
23
-
24
- - Click
25
- - diskcache
26
- - Jinja2
27
- - jsonschema
28
- - platformdirs
29
- - PyYAML
19
+ - Python 3.8 (or later)
30
20
 
31
21
  ## Usage
32
22
 
@@ -134,7 +134,6 @@ def check_unit_in_name(
134
134
 
135
135
  time_unit = getattr(metric, "time_unit", None)
136
136
  memory_unit = getattr(metric, "memory_unit", None)
137
- unit = getattr(metric, "unit", None)
138
137
 
139
138
  if time_unit is not None:
140
139
  if (
@@ -175,14 +174,6 @@ def check_unit_in_name(
175
174
  "Confirm the unit is correct and only include memory_unit."
176
175
  )
177
176
 
178
- elif unit is not None:
179
- if unit_in_name == unit:
180
- yield (
181
- f"Suffix '{unit_in_name}' is redundant with unit param "
182
- f"'{unit}'. "
183
- "Only include unit."
184
- )
185
-
186
177
 
187
178
  def check_category_generic(
188
179
  category_name: str, metrics: Iterable[metrics.Metric]
@@ -581,6 +572,20 @@ def lint_metrics(
581
572
 
582
573
  nits.extend(_lint_all_objects(objs, parser_config))
583
574
 
575
+ # The information for whether there's duplicate categories found within the
576
+ # same YAML document is on the objs value, which is not presently linted
577
+ # (we pull out its metrics, pings, and tags and lint those).
578
+ # So we perform that custom work here.
579
+ if getattr(objs, "duplicate", None):
580
+ nits.append(
581
+ GlinterNit(
582
+ "REDEFINED_CATEGORY",
583
+ getattr(objs, "duplicate", ""),
584
+ f"Category redefined {objs.duplicate}", # type: ignore[attr-defined]
585
+ CheckType.error,
586
+ )
587
+ )
588
+
584
589
  for category_name, category in sorted(list(objs.items())):
585
590
  if category_name == "pings":
586
591
  nits.extend(_lint_pings(category, parser_config, valid_tag_names))
@@ -590,6 +595,20 @@ def lint_metrics(
590
595
  # currently we have no linting for tags
591
596
  continue
592
597
 
598
+ # The information for whether there's duplicate metrics found within the
599
+ # same YAML document is on the category value, which is not presently linted
600
+ # (we lint only its metrics).
601
+ # So we perform that custom work here.
602
+ if getattr(objs[category_name], "duplicate", None):
603
+ nits.append(
604
+ GlinterNit(
605
+ "REDEFINED_METRIC",
606
+ category_name,
607
+ f"Metric redefined {getattr(objs[category_name], 'duplicate', '')}",
608
+ CheckType.error,
609
+ )
610
+ )
611
+
593
612
  # Make sure the category has only Metrics, not Pings or Tags
594
613
  category_metrics = dict(
595
614
  (name, metric)
@@ -416,18 +416,23 @@ class LabeledString(Labeled, String):
416
416
  class LabeledCounter(Labeled, Counter):
417
417
  typename = "labeled_counter"
418
418
 
419
+
419
420
  class LabeledCustomDistribution(Labeled, CustomDistribution):
420
421
  typename = "labeled_custom_distribution"
421
422
 
423
+
422
424
  class LabeledMemoryDistribution(Labeled, MemoryDistribution):
423
425
  typename = "labeled_memory_distribution"
424
426
 
427
+
425
428
  class LabeledTimingDistribution(Labeled, TimingDistribution):
426
429
  typename = "labeled_timing_distribution"
427
430
 
431
+
428
432
  class LabeledQuantity(Labeled, Quantity):
429
433
  typename = "labeled_quantity"
430
434
 
435
+
431
436
  class Rate(Metric):
432
437
  typename = "rate"
433
438
 
@@ -527,4 +532,55 @@ class Object(Metric):
527
532
  return structure
528
533
 
529
534
 
535
+ class DualLabeledCounter(Metric):
536
+ typename = "dual_labeled_counter"
537
+ dual_labeled = True
538
+
539
+ def __init__(self, *args, **kwargs):
540
+ dual_labels = kwargs.pop("dual_labels", None)
541
+ if not dual_labels:
542
+ raise ValueError(
543
+ "`dual_labeled_counter` is missing required parameter `dual_labels`"
544
+ )
545
+ k = dual_labels.get("key", None)
546
+ if not k:
547
+ raise ValueError("`dual_labels` is missing required parameter `key`")
548
+ c = dual_labels.get("category", None)
549
+ if not c:
550
+ raise ValueError("`dual_labels` is missing required parameter `categories`")
551
+ keys = k.get("labels", None)
552
+ if keys is not None:
553
+ if not isinstance(keys, list) or not all(isinstance(k, str) for k in keys):
554
+ raise ValueError("key `labels` must be a list of strings")
555
+ self.ordered_keys = keys
556
+ self.keys = set([CowString(key) for key in keys])
557
+ else:
558
+ self.ordered_keys = None
559
+ self.keys = None
560
+ categories = c.get("labels", None)
561
+ if categories is not None:
562
+ if not isinstance(categories, list) or not all(
563
+ isinstance(c, str) for c in categories
564
+ ):
565
+ raise ValueError("category `labels` must be a list of strings")
566
+ self.ordered_categories = categories
567
+ self.categories = set([CowString(category) for category in categories])
568
+ else:
569
+ self.ordered_categories = None
570
+ self.categories = None
571
+ super().__init__(*args, **kwargs)
572
+
573
+ def serialize(self) -> Dict[str, util.JSONType]:
574
+ """
575
+ Serialize the metric back to JSON object model.
576
+ """
577
+ d = super().serialize()
578
+ d["keys"] = self.ordered_keys
579
+ d["categories"] = self.ordered_categories
580
+ del d["ordered_keys"]
581
+ del d["ordered_categories"]
582
+ del d["dual_labeled"]
583
+ return d
584
+
585
+
530
586
  ObjectTree = Dict[str, Dict[str, Union[Metric, pings.Ping, tags.Tag]]]
@@ -182,25 +182,42 @@ def _instantiate_metrics(
182
182
  global_tags = content.get("$tags", [])
183
183
  assert isinstance(global_tags, list)
184
184
 
185
+ # Duplicate category names end up on the root content.
186
+ if not hasattr(all_objects, "duplicate"):
187
+ setattr(all_objects, "duplicate", getattr(content, "duplicate", None))
188
+
185
189
  for category_key, category_val in sorted(content.items()):
186
190
  if category_key.startswith("$"):
187
191
  continue
188
192
  if category_key == "no_lint":
189
193
  continue
190
194
  if not config.get("allow_reserved") and category_key.split(".")[0] == "glean":
191
- yield util.format_error(
192
- filepath,
193
- f"For category '{category_key}'",
194
- "Categories beginning with 'glean' are reserved for "
195
- "Glean internal use.",
196
- )
197
- continue
195
+ if category_key not in ("glean.attribution", "glean.distribution"):
196
+ yield util.format_error(
197
+ filepath,
198
+ f"For category '{category_key}'",
199
+ "Categories beginning with 'glean' are reserved for "
200
+ "Glean internal use.",
201
+ )
202
+ continue
198
203
  all_objects.setdefault(category_key, DictWrapper())
199
204
 
200
205
  if not isinstance(category_val, dict):
201
206
  raise TypeError(f"Invalid content for {category_key}")
202
207
 
203
208
  for metric_key, metric_val in sorted(category_val.items()):
209
+ if (
210
+ not config.get("allow_reserved")
211
+ and category_key in ("glean.attribution", "glean.distribution")
212
+ and metric_key != "ext"
213
+ ):
214
+ yield util.format_error(
215
+ filepath,
216
+ f"For {category_key}.{metric_key}",
217
+ f"May only use semi-reserved category {category_key} with metric name 'ext'",
218
+ metric_val.defined_in["line"],
219
+ )
220
+ continue
204
221
  try:
205
222
  metric_obj = Metric.make_metric(
206
223
  category_key, metric_key, metric_val, validated=True, config=config
@@ -214,18 +231,28 @@ def _instantiate_metrics(
214
231
  )
215
232
  metric_obj = None
216
233
  else:
217
- if (
218
- not config.get("allow_reserved")
219
- and "all-pings" in metric_obj.send_in_pings
220
- ):
221
- yield util.format_error(
222
- filepath,
223
- f"On instance {category_key}.{metric_key}",
224
- 'Only internal metrics may specify "all-pings" '
225
- 'in "send_in_pings"',
226
- metric_val.defined_in["line"],
227
- )
228
- metric_obj = None
234
+ if not config.get("allow_reserved"):
235
+ if "all-pings" in metric_obj.send_in_pings:
236
+ yield util.format_error(
237
+ filepath,
238
+ f"On instance {category_key}.{metric_key}",
239
+ 'Only internal metrics may specify "all-pings" '
240
+ 'in "send_in_pings"',
241
+ metric_val.defined_in["line"],
242
+ )
243
+ metric_obj = None
244
+ elif (
245
+ metric_obj.identifier()
246
+ in ("glean.attribution.ext", "glean.distribution.ext")
247
+ and metric_obj.type != "object"
248
+ ):
249
+ yield util.format_error(
250
+ filepath,
251
+ f"On instance {category_key}.{metric_key}",
252
+ "Extended attribution/distribution metrics must be of type 'object'",
253
+ metric_val.defined_in["line"],
254
+ )
255
+ metric_obj = None
229
256
 
230
257
  if metric_obj is not None:
231
258
  metric_obj.no_lint = sorted(set(metric_obj.no_lint + global_no_lint))
@@ -250,6 +277,12 @@ def _instantiate_metrics(
250
277
  metric_obj.defined_in["line"],
251
278
  )
252
279
  else:
280
+ if not hasattr(all_objects[category_key], "duplicate"):
281
+ setattr(
282
+ all_objects[category_key],
283
+ "duplicate",
284
+ getattr(content[category_key], "duplicate", None),
285
+ )
253
286
  all_objects[category_key][metric_key] = metric_obj
254
287
  sources[(category_key, metric_key)] = filepath
255
288
 
@@ -269,6 +302,15 @@ def _instantiate_pings(
269
302
  assert isinstance(global_no_lint, list)
270
303
  ping_schedule_reverse_map: Dict[str, Set[str]] = dict()
271
304
 
305
+ # Duplicate ping names end up on the root content.
306
+ duplicate_ping = getattr(content, "duplicate", None)
307
+ if duplicate_ping:
308
+ yield util.format_error(
309
+ filepath,
310
+ "",
311
+ f"Duplicate ping named '{duplicate_ping}'.",
312
+ )
313
+
272
314
  for ping_key, ping_val in sorted(content.items()):
273
315
  if ping_key.startswith("$"):
274
316
  continue
@@ -321,8 +363,7 @@ def _instantiate_pings(
321
363
  yield util.format_error(
322
364
  filepath,
323
365
  "",
324
- f"Duplicate ping name '{ping_key}' "
325
- f"already defined in '{already_seen}'",
366
+ f"Duplicate ping name '{ping_key}' already defined in '{already_seen}'",
326
367
  )
327
368
  else:
328
369
  all_objects.setdefault("pings", {})[ping_key] = ping_obj
@@ -380,8 +421,7 @@ def _instantiate_tags(
380
421
  yield util.format_error(
381
422
  filepath,
382
423
  "",
383
- f"Duplicate tag name '{tag_key}' "
384
- f"already defined in '{already_seen}'",
424
+ f"Duplicate tag name '{tag_key}' already defined in '{already_seen}'",
385
425
  )
386
426
  else:
387
427
  all_objects.setdefault("tags", {})[tag_key] = tag_obj
@@ -481,16 +521,16 @@ def parse_objects(
481
521
  raise TypeError(f"Invalid content for {filepath}")
482
522
 
483
523
  for category_key, category_val in sorted(content.items()):
484
- if category_key.startswith("$"):
485
- continue
524
+ if category_key.startswith("$"):
525
+ continue
486
526
 
487
- interesting_metrics_dict.setdefault(category_key, DictWrapper())
527
+ interesting_metrics_dict.setdefault(category_key, DictWrapper())
488
528
 
489
- if not isinstance(category_val, dict):
490
- raise TypeError(f"Invalid category_val for {category_key}")
529
+ if not isinstance(category_val, dict):
530
+ raise TypeError(f"Invalid category_val for {category_key}")
491
531
 
492
- for metric_key, metric_val in sorted(category_val.items()):
493
- interesting_metrics_dict[category_key][metric_key] = metric_val
532
+ for metric_key, metric_val in sorted(category_val.items()):
533
+ interesting_metrics_dict[category_key][metric_key] = metric_val
494
534
 
495
535
  for category_key, category_val in all_objects.items():
496
536
  if category_key == "tags":
@@ -101,6 +101,8 @@ def type_name(obj):
101
101
 
102
102
  if getattr(obj, "labeled", False):
103
103
  return "LabeledMetric<{}>".format(class_name(obj.type))
104
+ if getattr(obj, "dual_labeled_counter", False):
105
+ return "DualLabeledCounterMetric"
104
106
  generate_enums = getattr(obj, "_generate_enums", []) # Extra Keys? Reasons?
105
107
  if len(generate_enums):
106
108
  generic = None
@@ -52,7 +52,16 @@ definitions:
52
52
  labeled_metric_id:
53
53
  type: string
54
54
  pattern: "^[ -~]+$"
55
- maxLength: 71 # Note: this should be category + metric + 1
55
+ maxLength: 111 # Note: this should be category + metric + 1
56
+
57
+ optional_labels:
58
+ anyOf:
59
+ - type: array
60
+ uniqueItems: true
61
+ items:
62
+ $ref: "#/definitions/labeled_metric_id"
63
+ maxItems: 4096
64
+ - type: "null"
56
65
 
57
66
  metric:
58
67
  description: |
@@ -121,14 +130,20 @@ definitions:
121
130
  metric types include:
122
131
 
123
132
  `labeled_boolean`, `labeled_string`, `labeled_counter`,
124
- `labeled_custom_distribution`, `labeled_memory_distribution`,
125
- `labeled_timing_distribution`, `labeled_quantity`.
133
+ `dual_labeled_counter`, `labeled_custom_distribution`,
134
+ `labeled_memory_distribution`, `labeled_timing_distribution`,
135
+ `labeled_quantity`.
126
136
 
127
137
  - `text`: Record long text data.
128
138
 
129
139
  - `object`: Record structured data based on a pre-defined schema
130
140
  Additional properties: `structure`.
131
141
 
142
+ - `dual_labeled_counter`: A counter with two label dimensions.
143
+ This metric type is used to record a counter with two different
144
+ label axes, such as a key and a category. Additional properties:
145
+ `dual_labels`.
146
+
132
147
  type: string
133
148
  enum:
134
149
  - event
@@ -155,6 +170,7 @@ definitions:
155
170
  - rate
156
171
  - text
157
172
  - object
173
+ - dual_labeled_counter
158
174
 
159
175
  description:
160
176
  title: Description
@@ -370,13 +386,51 @@ definitions:
370
386
  labels in the special label `__other__`.
371
387
 
372
388
  Valid with any of the labeled metric types.
373
- anyOf:
374
- - type: array
375
- uniqueItems: true
376
- items:
377
- $ref: "#/definitions/labeled_metric_id"
378
- maxItems: 4096
379
- - type: "null"
389
+ $ref: "#/definitions/optional_labels"
390
+
391
+ dual_labels:
392
+ type: object
393
+ description: Defines the two label dimensions for a dual-labeled metric.
394
+ properties:
395
+ key:
396
+ type: object
397
+ description: The primary label dimension.
398
+ properties:
399
+ description:
400
+ type: string
401
+ description: |
402
+ Human-readable description of the key dimension (first label
403
+ axis).
404
+ labels:
405
+ description: |
406
+ Optional list of statically defined label values for the key
407
+ dimension.
408
+ $ref: "#/definitions/optional_labels"
409
+ required:
410
+ - description
411
+ additionalProperties: false
412
+ category:
413
+ type: object
414
+ description: The secondary label dimension.
415
+ properties:
416
+ description:
417
+ type: string
418
+ description: |
419
+ Human-readable description of the category dimension (second
420
+ label axis).
421
+ labels:
422
+ description: |
423
+ Optional list of statically defined label values for the
424
+ category dimension.
425
+ $ref: "#/definitions/optional_labels"
426
+ required:
427
+ - description
428
+ additionalProperties: false
429
+ required:
430
+ - key
431
+ - category
432
+ additionalProperties: false
433
+
380
434
 
381
435
  extra_keys:
382
436
  title: Extra keys
@@ -565,7 +619,7 @@ definitions:
565
619
  Use is limited to Firefox Desktop only.
566
620
  Has no effect when used with non-FOG outputters.
567
621
  See FOG's documentation on mirroring for details -
568
- https://firefox-source-docs.mozilla.org/toolkit/components/glean/mirroring.html
622
+ https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/gifft.html
569
623
  type: string
570
624
  minLength: 6
571
625
 
@@ -89,7 +89,7 @@ CommonMetricData {
89
89
  disabled: {{ obj.is_disabled()|rust }},
90
90
  ..Default::default()
91
91
  }
92
- {% endmacro %}
92
+ {%- endmacro -%}
93
93
  {% for category in categories %}
94
94
  {% if category.contains_pings %}
95
95
  {% for obj in category.objs.values() %}
@@ -150,7 +150,7 @@ pub mod {{ category.name|snake_case }} {
150
150
  {%- for arg_name in extra_metric_args if not obj.labeled and obj[arg_name] is defined and arg_name != 'allowed_extra_keys' -%}
151
151
  , {{ obj[arg_name]|rust }}
152
152
  {%- endfor -%}
153
- {{ ", " if obj.labeled else ")\n" }}
153
+ {{ ", " if obj.labeled or obj.dual_labeled else ")\n" }}
154
154
  {%- if obj.labeled -%}
155
155
  {%- if obj.labels -%}
156
156
  Some({{ obj.labels|rust }})
@@ -158,6 +158,19 @@ pub mod {{ category.name|snake_case }} {
158
158
  None
159
159
  {%- endif -%})
160
160
  {% endif %}
161
+ {%- if obj.dual_labeled -%}
162
+ {%- if obj.keys -%}
163
+ Some({{ obj.keys|rust }})
164
+ {%- else -%}
165
+ None
166
+ {%- endif -%}
167
+ {{ ", " }}
168
+ {%- if obj.categories -%}
169
+ Some({{ obj.categories|rust }})
170
+ {%- else -%}
171
+ None
172
+ {%- endif -%})
173
+ {% endif %}
161
174
  });
162
175
  {% endfor %}
163
176
  }
@@ -88,7 +88,15 @@ def yaml_load(stream):
88
88
 
89
89
  def _construct_mapping_adding_line(loader, node):
90
90
  loader.flatten_mapping(node)
91
- mapping = DictWrapper(loader.construct_pairs(node))
91
+ pairs = loader.construct_pairs(node)
92
+
93
+ # Redefinition of a key might be a mistake if that key is a metric name.
94
+ mapping = DictWrapper()
95
+ for pair in pairs:
96
+ if pair[0] in mapping:
97
+ mapping.duplicate = pair[0]
98
+ mapping[pair[0]] = pair[1]
99
+
92
100
  mapping.defined_in = {"line": node.start_mark.line}
93
101
  return mapping
94
102
 
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "glean-parser"
3
+ dynamic = ["version"]
4
+ authors = [
5
+ { name = "The Glean Team", email = "glean-team@mozilla.com" }
6
+ ]
7
+ description = "Parser tools for Mozilla's Glean telemetry"
8
+ readme = "README.md"
9
+ classifiers = [
10
+ "Development Status :: 5 - Production/Stable",
11
+ "Intended Audience :: Developers",
12
+ "Natural Language :: English",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.8",
15
+ "Programming Language :: Python :: 3.9",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ ]
21
+ requires-python = ">=3.8"
22
+ dependencies = [
23
+ "Click>=7",
24
+ "diskcache>=4",
25
+ "Jinja2>=2.10.1",
26
+ "jsonschema>=3.0.2",
27
+ "platformdirs>=2.4.0",
28
+ "PyYAML>=5.3.1",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://mozilla.github.io/glean"
33
+ Repository = "https://github.com/mozilla/glean_parser"
34
+ Changelog = "https://github.com/mozilla/glean_parser/blob/main/CHANGELOG.md"
35
+
36
+ [project.scripts]
37
+ glean_parser = "glean_parser.__main__:main_wrapper"
38
+
39
+ [build-system]
40
+ requires = ["hatchling", "hatch-vcs"]
41
+ build-backend = "hatchling.build"
42
+
43
+ [dependency-groups]
44
+ dev = [
45
+ "coverage>=7.6.1",
46
+ "mypy>=1.14.1",
47
+ "pip-licenses>=4.5.1",
48
+ "pytest>=8.3.4",
49
+ "recommonmark>=0.7.1",
50
+ "ruff>=0.9.3",
51
+ "sphinx>=7.1.2",
52
+ "types-pyyaml>=6.0.12.20241230",
53
+ "yamllint>=1.35.1",
54
+ ]
55
+
56
+ [tool.hatch.build.targets.sdist]
57
+ include = [
58
+ "/glean_parser",
59
+ "/server_telemetry",
60
+ "/tests",
61
+ ]
62
+
63
+ [tool.hatch.version]
64
+ source = "vcs"
65
+
66
+ [tool.hatch.version.raw-options]
67
+ version_scheme = "no-guess-dev"
68
+
69
+ [tool.pytest.ini_options]
70
+ markers = [
71
+ "web_dependency: mark a test that requires a web connection.",
72
+ "node_dependency: mark a test that requires node.",
73
+ "ruby_dependency: mark a test that requires ruby.",
74
+ "go_dependency: mark a test that requires go.",
75
+ "rust_dependency: mark a test that requires rust.",
76
+ ]
@@ -27,6 +27,21 @@ all_metrics:
27
27
  - label_a
28
28
  - label_b
29
29
 
30
+ dual_labeled_counter:
31
+ <<: *defaults
32
+ type: dual_labeled_counter
33
+ dual_labels:
34
+ key:
35
+ description: The key for the dual labeled counter
36
+ labels:
37
+ - key1
38
+ - key2
39
+ category:
40
+ description: The category for the dual labeled counter
41
+ labels:
42
+ - category1
43
+ - category2
44
+
30
45
  bool:
31
46
  <<: *defaults
32
47
  type: boolean