Skip to content

Commit c5eb530

Browse files
committed
Do not render records using .dict() but usin JSOn
The `dict` method of the records are, by design, meant to just conver the record to a dictionary, without converting data types. If we want to serialize the records correcty, as we are doing when generating the SSM records, we need to rely on serialization. See [1] for more context. [1]: pydantic/pydantic#5155 (comment) Fixes: #113 SemVer: bugfix
1 parent 3ebe51b commit c5eb530

File tree

3 files changed

+161
-10
lines changed

3 files changed

+161
-10
lines changed

caso/messenger/ssm.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
"""Module containing the APEL SSM Messenger."""
1818

19-
import datetime
2019
import json
2120
import typing
2221
import warnings
@@ -27,7 +26,6 @@
2726
import dirq.QueueSimple
2827
from oslo_config import cfg
2928
from oslo_log import log
30-
import six
3129

3230
import caso.exception
3331
import caso.messenger
@@ -191,13 +189,13 @@ def push(self, records):
191189
for record in records:
192190
if isinstance(record, caso.record.CloudRecord):
193191
aux = []
194-
for k, v in six.iteritems(record.dict(**opts)):
195-
if v is not None:
196-
# FIXME(aloga): to be handled at record level.
197-
if isinstance(v, datetime.datetime):
198-
aux.append(f"{k}: {int(v.timestamp())}")
199-
else:
200-
aux.append(f"{k}: {v}")
192+
# NOTE(aloga): do not iter over the dictionary returned by record.dict()
193+
# as this is just a dictionary representation of the object, where no
194+
# serialization is done. In order to get objects correctly serialized
195+
# we need to convert to JSON, then reload the model
196+
serialized_record = json.loads(record.json(**opts))
197+
for k, v in serialized_record.items():
198+
aux.append(f"{k}: {v}")
201199
entries_cloud.append("\n".join(aux))
202200
elif isinstance(record, caso.record.IPRecord):
203201
entries_ip.append(record.json(**opts))

caso/record.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ def map_fields(value: str) -> str:
143143
}
144144
return d.get(value, value)
145145

146+
json_encoders = {datetime.datetime: lambda v: int(v.timestamp())}
146147
alias_generator = map_fields
147148
allow_population_by_field_name = True
148149
underscore_attrs_are_private = True
@@ -189,7 +190,7 @@ def map_fields(field: str) -> str:
189190
}
190191
return d.get(field, field)
191192

192-
json_encoders = {datetime: lambda v: int(v.timestamp())}
193+
json_encoders = {datetime.datetime: lambda v: int(v.timestamp())}
193194
alias_generator = map_fields
194195
allow_population_by_field_name = True
195196
underscore_attrs_are_private = True
@@ -262,6 +263,7 @@ def map_fields(field: str) -> str:
262263
}
263264
return d.get(field, field)
264265

266+
json_encoders = {datetime.datetime: lambda v: int(v.timestamp())}
265267
alias_generator = map_fields
266268
allow_population_by_field_name = True
267269
underscore_attrs_are_private = True

