stele1-datatypes 0.0.1__tar.gz

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.
@@ -0,0 +1,8 @@
1
+ Copyright (c) Stele Climbing contributors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: stele1-datatypes
3
+ Version: 0.0.1
4
+ Summary: Core datatypes for stele1 catalogs
5
+ Author-email: Daniel M <contact@steleclimbing.org>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://stele1.steleclimbing.org
8
+ Project-URL: Repository, https://codeberg.org/stele-climbing/stele1
9
+ Project-URL: Issues, https://codeberg.org/stele-climbing/stele1/issues
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Development Status :: 4 - Beta
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # stele1-datatypes
19
+
20
+ Core datatypes for [stele1](https://stele1.steleclimbing.org) catalogs.
21
+
22
+ Covers `Metadata`, `Climb`, `Area`, `Photo`, `Parking`, and `Approach`.
23
+ See the [stele1 spec](https://stele1.steleclimbing.org/spec/) for the data
24
+ model these implement.
25
+
26
+ Each datatype validates its input and exposes `from_data` / `to_data` for
27
+ serialization to and from plain dicts.
28
+
29
+ ## Use
30
+
31
+ ```python
32
+ from stele1.climb import Climb
33
+
34
+ climb = Climb.from_data({
35
+ 'uuid': '550e8400-e29b-41d4-a716-446655440000',
36
+ 'name': 'The Mandala',
37
+ 'rating': {'difficulty': 'V12'},
38
+ })
39
+
40
+ climb.to_data() # {'uuid': ..., 'name': ..., 'rating': ...}
41
+
42
+ climb.get_name().to_data() # 'The Mandala'
43
+ ```
44
+
45
+ Invalid input raises `TypeError` or `ValueError`.
46
+
47
+ `to_data` returns plain dicts.
48
+ `json.dumps(climb.to_data())` produces spec-compliant output;
49
+ formatting is up to the caller.
50
+
51
+ ## Install
52
+
53
+ ```
54
+ pip install stele1-datatypes
55
+ ```
56
+
57
+ ## License
58
+
59
+ MIT
@@ -0,0 +1,42 @@
1
+ # stele1-datatypes
2
+
3
+ Core datatypes for [stele1](https://stele1.steleclimbing.org) catalogs.
4
+
5
+ Covers `Metadata`, `Climb`, `Area`, `Photo`, `Parking`, and `Approach`.
6
+ See the [stele1 spec](https://stele1.steleclimbing.org/spec/) for the data
7
+ model these implement.
8
+
9
+ Each datatype validates its input and exposes `from_data` / `to_data` for
10
+ serialization to and from plain dicts.
11
+
12
+ ## Use
13
+
14
+ ```python
15
+ from stele1.climb import Climb
16
+
17
+ climb = Climb.from_data({
18
+ 'uuid': '550e8400-e29b-41d4-a716-446655440000',
19
+ 'name': 'The Mandala',
20
+ 'rating': {'difficulty': 'V12'},
21
+ })
22
+
23
+ climb.to_data() # {'uuid': ..., 'name': ..., 'rating': ...}
24
+
25
+ climb.get_name().to_data() # 'The Mandala'
26
+ ```
27
+
28
+ Invalid input raises `TypeError` or `ValueError`.
29
+
30
+ `to_data` returns plain dicts.
31
+ `json.dumps(climb.to_data())` produces spec-compliant output;
32
+ formatting is up to the caller.
33
+
34
+ ## Install
35
+
36
+ ```
37
+ pip install stele1-datatypes
38
+ ```
39
+
40
+ ## License
41
+
42
+ MIT
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "stele1-datatypes"
7
+ version = "0.0.1"
8
+ description = "Core datatypes for stele1 catalogs"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ authors = [{ name = "Daniel M", email = "contact@steleclimbing.org" }]
12
+ requires-python = ">=3.8"
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Operating System :: OS Independent",
16
+ "Development Status :: 4 - Beta",
17
+ ]
18
+
19
+ [project.urls]
20
+ Homepage = "https://stele1.steleclimbing.org"
21
+ Repository = "https://codeberg.org/stele-climbing/stele1"
22
+ Issues = "https://codeberg.org/stele-climbing/stele1/issues"
23
+
24
+ [tool.setuptools.packages.find]
25
+ namespaces = true
26
+ where = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,152 @@
1
+ class Name:
2
+ def __init__(self, n):
3
+ if not isinstance(n, str):
4
+ raise TypeError("approach name is not a string")
5
+ if n == "":
6
+ raise ValueError("approach name is an empty string")
7
+ self._name = n
8
+ @staticmethod
9
+ def from_data(j):
10
+ return Name(j)
11
+ def to_data(self):
12
+ return self._name
13
+
14
+ class Description:
15
+ def __init__(self, description):
16
+ if not isinstance(description, str):
17
+ raise TypeError("approach description is not a string")
18
+ if description == "":
19
+ raise ValueError("approach description is an empty string")
20
+ self._description = description
21
+ @staticmethod
22
+ def from_data(j):
23
+ return Description(j)
24
+ def to_data(self):
25
+ return self._description
26
+
27
+ class Path:
28
+ def __init__(self, points):
29
+ if not isinstance(points, list):
30
+ raise TypeError("approach path must be a list")
31
+ if len(points) < 2:
32
+ raise TypeError("approach path must have a least two points")
33
+ good = []
34
+ for longitude, latitude in points:
35
+ if not isinstance(longitude, (int, float)):
36
+ raise TypeError("approach path points' longitude must be a number")
37
+ if not isinstance(latitude, (int, float)):
38
+ raise TypeError("approach path points' latitude must be a number")
39
+ if longitude < -180 or 180 < longitude:
40
+ raise ValueError("approach path points' longitude must be between -180 and 180")
41
+ if latitude < -90 or 90 < latitude:
42
+ raise ValueError("approach location's latitude must be between -90 and 90")
43
+ good.append((longitude, latitude))
44
+ self._points = good
45
+ @staticmethod
46
+ def from_data(j):
47
+ return Path([(pt['longitude'], pt['latitude']) for pt in j])
48
+ def to_data(self):
49
+ return [{'longitude': lng, 'latitude': lat} for (lng, lat) in self._points]
50
+
51
+ class Uuid:
52
+ def __init__(self, a):
53
+ import uuid
54
+ if isinstance(a, str):
55
+ self._uuid = uuid.UUID(a)
56
+ elif isinstance(a, uuid.UUID):
57
+ self._uuid = a
58
+ else:
59
+ raise TypeError("approach uuid must be a string or UUID")
60
+ @staticmethod
61
+ def from_data(a):
62
+ import re
63
+ rex = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
64
+ if not isinstance(a, str):
65
+ raise TypeError("approach uuid must be a string")
66
+ if not rex.match(a):
67
+ raise ValueError("approach uuid string must look like a uuid: " +
68
+ "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
69
+ return Uuid(a)
70
+ def to_data(self):
71
+ return str(self._uuid)
72
+
73
+ class Approach:
74
+
75
+ def __init__(self):
76
+ self._description = None
77
+ self._name = None
78
+ self._path = None
79
+ self._uuid = None
80
+
81
+ @staticmethod
82
+ def from_data(d):
83
+ a = Approach()
84
+ for k, v in d.items():
85
+ if k == 'description':
86
+ a._description = Description.from_data(v)
87
+ elif k == 'name':
88
+ a._name = Name.from_data(v)
89
+ elif k == 'path':
90
+ a._path = Path.from_data(v)
91
+ elif k == 'uuid':
92
+ a._uuid = Uuid.from_data(v)
93
+ else:
94
+ raise ValueError('unexpected key ' + k)
95
+ return a
96
+
97
+ def to_data(self):
98
+ d = {}
99
+ if self._description:
100
+ d['description'] = self._description.to_data()
101
+ if self._name:
102
+ d['name'] = self._name.to_data()
103
+ if self._path:
104
+ d['path'] = self._path.to_data()
105
+ if self._uuid:
106
+ d['uuid'] = self._uuid.to_data()
107
+ return d
108
+
109
+ def get_description(self):
110
+ return self._description
111
+ def unset_description(self):
112
+ self._description = None
113
+ return self
114
+ def set_description(self, v):
115
+ if not isinstance(v, Description):
116
+ raise TypeError('arguments to set_description must be an instance of Description')
117
+ self._description = v
118
+ return self
119
+
120
+ def get_name(self):
121
+ return self._name
122
+ def unset_name(self):
123
+ self._name = None
124
+ return self
125
+ def set_name(self, v):
126
+ if not isinstance(v, Name):
127
+ raise TypeError('arguments to set_name must be an instance of Name')
128
+ self._name = v
129
+ return self
130
+
131
+ def get_path(self):
132
+ return self._path
133
+ def unset_path(self):
134
+ self._path = None
135
+ return self
136
+ def set_path(self, v):
137
+ if not isinstance(v, Path):
138
+ raise TypeError('arguments to set_path must be an instance of Path')
139
+ self._path = v
140
+ return self
141
+
142
+ def get_uuid(self):
143
+ return self._uuid
144
+ def unset_uuid(self):
145
+ self._uuid = None
146
+ return self
147
+ def set_uuid(self, v):
148
+ if not isinstance(v, Uuid):
149
+ raise TypeError('arguments to set_uuid must be an instance of Uuid')
150
+ self._uuid = v
151
+ return self
152
+
@@ -0,0 +1,320 @@
1
+ class Name:
2
+ def __init__(self, name):
3
+ if not isinstance(name, str):
4
+ raise TypeError("area name is not a string")
5
+ if name == "":
6
+ raise ValueError("area name is an empty string")
7
+ self._name = name
8
+ @staticmethod
9
+ def from_data(j):
10
+ return Name(j)
11
+ def to_data(self):
12
+ return self._name
13
+
14
+ class AlternateNames:
15
+ def __init__(self, names):
16
+ if not isinstance(names, list):
17
+ raise TypeError("area alternate names is not a list")
18
+ self._names = []
19
+ for n in names:
20
+ self._names.append(Name(n))
21
+ @staticmethod
22
+ def from_data(j):
23
+ return AlternateNames(j)
24
+ def to_data(self):
25
+ return [n.to_data() for n in self._names]
26
+
27
+ class Location:
28
+ # TODO: use these internally
29
+ # APPROXIMATION = 'approximation'
30
+ # PERIMETER = 'perimeter'
31
+ def __init__(self, type, coords):
32
+ if not isinstance(type, str):
33
+ raise TypeError("area location type must be a string")
34
+ if type == 'approximation':
35
+ self._type = 'approximation'
36
+ if not isinstance(coords, dict):
37
+ raise TypeError("an \"approximation\" area location coordinates must be a dict")
38
+ # TODO: check structure
39
+ if not isinstance(coords['latitude'], (int, float)):
40
+ raise TypeError("area location's latitude must be numbers")
41
+ if coords['latitude'] < -90 or 90 < coords['latitude']:
42
+ raise TypeError("area location's latitude must from -90 to 90")
43
+ if not isinstance(coords['longitude'], (int, float)):
44
+ raise TypeError("area location's longitude must be numbers")
45
+ if coords['longitude'] < -180 or 180 < coords['longitude']:
46
+ raise TypeError("area location's longitude must from -180 to 180")
47
+ if not isinstance(coords['meter_radius'], (int, float)):
48
+ raise TypeError("area location's meter_radius must be a number")
49
+ if coords['meter_radius'] <= 0:
50
+ raise TypeError("area location's meter_radius must be greater than 0")
51
+ self._coords = {
52
+ 'longitude': coords['longitude'],
53
+ 'latitude': coords['latitude'],
54
+ 'radius': coords['meter_radius'],
55
+ }
56
+ elif type == 'perimeter':
57
+ self._type = 'perimeter'
58
+ self._coords = []
59
+ if not isinstance(coords, list):
60
+ raise TypeError("a \"perimeter\" area location's coordinates must be a list")
61
+ if not isinstance(coords[0], dict):
62
+ raise ValueError("a \"perimeter\" area location must be an array of {'longitude': lng, 'latitude': lat} points")
63
+ if len(coords) < 4:
64
+ raise ValueError("a \"perimeter\" area location must have at least 4 points")
65
+ for ll in coords:
66
+ if not isinstance(ll['latitude'], (int, float)):
67
+ raise TypeError("area location's latitude must be numbers")
68
+ if ll['latitude'] < -90 or 90 < ll['latitude']:
69
+ raise TypeError("area location's latitude must from -90 to 90")
70
+ if not isinstance(ll['longitude'], (int, float)):
71
+ raise TypeError("area location's longitude must be numbers")
72
+ if ll['longitude'] < -180 or 180 < ll['longitude']:
73
+ raise TypeError("area location's longitude must from -180 to 180")
74
+ self._coords.append((ll['longitude'], ll['latitude']))
75
+ else:
76
+ raise ValueError("area location type must be a \"approximation\" or \"perimeter\"")
77
+ @staticmethod
78
+ def approximation(lngLat, meter_radius):
79
+ longitude, latitude = lngLat
80
+ return Location('approximation', {
81
+ 'longitude': longitude,
82
+ 'latitude': latitude,
83
+ 'meter_radius': meter_radius,
84
+ })
85
+ @staticmethod
86
+ def perimeter(pts):
87
+ return Location('perimeter', [{
88
+ 'latitude': lat,
89
+ 'longitude': lng
90
+ } for (lng, lat) in pts])
91
+ @staticmethod
92
+ def from_data(j):
93
+ if j['type'] == 'approximation':
94
+ return Location('approximation', j['circle'])
95
+ if j['type'] == 'perimeter':
96
+ return Location('perimeter', j['polygon'])
97
+ def to_data(self):
98
+ if self._type == 'approximation':
99
+ return {
100
+ 'type': self._type,
101
+ 'circle': {
102
+ 'longitude': self._coords['longitude'],
103
+ 'latitude': self._coords['latitude'],
104
+ 'meter_radius': self._coords['radius'],
105
+ },
106
+ }
107
+ if self._type == 'perimeter':
108
+ return {
109
+ 'type': self._type,
110
+ 'polygon': [{'latitude': lat, 'longitude': lng} for (lng, lat) in self._coords],
111
+ }
112
+
113
+ class Tags:
114
+ def __init__(self, tags):
115
+ if not isinstance(tags, list):
116
+ raise TypeError("area tags must be a list")
117
+ self._tags = []
118
+ for tag in tags:
119
+ if not isinstance(tag, str):
120
+ raise TypeError("area tags must be a string")
121
+ if tag == "":
122
+ raise ValueError("area tag must be a non-empty string")
123
+ self._tags.append(tag)
124
+ @staticmethod
125
+ def from_data(j):
126
+ return Tags(j)
127
+ def to_data(self):
128
+ return [t for t in self._tags]
129
+
130
+ class Fields:
131
+ def __init__(self, fields):
132
+ if not isinstance(fields, dict):
133
+ raise TypeError("area fields must be a dict")
134
+ self._fields = {}
135
+ for f, v in fields.items():
136
+ if not isinstance(v, str):
137
+ raise TypeError("area field values must be a string")
138
+ self._fields[f] = v
139
+ @staticmethod
140
+ def from_data(j):
141
+ return Fields(j)
142
+ def to_data(self):
143
+ return self._fields.copy()
144
+
145
+ class Notes:
146
+ def __init__(self, notes):
147
+ if not isinstance(notes, list):
148
+ raise TypeError("area notes must be a list")
149
+ good = []
150
+ topics = set()
151
+ for (topic, content) in notes:
152
+ if not isinstance(topic, str):
153
+ raise TypeError("area note topic must be a string")
154
+ if topic == '':
155
+ raise ValueError("area notes topic must not be empty")
156
+ if topic in topics:
157
+ raise ValueError("area notes topics must all be unique")
158
+ if not isinstance(content, str):
159
+ raise TypeError("area note topic must be a string")
160
+ if topic == '':
161
+ raise ValueError("area notes topic must not be empty")
162
+ topics.add(topic)
163
+ good.append((topic, content))
164
+ self._notes = good
165
+ @staticmethod
166
+ def from_data(j):
167
+ return Notes([(n['topic'], n['content']) for n in j])
168
+ def to_data(self):
169
+ return [{'topic': t, 'content': c} for (t, c) in self._notes]
170
+
171
+ class Uuid:
172
+ def __init__(self, a):
173
+ import uuid
174
+ if isinstance(a, str):
175
+ self._uuid = uuid.UUID(a)
176
+ elif isinstance(a, uuid.UUID):
177
+ self._uuid = a
178
+ else:
179
+ raise TypeError("area uuid must be a string or UUID")
180
+ @staticmethod
181
+ def from_data(a):
182
+ import re
183
+ rex = re.compile('[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
184
+ if not isinstance(a, str):
185
+ raise TypeError("area uuid must be a string")
186
+ if not rex.match(a):
187
+ raise ValueError("area uuid string must look like a uuid: " +
188
+ "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")
189
+ return Uuid(a)
190
+ def to_data(self):
191
+ return str(self._uuid)
192
+
193
+ class Area:
194
+
195
+ def __init__(self):
196
+ self._alternate_names = None
197
+ self._fields = None
198
+ self._location = None
199
+ self._name = None
200
+ self._notes = None
201
+ self._tags = None
202
+ self._uuid = None
203
+
204
+ @staticmethod
205
+ def from_data(d):
206
+ a = Area()
207
+ for k, v in d.items():
208
+ if k == 'alternate_names':
209
+ a._alternate_names = AlternateNames.from_data(v)
210
+ elif k == 'fields':
211
+ a._fields = Fields.from_data(v)
212
+ elif k == 'location':
213
+ a._location = Location.from_data(v)
214
+ elif k == 'name':
215
+ a._name = Name.from_data(v)
216
+ elif k == 'notes':
217
+ a._notes = Notes.from_data(v)
218
+ elif k == 'tags':
219
+ a._tags = Tags.from_data(v)
220
+ elif k == 'uuid':
221
+ a._uuid = Uuid.from_data(v)
222
+ else:
223
+ raise ValueError('unexpected key ' + k)
224
+ return a
225
+
226
+ def to_data(self):
227
+ d = {}
228
+ if self._alternate_names:
229
+ d['alternate_names'] = self._alternate_names.to_data()
230
+ if self._fields:
231
+ d['fields'] = self._fields.to_data()
232
+ if self._location:
233
+ d['location'] = self._location.to_data()
234
+ if self._name:
235
+ d['name'] = self._name.to_data()
236
+ if self._notes:
237
+ d['notes'] = self._notes.to_data()
238
+ if self._tags:
239
+ d['tags'] = self._tags.to_data()
240
+ if self._uuid:
241
+ d['uuid'] = self._uuid.to_data()
242
+ return d
243
+
244
+ def get_alternate_names(self):
245
+ return self._alternate_names
246
+ def unset_alternate_names(self):
247
+ self._alternate_names = None
248
+ return self
249
+ def set_alternate_names(self, v):
250
+ if not isinstance(v, AlternateNames):
251
+ raise TypeError('arguments to set_alternate_names must be an instance of AlternateNames')
252
+ self._alternate_names = v
253
+ return self
254
+
255
+ def get_fields(self):
256
+ return self._fields
257
+ def unset_fields(self):
258
+ self._fields = None
259
+ return self
260
+ def set_fields(self, v):
261
+ if not isinstance(v, Fields):
262
+ raise TypeError('arguments to set_fields must be an instance of Fields')
263
+ self._fields = v
264
+ return self
265
+
266
+ def get_location(self):
267
+ return self._location
268
+ def unset_location(self):
269
+ self._location = None
270
+ return self
271
+ def set_location(self, v):
272
+ if not isinstance(v, Location):
273
+ raise TypeError('arguments to set_location must be an instance of Location')
274
+ self._location = v
275
+ return self
276
+
277
+ def get_name(self):
278
+ return self._name
279
+ def unset_name(self):
280
+ self._name = None
281
+ return self
282
+ def set_name(self, v):
283
+ if not isinstance(v, Name):
284
+ raise TypeError('arguments to set_name must be an instance of Name')
285
+ self._name = v
286
+ return self
287
+
288
+ def get_notes(self):
289
+ return self._notes
290
+ def unset_notes(self):
291
+ self._notes = None
292
+ return self
293
+ def set_notes(self, v):
294
+ if not isinstance(v, Notes):
295
+ raise TypeError('arguments to set_notes must be an instance of Notes')
296
+ self._notes = v
297
+ return self
298
+
299
+ def get_tags(self):
300
+ return self._tags
301
+ def unset_tags(self):
302
+ self._tags = None
303
+ return self
304
+ def set_tags(self, v):
305
+ if not isinstance(v, Tags):
306
+ raise TypeError('arguments to set_tags must be an instance of Tags')
307
+ self._tags = v
308
+ return self
309
+
310
+ def get_uuid(self):
311
+ return self._uuid
312
+ def unset_uuid(self):
313
+ self._uuid = None
314
+ return self
315
+ def set_uuid(self, v):
316
+ if not isinstance(v, Uuid):
317
+ raise TypeError('arguments to set_uuid must be an instance of Uuid')
318
+ self._uuid = v
319
+ return self
320
+