Skip to content

Commit f2b8ecc

Browse files
authored
Refactor other fields to #1751 (#1760)
1 parent 02ce1af commit f2b8ecc

26 files changed

Lines changed: 880 additions & 815 deletions
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import Any, Literal, Optional
2+
3+
from .base_field_descriptor import BaseFieldDescriptor
4+
from .field_constraints import BaseConstraints
5+
6+
7+
class AnyFieldDescriptor(BaseFieldDescriptor):
8+
"""The field contains values of a unspecified or mixed type."""
9+
10+
type: Literal["any"] = "any"
11+
format: Optional[Literal["default"]] = None
12+
constraints: Optional[BaseConstraints[str]] = None
13+
14+
def read_value(self, cell: Any) -> Any:
15+
# Any field accepts any value as-is
16+
return cell
17+
18+
def write_value(self, cell: Any) -> Any:
19+
# Any field returns the value as-is
20+
return cell
21+

frictionless/fields/array.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import json
43
from typing import Any, Dict, Optional
54

65
import attrs
@@ -55,34 +54,6 @@ def cell_reader(cell: Any):
5554

5655
return cell_reader
5756

58-
def create_value_reader(self):
59-
# Create reader
60-
def value_reader(cell: Any): # type: ignore
61-
if not isinstance(cell, list):
62-
if isinstance(cell, str):
63-
try:
64-
cell = json.loads(cell)
65-
except Exception:
66-
return None
67-
if not isinstance(cell, list):
68-
return None
69-
elif isinstance(cell, tuple):
70-
cell = list(cell) # type: ignore
71-
else:
72-
return None
73-
return cell # type: ignore
74-
75-
return value_reader
76-
77-
# Write
78-
79-
def create_value_writer(self):
80-
# Create writer
81-
def value_writer(cell: Any):
82-
return json.dumps(cell)
83-
84-
return value_writer
85-
8657
# Metadata
8758

8859
metadata_profile_patch = {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from typing import Any, Literal, Optional
5+
6+
from pydantic import Field as PydanticField
7+
8+
from .base_field_descriptor import BaseFieldDescriptor
9+
from .field_constraints import JSONConstraints
10+
11+
12+
class ArrayFieldDescriptor(BaseFieldDescriptor):
13+
"""The field contains a valid JSON array."""
14+
15+
type: Literal["array"] = "array"
16+
format: Optional[Literal["default"]] = None
17+
constraints: Optional[JSONConstraints] = None
18+
# TODO: check later:
19+
# arrayItem in Frictionless schemas is an unnamed field-like descriptor to prevent using a full FieldDescriptor with "name" (backward compatibility)
20+
array_item: Optional[dict[str, Any]] = PydanticField(default=None, alias="arrayItem")
21+
22+
def read_value(self, cell: Any) -> Optional[list[Any]]:
23+
if not isinstance(cell, list):
24+
if isinstance(cell, str):
25+
try:
26+
cell = json.loads(cell)
27+
except Exception:
28+
return None
29+
if not isinstance(cell, list):
30+
return None
31+
elif isinstance(cell, tuple):
32+
cell = list(cell) # type: ignore[arg-type]
33+
else:
34+
return None
35+
return cell # type: ignore[return-value]
36+
37+
def write_value(self, cell: Any) -> str:
38+
return json.dumps(cell)
39+
40+

frictionless/fields/datetime.py

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
from __future__ import annotations
22

3-
from datetime import datetime
4-
from typing import Any
5-
63
import attrs
74

8-
from .. import settings
9-
from ..platform import platform
105
from ..schema import Field
116

127

@@ -20,44 +15,3 @@ class DatetimeField(Field):
2015
"maximum",
2116
"enum",
2217
]
23-
24-
# Read
25-
26-
# TODO: use different value_readers based on format (see string)
27-
def create_value_reader(self):
28-
# Create reader
29-
def value_reader(cell: Any):
30-
if not isinstance(cell, datetime):
31-
if not isinstance(cell, str):
32-
return None
33-
try:
34-
if self.format == "default":
35-
# Guard against shorter formats supported by dateutil
36-
assert cell[16] == ":"
37-
assert len(cell) >= 19
38-
cell = platform.dateutil_parser.isoparse(cell)
39-
elif self.format == "any":
40-
cell = platform.dateutil_parser.parse(cell)
41-
else:
42-
cell = datetime.strptime(cell, self.format)
43-
except Exception:
44-
return None
45-
return cell
46-
47-
return value_reader
48-
49-
# Write
50-
51-
def create_value_writer(self):
52-
# Create format
53-
format = self.format
54-
if format == settings.DEFAULT_FIELD_FORMAT:
55-
format = settings.DEFAULT_DATETIME_PATTERN
56-
57-
# Create writer
58-
def value_writer(cell: Any):
59-
cell = cell.strftime(format)
60-
cell = cell.replace("+0000", "Z")
61-
return cell
62-
63-
return value_writer
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import datetime
2+
from typing import Any, Literal, Optional
3+
4+
from .. import settings
5+
from ..platform import platform
6+
from .base_field_descriptor import BaseFieldDescriptor
7+
from .field_constraints import ValueConstraints
8+
9+
10+
class DatetimeFieldDescriptor(BaseFieldDescriptor):
11+
"""The field contains a date with a time."""
12+
13+
type: Literal["datetime"] = "datetime"
14+
format: Optional[str] = None
15+
constraints: Optional[ValueConstraints[datetime.datetime]] = None
16+
17+
def read_value(self, cell: Any) -> Optional[datetime.datetime]:
18+
if not isinstance(cell, datetime.datetime):
19+
if not isinstance(cell, str):
20+
return None
21+
try:
22+
format_value = self.format or "default"
23+
if format_value == "default":
24+
# Guard against shorter formats supported by dateutil
25+
assert cell[16] == ":"
26+
assert len(cell) >= 19
27+
cell = platform.dateutil_parser.isoparse(cell)
28+
elif format_value == "any":
29+
cell = platform.dateutil_parser.parse(cell)
30+
else:
31+
cell = datetime.datetime.strptime(cell, format_value)
32+
except Exception:
33+
return None
34+
return cell
35+
36+
def write_value(self, cell: Optional[datetime.datetime]) -> Optional[str]:
37+
if cell is None:
38+
return None
39+
format_value = self.format or "default"
40+
if format_value == settings.DEFAULT_FIELD_FORMAT:
41+
format_value = settings.DEFAULT_DATETIME_PATTERN
42+
result = cell.strftime(format_value)
43+
result = result.replace("+0000", "Z")
44+
return result

