1+ import datetime
12import logging
23import typing
4+ from typing import Any
35
4- from pydantic . v1 . class_validators import validator
6+ import pydantic as pydantic_v2
57
68from pcapi import settings
79from pcapi .connectors .big_query .queries .base import BaseQuery
8- from pcapi .connectors .serialization .titelive_serializers import TiteLiveBookArticle
9- from pcapi .connectors .serialization .titelive_serializers import TiteliveMusicArticle
1010from pcapi .connectors .titelive import TiteliveBase
11+ from pcapi .utils import date as date_utils
1112
1213
1314logger = logging .getLogger (__name__ )
1415
1516
16- class BigQueryTiteliveBookProductModel (TiteLiveBookArticle ):
17+ def _format_gtl_code (code : str ) -> str :
18+ # A GTL id is 8 characters long.
19+ # Each pair represents a GTL level.
20+ # The first 2 characters are level 1, the next 2 are level 2, etc.
21+ # - example: 05030000 corresponds to a level 1 GTL of 05 and a level 2 of 03. So "Tourism & Travel World".
22+
23+ # We receive gtl_ids without leading zeros, and sometimes without trailing ones.
24+ # We must add them to have an 8-character code.
25+
26+ # We start by adding the missing zeros to the left.
27+ # If we receive a code with an odd number of characters, we must add a zero to the left.
28+ # '5030000' -> '05030000'
29+ # Otherwise, we don't add anything.
30+ # '110400' -> '110400'
31+
32+ if len (code ) % 2 == 1 :
33+ code = "0" + code
34+
35+ # Then we add the missing zeros to the right to have 8 characters.
36+ # '050300' -> '05030000'
37+ # '110400' -> '11040000'
38+ code = code .ljust (8 , "0" )
39+ return code
40+
41+
42+ class GenreTitelive (pydantic_v2 .BaseModel ):
43+ code : str = pydantic_v2 .Field (min_length = 8 , max_length = 8 )
44+ libelle : str
45+
46+ @pydantic_v2 .field_validator ("code" , mode = "before" )
47+ @classmethod
48+ def validate_code (cls , code : str ) -> str :
49+ return _format_gtl_code (code )
50+
51+
52+ class TiteliveGtl (pydantic_v2 .BaseModel ):
53+ first : dict [str , GenreTitelive ] | None = None
54+
55+
56+ class BigQueryTiteliveProductBaseModel (pydantic_v2 .BaseModel ):
57+ model_config = pydantic_v2 .ConfigDict (populate_by_name = True )
58+
1759 ean : str
1860 titre : str
19- recto_uuid : str | None
20- verso_uuid : str | None
61+ recto_uuid : str | None = None
62+ verso_uuid : str | None = None
63+ has_image : bool = pydantic_v2 .Field (alias = "image" , default = False )
64+ has_verso_image : bool = pydantic_v2 .Field (alias = "image_4" , default = False )
65+
66+ resume : str | None = None
67+ codesupport : str | None = None
68+ gtl : TiteliveGtl | None = None
69+ dateparution : datetime .date | None = None
70+ editeur : str | None = None
71+ prix : float | None = None
72+
73+ gencod : str = pydantic_v2 .Field (min_length = 13 , max_length = 13 )
74+
75+ @pydantic_v2 .model_validator (mode = "before" )
76+ @classmethod
77+ def parse_empty_strings_as_none (cls , data : Any ) -> Any :
78+ if isinstance (data , dict ):
79+ return {k : (None if v == "" else v ) for k , v in data .items ()}
80+ return data
81+
82+ @pydantic_v2 .field_validator ("dateparution" , mode = "before" )
83+ @classmethod
84+ def parse_dates (cls , v : Any ) -> Any :
85+ return date_utils .parse_french_date (v )
86+
87+ @pydantic_v2 .field_validator ("gtl" , mode = "before" )
88+ @classmethod
89+ def validate_gtl (cls , gtl : TiteliveGtl | list ) -> TiteliveGtl | None :
90+ if isinstance (gtl , list ):
91+ return None
92+ return gtl
93+
94+ @pydantic_v2 .field_validator ("has_image" , "has_verso_image" , mode = "before" )
95+ @classmethod
96+ def validate_image (cls , image : str | int | None ) -> bool :
97+ # The API currently sends 0 (int) if no image is available, and "1" (str) if an image is available.
98+ # Because it has been famously flaky in the past, we are being defensive here and consider:
99+ # - all forms of 0 and None as False.
100+ # - all forms of "1" as True.
101+ if image is not None and int (image ) not in (0 , 1 ):
102+ raise ValueError (f"unhandled image value { image } " )
103+ return bool (image and int (image ) == 1 )
104+
105+
106+ class BigQueryTiteliveBookProductModel (BigQueryTiteliveProductBaseModel ):
21107 auteurs_multi : list [str ]
22-
23- @validator ("auteurs_multi" , pre = True )
108+ langueiso : str | None = None
109+ taux_tva : str | None = None
110+ id_lectorat : str | None = None
111+
112+ @pydantic_v2 .field_validator ("taux_tva" , mode = "before" )
113+ @classmethod
114+ def validate_code_tva (cls , value : typing .Literal [0 ] | str | None ) -> str | None :
115+ if value == 0 :
116+ return None
117+ return value
118+
119+ @pydantic_v2 .field_validator ("auteurs_multi" , mode = "before" )
120+ @classmethod
24121 def validate_auteurs_multi (cls , auteurs_multi : typing .Any ) -> list :
25122 if isinstance (auteurs_multi , list ):
26123 return auteurs_multi
@@ -62,11 +159,16 @@ class BigQueryTiteliveBookProductDeltaQuery(BaseQuery):
62159 model = BigQueryTiteliveBookProductModel
63160
64161
65- class BigQueryTiteliveMusicProductModel (TiteliveMusicArticle ):
66- ean : str
67- titre : str
68- recto_uuid : str | None
69- verso_uuid : str | None
162+ class BigQueryTiteliveMusicProductModel (BigQueryTiteliveProductBaseModel ):
163+ label : str | None = None
164+ compositeur : str | None = None
165+ interprete : str | None = None
166+ nb_galettes : str | None = None
167+ artiste : str | None = None
168+ commentaire : str | None = None
169+ contenu_explicite : int | None = None
170+ dispo : int | None = None
171+ distributeur : str | None = None
70172
71173
72174class BigQueryTiteliveMusicProductDeltaQuery (BaseQuery ):
@@ -87,7 +189,10 @@ class BigQueryTiteliveMusicProductDeltaQuery(BaseQuery):
87189 performer as interprete,
88190 nb_discs as nb_galettes,
89191 artist as artiste,
90-
192+ comment as commentaire,
193+ explicit_content as contenu_explicite,
194+ availability as dispo,
195+ distributor as distributeur,
91196 recto_uuid,
92197 verso_uuid,
93198 image,
0 commit comments