contexts.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. from typing import Annotated, Any, Callable, Literal, Optional, TypedDict
  2. from ninja import Field, Schema
  3. from pydantic import BeforeValidator, model_serializer
  4. from .base import LaxIngestSchema
  5. class ExcludeNoneSchema(Schema):
  6. """
  7. Implements model_dump's exclude_none on the schema itself
  8. Useful for nested schemas where more granular control is needed
  9. Related https://github.com/pydantic/pydantic/discussions/5461
  10. """
  11. @model_serializer(mode="wrap")
  12. def ser_model(self, wrap: Callable) -> dict[str, Any]:
  13. if isinstance(self, Schema):
  14. return {
  15. model_field: getattr(self, model_field)
  16. for model_field in self.model_fields
  17. if getattr(self, model_field) is not None
  18. }
  19. return wrap(self)
  20. class DeviceContext(LaxIngestSchema, ExcludeNoneSchema):
  21. type: Literal["device"] = "device"
  22. name: Optional[str] = None # Inconsistency documented as required
  23. family: Optional[str] = None # Recommended but optional
  24. model: Optional[str] = None # Recommended but optional
  25. model_id: Optional[str] = None
  26. arch: Optional[str] = None
  27. battery_level: Optional[float] = None
  28. orientation: Optional[str] = None
  29. manufacturer: Optional[str] = None
  30. brand: Optional[str] = None
  31. screen_resolution: Optional[str] = None
  32. screen_height_pixels: Optional[int] = None
  33. screen_width_pixels: Optional[int] = None
  34. screen_density: Optional[float] = None
  35. screen_dpi: Optional[float] = None
  36. online: Optional[bool] = None
  37. charging: Optional[bool] = None
  38. low_memory: Optional[bool] = None
  39. simulator: Optional[bool] = None
  40. memory_size: Optional[int] = None
  41. free_memory: Optional[int] = None
  42. usable_memory: Optional[int] = None
  43. storage_size: Optional[int] = None
  44. free_storage: Optional[int] = None
  45. external_storage_size: Optional[int] = None
  46. external_free_storage: Optional[int] = None
  47. boot_time: Optional[str] = None
  48. timezone: Optional[str] = None # Deprecated, use timezone of culture context
  49. language: Optional[str] = None # Deprecated, use locale of culture context
  50. processor_count: Optional[int] = None
  51. cpu_description: Optional[str] = None
  52. processor_frequency: Optional[float] = None
  53. device_type: Optional[str] = None
  54. battery_status: Optional[str] = None
  55. device_unique_identifier: Optional[str] = None
  56. supports_vibration: Optional[bool] = None
  57. supports_accelerometer: Optional[bool] = None
  58. supports_gyroscope: Optional[bool] = None
  59. supports_audio: Optional[bool] = None
  60. supports_location_service: Optional[bool] = None
  61. class Config(LaxIngestSchema.Config):
  62. protected_namespaces = ()
  63. class OSContext(LaxIngestSchema, ExcludeNoneSchema):
  64. type: Literal["os"] = "os"
  65. name: str
  66. version: Optional[str] = None
  67. build: Optional[str] = None
  68. kernel_version: Optional[str] = None
  69. rooted: Optional[bool] = None
  70. theme: Optional[str] = None
  71. raw_description: Optional[str] = None # Recommended but optional
  72. class RuntimeContext(LaxIngestSchema, ExcludeNoneSchema):
  73. type: Literal["runtime"] = "runtime"
  74. name: str | None = None # Recommended
  75. version: str | None = None
  76. raw_description: str | None = None
  77. class AppContext(LaxIngestSchema, ExcludeNoneSchema):
  78. type: Literal["app"] = "app"
  79. app_start_time: Optional[str] = None
  80. device_app_hash: Optional[str] = None
  81. build_type: Optional[str] = None
  82. app_identifier: Optional[str] = None
  83. app_name: Optional[str] = None
  84. app_version: Optional[str] = None
  85. app_build: Optional[str] = None
  86. app_memory: Optional[int] = None
  87. in_foreground: Optional[bool] = None
  88. class BrowserContext(LaxIngestSchema, ExcludeNoneSchema):
  89. type: Literal["browser"] = "browser"
  90. name: str
  91. version: Optional[str] = None
  92. class GPUContext(LaxIngestSchema, ExcludeNoneSchema):
  93. type: Literal["gpu"] = "gpu"
  94. name: str
  95. version: Optional[str] = None
  96. id: Optional[str] = None
  97. vendor_id: Optional[str] = None
  98. vendor_name: Optional[str] = None
  99. memory_size: Optional[int] = None
  100. api_type: Optional[str] = None
  101. multi_threaded_rendering: Optional[bool] = None
  102. npot_support: Optional[str] = None
  103. max_texture_size: Optional[int] = None
  104. graphics_shader_level: Optional[str] = None
  105. supports_draw_call_instancing: Optional[bool] = None
  106. supports_ray_tracing: Optional[bool] = None
  107. supports_compute_shaders: Optional[bool] = None
  108. supports_geometry_shaders: Optional[bool] = None
  109. class StateContext(LaxIngestSchema):
  110. type: Literal["state"] = "state"
  111. state: dict
  112. class CultureContext(LaxIngestSchema, ExcludeNoneSchema):
  113. type: Literal["culture"] = "culture"
  114. calendar: Optional[str] = None
  115. display_name: Optional[str] = None
  116. locale: Optional[str] = None
  117. is_24_hour_format: Optional[bool] = None
  118. timezone: Optional[str] = None
  119. class CloudResourceContext(LaxIngestSchema):
  120. type: Literal["cloud_resource"] = "cloud_resource"
  121. cloud: dict
  122. host: dict
  123. class TraceContext(LaxIngestSchema, ExcludeNoneSchema):
  124. type: Literal["trace"] = "trace"
  125. trace_id: str
  126. span_id: str
  127. parent_span_id: str | None = None
  128. op: str | None = None
  129. status: str | None = None
  130. exclusive_time: float | None = None
  131. client_sample_rate: float | None = None
  132. tags: dict | list | None = None
  133. dynamic_sampling_context: dict | None = None
  134. origin: str | None = None
  135. class ReplayContext(LaxIngestSchema):
  136. type: Literal["replay"] = "replay"
  137. replay_id: str
  138. class ResponseContext(LaxIngestSchema):
  139. type: Literal["response"] = "response"
  140. status_code: int
  141. class ContextsDict(TypedDict):
  142. device: DeviceContext
  143. os: OSContext
  144. runtime: RuntimeContext
  145. app: AppContext
  146. browser: BrowserContext
  147. gpu: GPUContext
  148. state: StateContext
  149. culture: CultureContext
  150. cloud_resource: CloudResourceContext
  151. trace: TraceContext
  152. replay: ReplayContext
  153. response: ResponseContext
  154. ContextsUnion = Annotated[
  155. DeviceContext
  156. | OSContext
  157. | RuntimeContext
  158. | AppContext
  159. | BrowserContext
  160. | GPUContext
  161. | StateContext
  162. | CultureContext
  163. | CloudResourceContext
  164. | TraceContext
  165. | ReplayContext
  166. | ResponseContext,
  167. Field(discriminator="type"),
  168. ]
  169. type_strings = [
  170. "device",
  171. "os",
  172. "runtime",
  173. "app",
  174. "browser",
  175. "gpu",
  176. "state",
  177. "culture",
  178. "cloud_resource",
  179. "trace",
  180. "replay",
  181. "response",
  182. ]
  183. def default_types(v: Any) -> Any:
  184. if all(isinstance(value, dict) for value in v.values()):
  185. return {
  186. key: {
  187. **value,
  188. "type": key,
  189. }
  190. if key in type_strings and "type" not in value
  191. else value
  192. for key, value in v.items()
  193. }
  194. return v
  195. # TODO warns Failed to get discriminator value for tagged union serialization with value
  196. Contexts = Annotated[dict[str, ContextsUnion | Any], BeforeValidator(default_types)]