frictionless/fields/duration.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
from __future__ import annotations
22

3-
import datetime
4-
from typing import Any
5-
63
import attrs
74

8-
from ..platform import platform
95
from ..schema import Field
106

117

@@ -17,28 +13,3 @@ class DurationField(Field):
1713
"required",
1814
"enum",
1915
]
20-
21-
# Read
22-
23-
def create_value_reader(self):
24-
# Create reader
25-
def value_reader(cell: Any):
26-
if not isinstance(cell, (platform.isodate.Duration, datetime.timedelta)): # type: ignore
27-
if not isinstance(cell, str):
28-
return None
29-
try:
30-
cell = platform.isodate.parse_duration(cell) # type: ignore
31-
except Exception:
32-
return None
33-
return cell
34-
35-
return value_reader
36-
37-
# Write
38-
39-
def create_value_writer(self):
40-
# Create writer
41-
def value_writer(cell: Any): # type: ignore
42-
return platform.isodate.duration_isoformat(cell) # type: ignore
43-
44-
return value_writer
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import datetime
2+
from typing import Any, Literal, Optional
3+
4+
from ..platform import platform
5+
from .base_field_descriptor import BaseFieldDescriptor
6+
from .field_constraints import ValueConstraints
7+
8+
9+
class DurationFieldDescriptor(BaseFieldDescriptor):
10+
"""The field contains a duration of time."""
11+
12+
type: Literal["duration"] = "duration"
13+
format: Optional[Literal["default"]] = None
14+
constraints: Optional[ValueConstraints[str]] = None
15+
16+
def read_value(self, cell: Any) -> Any:
17+
if not isinstance(cell, (platform.isodate.Duration, datetime.timedelta)): # type: ignore
18+
if not isinstance(cell, str):
19+
return None
20+
try:
21+
cell = platform.isodate.parse_duration(cell) # type: ignore
22+
except Exception:
23+
return None
24+
return cell
25+
26+
def write_value(self, cell: Any) -> Optional[str]:
27+
if cell is None:
28+
return None
29+
return platform.isodate.duration_isoformat(cell) # type: ignore
30+
31+

0 commit comments

Comments
 (0)