archaic 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
archaic/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ # flake8: noqa
2
+
3
+ from archaic.feature_class import FeatureClass
@@ -0,0 +1,210 @@
1
+ import arcpy
2
+ from typing import Any, Callable, Generic, Iterable, List, Optional, Set, TypeVar, Union
3
+ from archaic.info import Info
4
+ from archaic.predicate import to_sql
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ class FeatureClass(Generic[T]):
10
+ def __init__(self, data_path: str, **mapping: str) -> None:
11
+ """Initializes the feature class. e.g.
12
+
13
+ class City:
14
+ objectid: int
15
+ city_name: str
16
+ shape: arcpy.PointGeometry
17
+
18
+ cities_fc = FeatureClass [City] ('world.gdb/cities')
19
+
20
+ for city in cities_fc.read():
21
+ print(city.city_name, city.shape.WKT)
22
+
23
+ Args:
24
+ data_path (str): Feature class path.
25
+ mapping: Custom mapping of property to field.
26
+ """
27
+ self._data_path = data_path
28
+ self._mapping = mapping
29
+
30
+ @property
31
+ def info(self):
32
+ if not hasattr(self, "_info"):
33
+ self._info = Info[T](self)
34
+ return self._info
35
+
36
+ def read(
37
+ self,
38
+ filter: Union[
39
+ str, Callable[[T], bool], Iterable[int], Iterable[str], None
40
+ ] = None,
41
+ wkid: Optional[int] = None,
42
+ **kwargs: Any,
43
+ ) -> Iterable[T]:
44
+ """Queries the feature class.
45
+
46
+ Args:
47
+ filter: Where clause, lambda expression, object ids or global ids. Defaults to None.
48
+ wkid: Well-known id (e.g. 4326). Defaults to None.
49
+
50
+ Returns:
51
+ Iterable[T]: Strongly typed items.
52
+ """
53
+ if wkid is not None:
54
+ kwargs["spatial_reference"] = arcpy.SpatialReference(wkid)
55
+ data_path = self.info.data_path
56
+ fields = list(self.info.properties.values())
57
+ properties = self.info.properties
58
+ for where_clause in self._get_where_clauses_from_filter(filter):
59
+ with arcpy.da.SearchCursor(data_path, fields, where_clause, **kwargs) as cursor: # type: ignore
60
+ for row in cursor:
61
+ d = dict(zip(fields, row))
62
+ yield self._create(
63
+ **{p: d.get(f) if f else None for p, f in properties.items()}
64
+ )
65
+
66
+ def get(self, id: Union[int, str], wkid: Optional[int] = None) -> Optional[T]:
67
+ """Gets an item from the feature class.
68
+
69
+ Args:
70
+ id: Object id or global id.
71
+ wkid: Well-known id (e.g. 4326). Defaults to None.
72
+
73
+ Returns:
74
+ Optional[T]: Strongly typed item if found.
75
+ """
76
+ for where_clause in self._get_where_clauses_from_ids(id):
77
+ for item in self.read(where_clause, wkid):
78
+ return item
79
+ return None
80
+
81
+ def insert_many(self, items: Iterable[T], **kwargs: Any) -> List[int]:
82
+ data_path = self.info.data_path
83
+ fields = list(self.info.edit_properties.values())
84
+ properties = self.info.edit_properties
85
+ with arcpy.da.InsertCursor(data_path, fields, **kwargs) as cursor: # type: ignore
86
+ return [
87
+ cursor.insertRow(self._get_values(item, properties)) for item in items
88
+ ]
89
+
90
+ def insert(self, item: T) -> T:
91
+ return self.get(self.insert_many([item])[0]) # type: ignore
92
+
93
+ def update_where(
94
+ self,
95
+ filter: Union[str, Callable[[T], bool], Iterable[int], Iterable[str], None],
96
+ update: Callable[[T], Union[None, T]],
97
+ **kwargs: Any,
98
+ ) -> List[int]:
99
+ data_path = self.info.data_path
100
+ fields = list(self.info.edit_properties.values())
101
+ properties = self.info.edit_properties
102
+ ids: Set[int] = set()
103
+ for where_clause in self._get_where_clauses_from_filter(filter):
104
+ with arcpy.da.UpdateCursor(data_path, fields, where_clause, **kwargs) as cursor: # type: ignore
105
+ for row in cursor:
106
+ d = dict(zip(fields, row))
107
+ before = self._create(
108
+ **{p: d.get(f) if f else None for p, f in properties.items()}
109
+ )
110
+ result = update(before)
111
+ after = before if result is None else result
112
+ cursor.updateRow(self._get_values(after, properties))
113
+ ids.add(self._get_oid(before))
114
+ return list(ids)
115
+
116
+ def update(self, items: Union[T, List[T]]) -> List[int]:
117
+ items = list(items) if isinstance(items, Iterable) else [items]
118
+ cache = {self._get_oid(x): x for x in items}
119
+ ids: Set[int] = set()
120
+ for where_clause in self._get_where_clauses_from_ids(items):
121
+ for id in self.update_where(
122
+ where_clause, lambda x: cache[self._get_oid(x)]
123
+ ):
124
+ ids.add(id)
125
+ return list(ids)
126
+
127
+ def delete_where(self, filter: Union[str, Callable[[T], bool], None]) -> List[int]:
128
+ data_path = self.info.data_path
129
+ ids: Set[int] = set()
130
+ for where_clause in self._get_where_clauses_from_filter(filter):
131
+ with arcpy.da.UpdateCursor(data_path, self.info.oid_field, where_clause) as cursor: # type: ignore
132
+ for row in cursor:
133
+ cursor.deleteRow()
134
+ ids.add(row[0])
135
+ return list(ids)
136
+
137
+ def delete(
138
+ self, items: Union[T, int, str, Iterable[T], Iterable[int], Iterable[str]]
139
+ ) -> List[int]:
140
+ ids: Set[int] = set()
141
+ for where_clause in self._get_where_clauses_from_ids(items):
142
+ for id in self.delete_where(where_clause):
143
+ ids.add(id)
144
+ return list(ids)
145
+
146
+ def _create(self, **kwargs: Any) -> T:
147
+ if self.info.has_default_constructor:
148
+ item = self.info.model()
149
+ for property in self.info.properties:
150
+ setattr(item, property, kwargs.get(property))
151
+ return item
152
+ return self.info.model(
153
+ **{k: v for k, v in kwargs.items() if k in self.info.properties}
154
+ )
155
+
156
+ def _get_values(self, item: T, properties: Iterable[str]) -> List[Any]:
157
+ values: List[Any] = []
158
+ for property in properties:
159
+ values.append(getattr(item, property) if hasattr(item, property) else None)
160
+ return values
161
+
162
+ def _get_where_clauses_from_ids(
163
+ self, obj: Union[T, int, str, Iterable[T], Iterable[int], Iterable[str]]
164
+ ) -> List[str]:
165
+ where_clauses: List[str] = []
166
+ ids = list(self._get_ids(obj))
167
+ n = 1000
168
+ for chunk in [ids[i : i + n] for i in range(0, len(ids), n)]:
169
+ first = chunk[0]
170
+ if isinstance(first, int):
171
+ where_clauses.append(
172
+ f"{self.info.oid_field} IN ({','.join(map(str, chunk))})"
173
+ )
174
+ elif isinstance(first, str):
175
+ where_clauses.append(
176
+ f"GlobalID IN ({','.join(map(self._quote, chunk))})"
177
+ )
178
+ return where_clauses
179
+
180
+ def _get_where_clauses_from_filter(
181
+ self,
182
+ filter: Union[str, Callable[[T], bool], Iterable[int], Iterable[str], None],
183
+ ) -> List[str]:
184
+ if filter is None:
185
+ return [""]
186
+ if isinstance(filter, str):
187
+ return [filter]
188
+ if callable(filter):
189
+ return [to_sql(filter, self.info.properties)]
190
+ return self._get_where_clauses_from_ids(filter)
191
+
192
+ def _quote(self, value: Any) -> str:
193
+ return f"'{value}'"
194
+
195
+ def _get_ids(self, obj) -> Iterable[Union[int, str]]:
196
+ if isinstance(obj, int) or isinstance(obj, str):
197
+ yield obj
198
+ elif isinstance(obj, self.info.model):
199
+ yield self._get_oid(obj)
200
+ else:
201
+ for o in obj:
202
+ for id in self._get_ids(o):
203
+ yield id
204
+
205
+ def _get_oid(self, item) -> int:
206
+ if not self.info.oid_property:
207
+ raise TypeError(
208
+ f"'{self.info.model.__name__}' is missing the OID property."
209
+ )
210
+ return getattr(item, self.info.oid_property)
archaic/info.py ADDED
@@ -0,0 +1,82 @@
1
+ import arcpy
2
+ import re
3
+
4
+ from inspect import signature
5
+ from itertools import chain
6
+ from types import SimpleNamespace
7
+ from typing import TYPE_CHECKING, Dict, Generic, Set, Type, TypeVar
8
+
9
+ if TYPE_CHECKING:
10
+ from archaic.feature_class import FeatureClass
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class Info(Generic[T]):
16
+ def __init__(self, feature_class: "FeatureClass[T]") -> None:
17
+ if hasattr(feature_class, "__orig_class__"):
18
+ model = feature_class.__orig_class__.__args__[0] # type: ignore
19
+ else:
20
+ model = SimpleNamespace
21
+
22
+ keys = chain(
23
+ signature(model.__init__).parameters.keys(),
24
+ signature(model.__new__).parameters.keys(),
25
+ )
26
+
27
+ description = arcpy.Describe(feature_class._data_path)
28
+
29
+ # Members.
30
+ self.model: Type[T] = model # type: ignore
31
+ self.has_default_constructor = len(set(keys)) == 3
32
+ self.data_path: str = description.catalogPath # type: ignore
33
+ self.oid_field: str
34
+ self.oid_property: str
35
+ self.properties: Dict[str, str] = {}
36
+ self.edit_properties: Dict[str, str] = {}
37
+
38
+ # Inspect the fields.
39
+ upper_fields: Dict[str, str] = {}
40
+ upper_read_only_fields: Set[str] = set()
41
+ for field in description.fields: # type: ignore
42
+ if re.match(r"^(?!\d)[\w$]+$", field.name):
43
+ upper_fields[field.name.upper()] = field.name
44
+ if field.type == "OID":
45
+ self.oid_field = field.name
46
+ elif not field.editable:
47
+ upper_read_only_fields.add(field.name.upper())
48
+
49
+ def resolve_fields():
50
+ if model == SimpleNamespace:
51
+ upper_field_to_property: Dict[str, str] = {
52
+ f.upper(): p for p, f in feature_class._mapping.items()
53
+ }
54
+ for upper_field, field in upper_fields.items():
55
+ property = upper_field_to_property.get(upper_field) or field
56
+ if upper_field == "SHAPE":
57
+ field = "SHAPE@"
58
+ yield property, field
59
+ else:
60
+ for model_type in reversed(model.mro()):
61
+ if hasattr(model_type, "__annotations__"):
62
+ for property in model_type.__annotations__:
63
+ field = feature_class._mapping.get(property) or property
64
+ upper_field = field.upper()
65
+ if upper_field == "SHAPE":
66
+ field = "SHAPE@"
67
+ elif upper_field.startswith("SHAPE_"):
68
+ field = upper_field.replace("SHAPE_", "SHAPE@")
69
+ elif upper_field not in upper_fields:
70
+ raise ValueError(
71
+ f"Field '{field}' not found in {self.data_path}."
72
+ )
73
+ else:
74
+ field = upper_fields[upper_field]
75
+ yield property, field
76
+
77
+ for property, field in resolve_fields():
78
+ self.properties[property] = field
79
+ if field == self.oid_field:
80
+ self.oid_property = property
81
+ if field.upper() not in upper_read_only_fields:
82
+ self.edit_properties[property] = field
archaic/predicate.py ADDED
@@ -0,0 +1,155 @@
1
+ import ast
2
+ from _ast import Attribute, BoolOp, Call, Compare, Constant, Name
3
+ from datetime import datetime
4
+ from inspect import getsource
5
+ from typing import Any, Callable, Dict, List, TypeVar, Union
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ def to_sql(predicate: Callable[[T], bool], properties: Dict[str, str]) -> str:
11
+ class LambdaFinder(ast.NodeVisitor):
12
+ def __init__(self, expression: Any) -> None:
13
+ super().__init__()
14
+
15
+ self.freevars: Dict[str, Any] = {}
16
+
17
+ # Check globals.
18
+ for name in expression.__code__.co_names:
19
+ if name in expression.__globals__:
20
+ self.freevars[name] = expression.__globals__[name]
21
+
22
+ # Capture closure variables.
23
+ closure = expression.__closure__
24
+ if closure:
25
+ for name, value in zip(
26
+ expression.__code__.co_freevars, [x.cell_contents for x in closure]
27
+ ):
28
+ self.freevars[name] = value
29
+
30
+ line = getsource(expression).strip()
31
+
32
+ if line.endswith(":"):
33
+ line = f"{line}\n pass"
34
+
35
+ self.visit(ast.parse(line))
36
+
37
+ def visit_Lambda(self, node: ast.Lambda) -> Any: # pylint: disable-all
38
+ self.expression = node
39
+
40
+ @staticmethod
41
+ def find(expression: Any): # pylint: disable-all
42
+ visitor = LambdaFinder(expression)
43
+ return visitor.expression, visitor.freevars
44
+
45
+ class LambdaVisitor(ast.NodeVisitor):
46
+ def __init__(self, expression: ast.expr, freevars: Dict[str, Any]) -> None:
47
+ super().__init__()
48
+ self._expressions: List[Union[LambdaVisitor, str]] = []
49
+ self._freevars = freevars
50
+ self.visit(expression)
51
+
52
+ def visit_Attribute(self, node: Attribute) -> Any:
53
+ attr = node.attr
54
+ value: Any = node.value
55
+ if value.id in self._freevars:
56
+ self._expressions.append(
57
+ self._get_sql_value(getattr(self._freevars[value.id], attr))
58
+ )
59
+ else:
60
+ self._expressions.append(properties[attr])
61
+
62
+ def visit_BoolOp(self, node: BoolOp) -> Any:
63
+ self._expressions.append("(")
64
+ expressions: List[Union[LambdaVisitor, str]] = []
65
+ for value in node.values:
66
+ expressions.append(LambdaVisitor(value, self._freevars))
67
+ expressions.append(self._convert_op(node.op))
68
+ expressions.pop()
69
+ self._expressions.extend(expressions)
70
+ self._expressions.append(")")
71
+
72
+ def visit_Call(self, node: Call) -> Any:
73
+ if not hasattr(node.func, "attr"):
74
+ self.generic_visit(node)
75
+ return
76
+ attr = node.func.attr # type: ignore
77
+ if attr == "startswith":
78
+ field_name = properties[node.func.value.attr] # type: ignore
79
+ self._expressions.append(
80
+ f"{field_name} LIKE '{self._get_value(node.args[0])}%'"
81
+ )
82
+ elif attr == "endswith":
83
+ field_name = properties[node.func.value.attr] # type: ignore
84
+ self._expressions.append(
85
+ f"{field_name} LIKE '%{self._get_value(node.args[0])}'"
86
+ )
87
+
88
+ def visit_Compare(self, node: Compare) -> Any:
89
+ op = node.ops[0]
90
+ if isinstance(op, ast.In):
91
+ field_name = properties[node.comparators[0].attr] # type: ignore
92
+ self._expressions.append(
93
+ f"{field_name} LIKE '%{self._get_value(node.left)}%'"
94
+ )
95
+ else:
96
+ self._expressions.append(LambdaVisitor(node.left, self._freevars))
97
+ self._expressions.append(self._convert_op(node.ops[0]))
98
+ self._expressions.append(
99
+ LambdaVisitor(node.comparators[0], self._freevars)
100
+ )
101
+
102
+ def visit_Constant(self, node: Constant) -> Any:
103
+ self._expressions.append(self._get_sql_value(node.value))
104
+
105
+ def visit_Name(self, node: Name) -> Any:
106
+ self._expressions.append(self._get_sql_value(self._freevars[node.id]))
107
+
108
+ def _get_sql_value(self, value: Any) -> str:
109
+ if value is None:
110
+ return "NULL"
111
+ if isinstance(value, str):
112
+ return f"'{value}'"
113
+ if isinstance(value, datetime):
114
+ return f"timestamp '{value:%Y-%m-%d %H:%M:%S}'"
115
+ return str(value)
116
+
117
+ def _get_value(self, node: Any) -> Any:
118
+ return self._freevars[node.id] if isinstance(node, Name) else node.value
119
+
120
+ def _convert_op(self, op: Any) -> str:
121
+ if isinstance(op, ast.And):
122
+ return "AND"
123
+ if isinstance(op, ast.Or):
124
+ return "OR"
125
+ if isinstance(op, ast.Is):
126
+ return "IS"
127
+ if isinstance(op, ast.IsNot):
128
+ return "IS NOT"
129
+ if isinstance(op, ast.Eq):
130
+ return "="
131
+ if isinstance(op, ast.NotEq):
132
+ return "<>"
133
+ if isinstance(op, ast.Gt):
134
+ return ">"
135
+ if isinstance(op, ast.GtE):
136
+ return ">="
137
+ if isinstance(op, ast.Lt):
138
+ return "<"
139
+ if isinstance(op, ast.LtE):
140
+ return "<="
141
+ return type(op).__name__
142
+
143
+ def to_sql(self) -> str:
144
+ text = ""
145
+ for e in self._expressions:
146
+ text += e.to_sql() if isinstance(e, LambdaVisitor) else f" {e}"
147
+ return text
148
+
149
+ # Find the lambda expression and any free variables encapsulated in it.
150
+ expression, freevars = LambdaFinder.find(predicate)
151
+
152
+ # Generate a where clause.
153
+ where_clause = LambdaVisitor(expression, freevars).to_sql().strip()
154
+
155
+ return where_clause
@@ -0,0 +1,14 @@
1
+ Metadata-Version: 2.1
2
+ Name: archaic
3
+ Version: 0.1.0
4
+ Summary: Simplifies handling of ArcPy rows
5
+ Author-email: Jiro Shirota <jshirota@gmail.com>
6
+ Project-URL: Homepage, https://github.com/jshirota/archaic
7
+ Project-URL: Bug Tracker, https://github.com/jshirota/archaic/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+
14
+ # archaic
@@ -0,0 +1,8 @@
1
+ archaic/__init__.py,sha256=nwSLYBqzXoFULI9Kq8Ac0Q1QpQLj8a7r1-gMn1zV8w0,63
2
+ archaic/feature_class.py,sha256=iUKrmFOdIt51qY1CLA8H_vMTDjmOtObt1T4rKrJdrbY,8123
3
+ archaic/info.py,sha256=cWwh271vWg47Z1WVd98CTx3ApkBiNb6Iat17iO-BxcU,3465
4
+ archaic/predicate.py,sha256=pS1iy7z0914GgW4hg40cK8_69Q2Eto9uPSfctOyLSyw,6052
5
+ archaic-0.1.0.dist-info/METADATA,sha256=dgin4C5wH4EK8xjSodXNkJG-ldXH9IG0Az1QgBPWNlg,504
6
+ archaic-0.1.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
7
+ archaic-0.1.0.dist-info/top_level.txt,sha256=8OZUWuQkNdNWUgch_zC_PdHqKZHLXm_e9CXi88bS4os,8
8
+ archaic-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ archaic