unitysvc-core 0.2.31__tar.gz → 0.2.32__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 (45) hide show
  1. {unitysvc_core-0.2.31/src/unitysvc_core.egg-info → unitysvc_core-0.2.32}/PKG-INFO +1 -1
  2. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/pyproject.toml +1 -1
  3. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/utils.py +52 -32
  4. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
  5. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_utils.py +65 -0
  6. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/LICENSE +0 -0
  7. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/README.md +0 -0
  8. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/setup.cfg +0 -0
  9. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/__init__.py +0 -0
  10. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/__init__.py +0 -0
  11. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/base.py +0 -0
  12. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/documents.py +0 -0
  13. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/listing_data.py +0 -0
  14. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/listing_v1.py +0 -0
  15. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/offering_data.py +0 -0
  16. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/offering_v1.py +0 -0
  17. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/pricing.py +0 -0
  18. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/promotion_data.py +0 -0
  19. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/provider_data.py +0 -0
  20. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/provider_v1.py +0 -0
  21. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/service.py +0 -0
  22. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/service_group_data.py +0 -0
  23. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/models/validators.py +0 -0
  24. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/py.typed +0 -0
  25. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/base.json +0 -0
  26. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/listing_v1.json +0 -0
  27. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/offering_v1.json +0 -0
  28. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/promotion_v1.json +0 -0
  29. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/provider_v1.json +0 -0
  30. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/service_group_v1.json +0 -0
  31. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core/validator.py +0 -0
  32. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
  33. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
  34. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core.egg-info/requires.txt +0 -0
  35. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/src/unitysvc_core.egg-info/top_level.txt +0 -0
  36. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_access_interface_routing.py +0 -0
  37. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_effective_price.py +0 -0
  38. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_hidden_path_skip.py +0 -0
  39. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_offering_data.py +0 -0
  40. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_pricing.py +0 -0
  41. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_provider_account_rate_limits.py +0 -0
  42. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_service_group.py +0 -0
  43. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_service_status.py +0 -0
  44. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_validator.py +0 -0
  45. {unitysvc_core-0.2.31 → unitysvc_core-0.2.32}/tests/test_validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.31
3
+ Version: 0.2.32
4
4
  Summary: Shared data models and validation helpers for UnitySVC packages
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unitysvc-core"
7
- version = "0.2.31"
7
+ version = "0.2.32"
8
8
  description = "Shared data models and validation helpers for UnitySVC packages"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Bo Peng", email = "bo.peng@unitysvc.com" }]
@@ -11,6 +11,7 @@ and unitysvc backend, including:
11
11
 
12
12
  import hashlib
13
13
  import json
14
+ import mimetypes
14
15
  import os
15
16
  import tomllib
16
17
  import warnings
@@ -104,52 +105,71 @@ def get_basename(filename: str) -> str:
104
105
 
105
106
 
106
107
  def mime_type_to_extension(mime_type: str) -> str:
107
- """Convert MIME type to file extension.
108
+ """File extension for a media type or a document-kind token.
109
+
110
+ Both forms arrive here. ``Document.mime_type`` is a :class:`MimeTypeEnum`,
111
+ whose values are document KINDS rather than media types -- ``png``,
112
+ ``markdown``, ``python``, and ``url``, which is not a media type at all --
113
+ while other callers pass a real ``image/png``. A kind that fell through to
114
+ ``txt`` meant a PNG stored under a ``.txt`` key and served as
115
+ ``text/plain``, which browsers refuse to render under ``nosniff``.
116
+
117
+ Resolution order: the explicit table, then the stdlib (which knows every
118
+ image kind both as a media type and as an extension), then the media type's
119
+ subtype, then ``txt``. The fallback stays: an honest, uninformative
120
+ extension beats a guessed-wrong media type.
108
121
 
109
122
  Args:
110
- mime_type: MIME type string
123
+ mime_type: A media type (``image/png``) or a kind token (``png``).
111
124
 
112
125
  Returns:
113
- File extension without dot
126
+ File extension without the dot.
114
127
  """
115
- # Common MIME type to extension mappings
116
- mime_map = {
128
+ if not mime_type:
129
+ return "txt"
130
+
131
+ value = mime_type.strip().lower()
132
+ if not value:
133
+ return "txt"
134
+
135
+ # Spelled out where the stdlib is absent or disagrees with what we store:
136
+ # the languages have no media type it knows, ``url`` is not a media type,
137
+ # and we store ``.md`` and ``.jpg`` rather than ``.markdown`` and ``.jpeg``.
138
+ explicit = {
117
139
  "text": "txt",
118
140
  "plain": "txt",
119
141
  "text/plain": "txt",
120
- "text/html": "html",
121
142
  "text/markdown": "md",
122
- "text/csv": "csv",
123
- "application/json": "json",
124
- "application/pdf": "pdf",
125
- "application/xml": "xml",
126
- "application/x-yaml": "yaml",
127
- "application/octet-stream": "bin",
128
- "image/png": "png",
129
- "image/jpeg": "jpg",
130
- "image/gif": "gif",
131
143
  "markdown": "md",
132
- "html": "html",
133
- "json": "json",
134
- "pdf": "pdf",
135
- "xml": "xml",
144
+ "image/jpeg": "jpg",
145
+ "jpeg": "jpg",
146
+ "python": "py",
147
+ "javascript": "js",
148
+ "bash": "sh",
136
149
  "yaml": "yaml",
137
- "csv": "csv",
150
+ "application/x-yaml": "yaml",
151
+ "application/octet-stream": "bin",
138
152
  "url": "url",
139
153
  }
154
+ if value in explicit:
155
+ return explicit[value]
156
+
157
+ if "/" in value:
158
+ guessed = mimetypes.guess_extension(value)
159
+ if guessed:
160
+ return guessed.lstrip(".")
161
+ subtype = value.split("/", 1)[1]
162
+ if subtype in explicit:
163
+ return explicit[subtype]
164
+ if mimetypes.guess_type(f"x.{subtype}")[0]:
165
+ return subtype
166
+ return "txt"
167
+
168
+ # A bare token that is already a real extension: png, svg, webp, pdf, json,
169
+ # html, csv, xml. Resolved rather than listed, so the table stays short.
170
+ if mimetypes.guess_type(f"x.{value}")[0]:
171
+ return value
140
172
 
141
- # Try exact match first
142
- mime_lower = mime_type.lower()
143
- if mime_lower in mime_map:
144
- return mime_map[mime_lower]
145
-
146
- # Try to extract from mime type parts
147
- if "/" in mime_lower:
148
- _, subtype = mime_lower.split("/", 1)
149
- if subtype in mime_map:
150
- return mime_map[subtype]
151
-
152
- # Default to txt
153
173
  return "txt"
154
174
 
155
175
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.31
3
+ Version: 0.2.32
4
4
  Summary: Shared data models and validation helpers for UnitySVC packages
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -5,6 +5,7 @@ from pathlib import Path
5
5
 
6
6
  import pytest
7
7
 
8
+ from unitysvc_core.models.base import MimeTypeEnum
8
9
  from unitysvc_core.utils import (
9
10
  DEFAULT_PRESET_FNS,
10
11
  DataFileLoadWarning,
@@ -64,6 +65,70 @@ def test_mime_type_to_extension() -> None:
64
65
  assert mime_type_to_extension("application/pdf") == "pdf"
65
66
 
66
67
 
68
+ @pytest.mark.parametrize(
69
+ ("value", "expected"),
70
+ [
71
+ ("image/png", "png"),
72
+ ("image/jpeg", "jpg"),
73
+ ("image/svg+xml", "svg"),
74
+ ("image/webp", "webp"),
75
+ ("text/markdown", "md"),
76
+ ("text/plain", "txt"),
77
+ ("application/json", "json"),
78
+ ],
79
+ )
80
+ def test_a_media_type_resolves_to_its_extension(value: str, expected: str) -> None:
81
+ assert mime_type_to_extension(value) == expected
82
+
83
+
84
+ @pytest.mark.parametrize(
85
+ ("value", "expected"),
86
+ [
87
+ ("markdown", "md"),
88
+ ("python", "py"),
89
+ ("javascript", "js"),
90
+ ("bash", "sh"),
91
+ ("html", "html"),
92
+ ("json", "json"),
93
+ ("text", "txt"),
94
+ ("pdf", "pdf"),
95
+ ("jpeg", "jpg"),
96
+ ("png", "png"),
97
+ ("svg", "svg"),
98
+ ("webp", "webp"),
99
+ ("url", "url"),
100
+ ],
101
+ )
102
+ def test_a_document_kind_token_resolves_to_its_extension(
103
+ value: str, expected: str
104
+ ) -> None:
105
+ """``Document.mime_type`` is a ``MimeTypeEnum``, whose values are kinds
106
+ rather than media types. A kind that fell through to ``txt`` meant a PNG
107
+ stored under a ``.txt`` key and served as ``text/plain``."""
108
+ assert mime_type_to_extension(value) == expected
109
+
110
+
111
+ def test_every_mime_type_enum_value_resolves() -> None:
112
+ """The regression this guards: an unknown kind became ``txt`` silently."""
113
+ unresolved = [
114
+ kind.value
115
+ for kind in MimeTypeEnum
116
+ if kind is not MimeTypeEnum.text and mime_type_to_extension(kind.value) == "txt"
117
+ ]
118
+
119
+ assert unresolved == []
120
+
121
+
122
+ @pytest.mark.parametrize("value", ["", " ", "not-a-thing", "application/x-unknown"])
123
+ def test_an_unresolvable_value_falls_back_to_txt(value: str) -> None:
124
+ assert mime_type_to_extension(value) == "txt"
125
+
126
+
127
+ def test_case_and_whitespace_are_not_the_callers_problem() -> None:
128
+ assert mime_type_to_extension("PNG") == "png"
129
+ assert mime_type_to_extension(" image/png ") == "png"
130
+
131
+
67
132
  # =============================================================================
68
133
  # deep_merge_dicts
69
134
  # =============================================================================
File without changes
File without changes
File without changes