caso/tests/test_record.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright 2014 Spanish National Research Council (CSIC)
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
"""Test for cASO records."""
18+
19+
import datetime
20+
import json
21+
import uuid
22+
23+
import pytest
24+
25+
import caso.record
26+
27+
valid_record_fields = dict(
28+
uuid=str(uuid.uuid4()),
29+
site_name="TEST-Site",
30+
name="VM Name",
31+
user_id=str(uuid.uuid4()),
32+
group_id=str(uuid.uuid4()),
33+
fqan="VO FQAN",
34+
start_time=datetime.datetime.now() - datetime.timedelta(days=5),
35+
end_time=datetime.datetime.now(),
36+
compute_service="Fake Cloud Service",
37+
status="ACTIVE",
38+
image_id=str(uuid.uuid4()),
39+
user_dn="User DN",
40+
benchmark_type=None,
41+
benchmark_value=None,
42+
memory=16,
43+
cpu_count=8,
44+
disk=250,
45+
public_ip_count=7,
46+
)
47+
48+
valid_record = {
49+
"CloudComputeService": "Fake Cloud Service",
50+
"CpuCount": 8,
51+
"CpuDuration": 3456000,
52+
"Disk": 250,
53+
"StartTime": int(valid_record_fields["start_time"].timestamp()),
54+
"EndTime": int(valid_record_fields["end_time"].timestamp()),
55+
"FQAN": "VO FQAN",
56+
"GlobalUserName": "User DN",
57+
"ImageId": valid_record_fields["image_id"],
58+
"LocalGroupId": valid_record_fields["group_id"],
59+
"LocalUserId": valid_record_fields["user_id"],
60+
"MachineName": "VM Name",
61+
"Memory": 16,
62+
"PublicIPCount": 7,
63+
"SiteName": "TEST-Site",
64+
"Status": "ACTIVE",
65+
"VMUUID": valid_record_fields["uuid"],
66+
"WallDuration": 432000,
67+
}
68+
69+
70+
@pytest.mark.skip(reason="Pydantic 1 does not support computed fields")
71+
def test_cloud_record():
72+
"""Test a cloud record is correctly generated."""
73+
record = caso.record.CloudRecord(**valid_record_fields)
74+
75+
wall = datetime.timedelta(days=5).total_seconds()
76+
cpu = wall * record.cpu_count
77+
78+
assert record.wall_duration == wall
79+
assert record.cpu_duration == cpu
80+
81+
assert isinstance(record.start_time, datetime.datetime)
82+
assert isinstance(record.end_time, datetime.datetime)
83+
84+
85+
@pytest.mark.skip(reason="Pydantic 1 does not support computed fields")
86+
def test_cloud_record_map_opts():
87+
"""Test a cloud record is correctly rendered."""
88+
record = caso.record.CloudRecord(**valid_record_fields)
89+
90+
opts = {
91+
"by_alias": True,
92+
"exclude_unset": True,
93+
"exclude_none": True,
94+
}
95+
96+
assert json.loads(record.json(**opts)) == valid_record
97+
98+
99+
def test_cloud_record_map_opts_custom_wall_cpu():
100+
"""Test a cloud record is correctly rendered with custom wall and cpu times."""
101+
record = caso.record.CloudRecord(**valid_record_fields)
102+
103+
wall = datetime.timedelta(days=5).total_seconds()
104+
cpu = wall * record.cpu_count
105+
record.wall_duration = wall
106+
record.cpu_duration = cpu
107+
108+
opts = {
109+
"by_alias": True,
110+
"exclude_unset": True,
111+
"exclude_none": True,
112+
}
113+
114+
assert json.loads(record.json(**opts)) == valid_record
115+
116+
117+
@pytest.mark.skip(reason="Pydantic 1 does not support computed fields")
118+
def test_cloud_record_custom_wall():
119+
"""Test a cloud record is correctly rendered with custom wall time."""
120+
record = caso.record.CloudRecord(**valid_record_fields)
121+
122+
wall = 200
123+
cpu = wall * record.cpu_count
124+
record.wall_duration = wall
125+
assert record.wall_duration == wall
126+
assert record.cpu_duration == cpu
127+
128+
129+
def test_cloud_record_custom_wall_cpu():
130+
"""Test a cloud record is correctly generated with custom wall and cpu time."""
131+
record = caso.record.CloudRecord(**valid_record_fields)
132+
133+
wall = 200
134+
cpu = wall * record.cpu_count
135+
record.wall_duration = wall
136+
record.cpu_duration = cpu
137+
138+
assert record.wall_duration == wall
139+
assert record.cpu_duration == cpu
140+
141+
142+
@pytest.mark.skip(reason="Pydantic 1 does not support computed fields")
143+
def test_cloud_record_custom_cpu():
144+
"""Test a cloud record is correctly rendered with custom cpu time."""
145+
record = caso.record.CloudRecord(**valid_record_fields)
146+
147+
wall = datetime.timedelta(days=5).total_seconds()
148+
cpu = wall * record.cpu_count * 10
149+
record.cpu_duration = cpu
150+
assert record.wall_duration == wall
151+
assert record.cpu_duration == cpu

0 commit comments

Comments
 (0)