Skip to content
This repository was archived by the owner on Apr 30, 2025. It is now read-only.

Commit 15536d7

Browse files
JozefielJozef Volak
and
Jozef Volak
authored
[graphql-pydantic-converter] Fix Map type rendering (#49)
Co-authored-by: Jozef Volak <[email protected]>
1 parent e89095d commit 15536d7

File tree

8 files changed

+137
-68
lines changed

8 files changed

+137
-68
lines changed

utils/graphql-pydantic-converter/CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,10 @@
1010

1111
# 0.1.2
1212
- Ignore generating of private graphql schema objects
13-
- Add graphql_pydantic_converter.graphql_types.concatenate_queries function
13+
- Add graphql_pydantic_converter.graphql_types.concatenate_queries function
14+
15+
# 1.0.0
16+
- Migration to pydantic v2
17+
18+
# 1.1.0
19+
- Change Map type to dict (key, value)

utils/graphql-pydantic-converter/README.md

Lines changed: 37 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ options:
4444
import typing
4545

4646
from pydantic import Field
47+
from pydantic import PrivateAttr
4748
from graphql_pydantic_converter.graphql_types import Input
4849
from graphql_pydantic_converter.graphql_types import Mutation
4950
from graphql_pydantic_converter.graphql_types import Payload
@@ -58,36 +59,36 @@ String: typing.TypeAlias = str
5859

5960
class CreateScheduleInput(Input):
6061
name: String
61-
workflow_name: String = Field(alias='workflowName')
62-
workflow_version: String = Field(alias='workflowVersion')
63-
cron_string: String = Field(alias='cronString')
64-
enabled: typing.Optional[Boolean]
65-
parallel_runs: typing.Optional[Boolean] = Field(alias='parallelRuns')
66-
workflow_context: typing.Optional[String] = Field(alias='workflowContext')
67-
from_date: typing.Optional[DateTime] = Field(alias='fromDate')
68-
to_date: typing.Optional[DateTime] = Field(alias='toDate')
62+
workflow_name: String = Field(default=None, alias='workflowName')
63+
workflow_version: String = Field(default=None, alias='workflowVersion')
64+
cron_string: String = Field(default=None, alias='cronString')
65+
enabled: typing.Optional[Boolean] = Field(default=None)
66+
parallel_runs: typing.Optional[Boolean] = Field(default=None, alias='parallelRuns')
67+
workflow_context: typing.Optional[String] = Field(default=None, alias='workflowContext')
68+
from_date: typing.Optional[DateTime] = Field(default=None, alias='fromDate')
69+
to_date: typing.Optional[DateTime] = Field(default=None, alias='toDate')
6970

7071
class Schedule(Payload):
71-
name: typing.Optional[Boolean] = Field(response='String', default=False)
72-
enabled: typing.Optional[Boolean] = Field(response='Boolean', default=False)
73-
parallel_runs: typing.Optional[Boolean] = Field(response='Boolean', alias='parallelRuns', default=False)
74-
workflow_name: typing.Optional[Boolean] = Field(response='String', alias='workflowName', default=False)
75-
workflow_version: typing.Optional[Boolean] = Field(response='String', alias='workflowVersion', default=False)
76-
cron_string: typing.Optional[Boolean] = Field(response='String', alias='cronString', default=False)
77-
workflow_context: typing.Optional[Boolean] = Field(response='String', alias='workflowContext', default=False)
78-
from_date: typing.Optional[Boolean] = Field(response='DateTime', alias='fromDate', default=False)
79-
to_date: typing.Optional[Boolean] = Field(response='DateTime', alias='toDate', default=False)
80-
status: typing.Optional[Boolean] = Field(response='Status', default=False)
72+
name: typing.Optional[bool] = Field(default=False)
73+
enabled: typing.Optional[bool] = Field(default=False)
74+
parallel_runs: typing.Optional[bool] = Field(alias='parallelRuns', default=False)
75+
workflow_name: typing.Optional[bool] = Field(alias='workflowName', default=False)
76+
workflow_version: typing.Optional[bool] = Field(alias='workflowVersion', default=False)
77+
cron_string: typing.Optional[bool] = Field(alias='cronString', default=False)
78+
workflow_context: typing.Optional[bool] = Field(alias='workflowContext', default=False)
79+
from_date: typing.Optional[bool] = Field(alias='fromDate', default=False)
80+
to_date: typing.Optional[bool] = Field(alias='toDate', default=False)
81+
status: typing.Optional[bool] = Field(default=False)
8182

8283

8384
class CreateScheduleMutation(Mutation):
84-
_name: str = Field('createSchedule', const=True)
85+
_name: str = PrivateAttr('createSchedule')
8586
input: CreateScheduleInput
8687
payload: Schedule
8788

88-
CreateScheduleInput.update_forward_refs()
89-
CreateScheduleMutation.update_forward_refs()
90-
Schedule.update_forward_refs()
89+
CreateScheduleInput.model_rebuild()
90+
CreateScheduleMutation.model_rebuild()
91+
Schedule.model_rebuild()
9192

9293
```
9394

@@ -147,7 +148,7 @@ mutation {
147148

148149
### Response parser
149150

150-
Example of generated model.py ()
151+
Example of generated model.py
151152

152153
```python
153154
import typing
@@ -173,25 +174,25 @@ class Status(ENUM):
173174

174175

175176
class SchedulePayload(BaseModel):
176-
name: typing.Optional[typing.Optional[String]]
177-
enabled: typing.Optional[typing.Optional[Boolean]]
178-
parallel_runs: typing.Optional[typing.Optional[Boolean]] = Field(alias='parallelRuns')
179-
workflow_name: typing.Optional[typing.Optional[String]] = Field(alias='workflowName')
180-
workflow_version: typing.Optional[typing.Optional[String]] = Field(alias='workflowVersion')
181-
cron_string: typing.Optional[typing.Optional[String]] = Field(alias='cronString')
182-
workflow_context: typing.Optional[typing.Optional[String]] = Field(alias='workflowContext')
183-
from_date: typing.Optional[typing.Optional[DateTime]] = Field(alias='fromDate')
184-
to_date: typing.Optional[typing.Optional[DateTime]] = Field(alias='toDate')
185-
status: typing.Optional[typing.Optional[Status]]
177+
name: typing.Optional[typing.Optional[String]] = Field(default=None)
178+
enabled: typing.Optional[typing.Optional[Boolean]] = Field(default=None)
179+
parallel_runs: typing.Optional[typing.Optional[Boolean]] = Field(default=None, alias='parallelRuns')
180+
workflow_name: typing.Optional[typing.Optional[String]] = Field(default=None, alias='workflowName')
181+
workflow_version: typing.Optional[typing.Optional[String]] = Field(default=None, alias='workflowVersion')
182+
cron_string: typing.Optional[typing.Optional[String]] = Field(default=None, alias='cronString')
183+
workflow_context: typing.Optional[typing.Optional[String]] = Field(default=None, alias='workflowContext')
184+
from_date: typing.Optional[typing.Optional[DateTime]] = Field(default=None, alias='fromDate')
185+
to_date: typing.Optional[typing.Optional[DateTime]] = Field(default=None, alias='toDate')
186+
status: typing.Optional[typing.Optional[Status]] = Field(default=None)
186187

187188

188189
class CreateScheduleData(BaseModel):
189-
create_schedule: SchedulePayload = Field(alias='createSchedule')
190+
create_schedule: SchedulePayload = Field(default=None, alias='createSchedule')
190191

191192

192193
class CreateScheduleResponse(BaseModel):
193-
data: typing.Optional[CreateScheduleData]
194-
errors: typing.Optional[typing.Any]
194+
data: typing.Optional[CreateScheduleData] = Field(default=None)
195+
errors: typing.Optional[typing.Any] = Field(default=None)
195196

196197

197198
```

utils/graphql-pydantic-converter/graphql_pydantic_converter/graphql_types.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -221,21 +221,31 @@ def dict_to_custom_string(self, value: dict[str, Any]) -> str:
221221
for item in value:
222222
pairs.append(self.dict_to_custom_string(item))
223223
return f"[ {', '.join(pairs)}]"
224-
else:
224+
elif isinstance(value, dict):
225+
pairs = []
226+
for key, values in value.items():
227+
pairs.append(f'{key}: {self.dict_to_custom_string(values)}')
228+
return f'{{ {", ".join(pairs)} }}'
229+
elif isinstance(value, str):
225230
return f'"{value}"'
231+
else:
232+
return f'{value}'
226233

227234
def render(self) -> str:
228-
payload = ''
235+
mutation_name: str = self._name
236+
payload: str = ''
229237
if isinstance(self.payload, Payload):
230238
payload = f'{{ {self.payload.render()} }}'
239+
231240
variables: list[str] = []
232-
for k, value in self:
233-
if k not in ['_name', 'payload']:
234-
variables.append(f' {k}: {self.dict_to_custom_string( value)}')
241+
inputs = self.model_dump(exclude={'_name', 'payload'}, exclude_none=True, by_alias=True)
242+
243+
for name, value in inputs.items():
244+
variables.append(f' {name}: {self.dict_to_custom_string(value)}')
245+
235246
variable = ', '.join(variables)
236-
name: str = self._name
237247

238-
return f'mutation {{ { name } ({variable}) {payload} }}'
248+
return f'mutation {{ {mutation_name} ({variable}) {payload} }}'
239249

240250

241251
class Query(BaseModel):

utils/graphql-pydantic-converter/graphql_pydantic_converter/schema_converter.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class Scalars(str, Enum):
6060
Int = 'Int'
6161
Boolean = 'Boolean'
6262
Upload = 'Upload'
63+
Map = 'Map'
6364

6465
class QueryType(BaseModel):
6566
name: Optional[str] = None
@@ -201,14 +202,14 @@ def __import_classes(self, items: dict[str, Any], worker_list: list[Optional[str
201202

202203
imports = [
203204
'from __future__ import annotations\n\n'
204-
'import typing\n',
205-
'from pydantic import BaseModel',
206-
'from pydantic import Field',
207-
'from pydantic import PrivateAttr',
208-
'\n'
205+
'import typing\n\n',
209206
]
210207
self.__result += '\n'.join(imports)
211208

209+
self.__result += 'from pydantic import BaseModel\n'
210+
self.__result += 'from pydantic import Field\n'
211+
self.__result += 'from pydantic import PrivateAttr\n\n'
212+
212213
if items[GraphqlJsonParser.TypeKind.ENUM]:
213214
self.__result += kv_template.substitute(type=GraphqlJsonParser.ConverterMap.ENUM.value)
214215
if items[GraphqlJsonParser.TypeKind.INPUT_OBJECT]:
@@ -223,6 +224,7 @@ def __import_classes(self, items: dict[str, Any], worker_list: list[Optional[str
223224
self.__result += kv_template.substitute(type=GraphqlJsonParser.ConverterMap.QUERY.value)
224225
if GraphqlJsonParser.ConverterMap.SUBSCRIPTION in worker_list:
225226
self.__result += kv_template.substitute(type=GraphqlJsonParser.ConverterMap.SUBSCRIPTION.value)
227+
226228
self.__result += '\n'
227229

228230
def __extract_fields(self, of_type: OfType | Type, previous: list[Any]) -> list[Any]:
@@ -305,6 +307,9 @@ def __create_scalar(self, scalars: list[Type]) -> None:
305307
scalar_type = 'list'
306308
case self.Scalars.ID:
307309
scalar_type = 'str'
310+
case self.Scalars.Map:
311+
scalar_type = 'dict[str, typing.Any]'
312+
308313
self.__result += kv_template.substitute(indent=self.__INDENT, name=scalar.name, type=scalar_type)
309314
self.__ignore_enums.append(scalar.name)
310315

@@ -360,7 +365,10 @@ def __create_input(self, inputs: list[Type]) -> None:
360365
name = field.name
361366
field_args = []
362367
field_str = ''
363-
if name.startswith('typing.Optional'):
368+
369+
kind = self.__build_type(self.__extract_fields(field.type, []))
370+
371+
if kind.startswith('typing.Optional'):
364372
field_args.append('default=None')
365373

366374
if self.is_not_snake_case(field.name):
@@ -370,7 +378,6 @@ def __create_input(self, inputs: list[Type]) -> None:
370378
if len(field_args) > 0:
371379
field_str = f" = Field({', '.join(field_args)})"
372380

373-
kind = self.__build_type(self.__extract_fields(field.type, []))
374381
self.__result += kv_template.substitute(
375382
indent=self.__INDENT, name=name, val=kind, field=field_str
376383
)

utils/graphql-pydantic-converter/pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ packages = [{ include = "graphql_pydantic_converter" }]
1919
name = "graphql-pydantic-converter"
2020
description = "Convert pydantic schema to pydantic datamodel and build request from it"
2121
authors = ["Jozef Volak <[email protected]>"]
22-
version = '1.0.0'
22+
version = '1.0.1'
2323
readme = ["README.md", "CHANGELOG.md"]
2424
keywords = ["graphql", "pydantic"]
2525
license = "Apache 2.0"
@@ -79,3 +79,6 @@ init_forbid_extra = true
7979
init_typed = true
8080
warn_required_dynamic_aliases = true
8181
warn_untyped_fields = true
82+
83+
[tool.pytest.ini_options]
84+
python_files = 'tests/*'

utils/graphql-pydantic-converter/tests/model.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,20 +56,20 @@ class AddBlueprintInput(Input):
5656

5757

5858
class AddDeviceInput(Input):
59-
address: typing.Optional[String]
60-
blueprint_id: typing.Optional[String] = Field(alias='blueprintId')
61-
device_size: typing.Optional[DeviceSize] = Field(alias='deviceSize')
62-
device_type: typing.Optional[String] = Field(alias='deviceType')
63-
label_ids: typing.Optional[list[String]] = Field(alias='labelIds')
64-
model: typing.Optional[String]
65-
mount_parameters: typing.Optional[String] = Field(alias='mountParameters')
59+
address: typing.Optional[String] = Field(default=None)
60+
blueprint_id: typing.Optional[String] = Field(default=None, alias='blueprintId')
61+
device_size: typing.Optional[DeviceSize] = Field(default=None, alias='deviceSize')
62+
device_type: typing.Optional[String] = Field(default=None, alias='deviceType')
63+
label_ids: typing.Optional[list[String]] = Field(default=None, alias='labelIds')
64+
model: typing.Optional[String] = Field(default=None)
65+
mount_parameters: typing.Optional[String] = Field(default=None, alias='mountParameters')
6666
name: String
67-
password: typing.Optional[String]
68-
port: typing.Optional[Int]
69-
service_state: typing.Optional[DeviceServiceState] = Field(alias='serviceState')
70-
username: typing.Optional[String]
71-
vendor: typing.Optional[String]
72-
version: typing.Optional[String]
67+
password: typing.Optional[String] = Field(default=None)
68+
port: typing.Optional[Int] = Field(default=None)
69+
service_state: typing.Optional[DeviceServiceState] = Field(default=None, alias='serviceState')
70+
username: typing.Optional[String] = Field(default=None)
71+
vendor: typing.Optional[String] = Field(default=None)
72+
version: typing.Optional[String] = Field(default=None)
7373
zone_id: String = Field(alias='zoneId')
7474

7575

@@ -80,8 +80,8 @@ class GraphNodeCoordinatesInput(Input):
8080

8181

8282
class UpdateBlueprintInput(Input):
83-
name: typing.Optional[String]
84-
template: typing.Optional[String]
83+
name: typing.Optional[String] = Field(default=None)
84+
template: typing.Optional[String] = Field(default=None)
8585

8686

8787
class AddBlueprintPayload(Payload):

utils/graphql-pydantic-converter/tests/render_models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@
44
from pydantic import PrivateAttr
55

66
from graphql_pydantic_converter.graphql_types import Input
7+
from graphql_pydantic_converter.graphql_types import Mutation
78
from graphql_pydantic_converter.graphql_types import Payload
89
from graphql_pydantic_converter.graphql_types import Query
910

11+
Boolean: typing.TypeAlias = bool
12+
Cursor: typing.TypeAlias = typing.Any
13+
Float: typing.TypeAlias = float
14+
ID: typing.TypeAlias = str
15+
Int: typing.TypeAlias = int
16+
Map: typing.TypeAlias = dict[str, typing.Any]
17+
String: typing.TypeAlias = str
18+
1019

1120
class TagAnd(Input):
1221
matches_all: typing.Optional[list[str]] = Field(default=None, alias='matchesAll')
@@ -109,3 +118,20 @@ class SchedulesQuery(Query):
109118
last: typing.Optional[int] = Field(default=None)
110119
filter: typing.Optional[SchedulesFilterInput] = Field(default=None)
111120
payload: ScheduleConnection
121+
122+
123+
class Resource(Payload):
124+
description: typing.Optional[Boolean] = Field(default=False, alias='Description')
125+
nested_pool: typing.Optional[ResourcePool] = Field(default=None, alias='NestedPool')
126+
parent_pool: typing.Optional[ResourcePool] = Field(default=None, alias='ParentPool')
127+
properties: typing.Optional[Boolean] = Field(default=False, alias='Properties')
128+
alternative_id: typing.Optional[Boolean] = Field(default=False, alias='AlternativeId')
129+
id: typing.Optional[Boolean] = Field(default=False)
130+
131+
132+
class ClaimResourceMutation(Mutation):
133+
_name: str = PrivateAttr('ClaimResource')
134+
pool_id: ID = Field(alias='poolId')
135+
description: typing.Optional[String] = Field(default=None)
136+
user_input: Map = Field(alias='userInput')
137+
payload: Resource

utils/graphql-pydantic-converter/tests/test_graphql_generator.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from model import BlueprintsQuery
1111
from pydantic import Field
1212
from render_models import AllocationStrategy
13+
from render_models import ClaimResourceMutation
1314
from render_models import PageInfoSchedule
15+
from render_models import Resource
1416
from render_models import ResourcePool
1517
from render_models import ResourcePoolConnection
1618
from render_models import ResourcePoolEdge
@@ -73,6 +75,21 @@ def test_render_mutation(self) -> None:
7375
).render()
7476
assert reference == mutation
7577

78+
reference = ('mutation { ClaimResource ( poolId: "00000000001", description: "<description>", userInput: '
79+
'{ address: "0.0.0.0", port: 80 }) { Properties AlternativeId id } }')
80+
mutation = ClaimResourceMutation(
81+
poolId='00000000001',
82+
description='<description>',
83+
userInput={'address': '0.0.0.0', 'port': 80},
84+
payload=Resource(
85+
id=True,
86+
Properties=True,
87+
AlternativeId=True
88+
)
89+
).render()
90+
91+
assert reference == mutation
92+
7693
def test_render_input(self) -> None:
7794
class DeviceSize(ENUM):
7895
SMALL = 'SMALL'
@@ -274,4 +291,3 @@ def test_multiple_query(self) -> None:
274291

275292
merged_query = graphql_pydantic_converter.graphql_types.concatenate_queries(queries)
276293
assert reference == merged_query
277-

0 commit comments

Comments
 (0)