oarepo-runtime 2.0.0.dev38__py3-none-any.whl → 2.0.0.dev39__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.
@@ -19,6 +19,6 @@ from .api import Model
19
19
  from .ext import OARepoRuntime
20
20
  from .proxies import current_runtime
21
21
 
22
- __version__ = "2.0.0dev38"
22
+ __version__ = "2.0.0dev39"
23
23
 
24
24
  __all__ = ("Model", "OARepoRuntime", "__version__", "current_runtime")
oarepo_runtime/api.py CHANGED
@@ -153,6 +153,7 @@ class Model[
153
153
  model_metadata: ModelMetadata | None = None,
154
154
  features: Mapping[str, Any] | None = None,
155
155
  imports: list[Import] | None = None,
156
+ ui_blueprint_name: str | None = None,
156
157
  ):
157
158
  """Initialize the model configuration.
158
159
 
@@ -179,6 +180,7 @@ class Model[
179
180
  :param features: Features of the model. Filled by the feature presets themselves during registration.
180
181
  :param imports: List of import formats that can be used to import the record.
181
182
  If not provided, no imports are available.
183
+ :param ui_blueprint_name: Name of the UI blueprint
182
184
  """
183
185
  self._code = code
184
186
  self._name = name
@@ -203,6 +205,7 @@ class Model[
203
205
  self._imports = imports or []
204
206
  self._model_metadata = model_metadata
205
207
  self._features = features
208
+ self._ui_blueprint_name = ui_blueprint_name
206
209
 
207
210
  @property
208
211
  def code(self) -> str:
@@ -302,6 +305,11 @@ class Model[
302
305
  """Get the API blueprint name for the model."""
303
306
  return cast("str", self.resource_config.blueprint_name)
304
307
 
308
+ @property
309
+ def ui_blueprint_name(self) -> str | None:
310
+ """Get the API blueprint name for the model."""
311
+ return self._ui_blueprint_name
312
+
305
313
  @property
306
314
  def record_pid_type(self) -> str | None:
307
315
  """Get the PID type for the model."""
@@ -342,6 +350,12 @@ class Model[
342
350
  """Get the API URL for the model."""
343
351
  return cast("str", invenio_url_for(f"{self.api_blueprint_name}.{view_name}", **kwargs))
344
352
 
353
+ def ui_url(self, view_name: str, **kwargs: Any) -> str | None:
354
+ """Get the UI URL for the model."""
355
+ if self.ui_blueprint_name is None:
356
+ return None
357
+ return cast("str", invenio_url_for(f"{self.ui_blueprint_name}.{view_name}", **kwargs))
358
+
345
359
  @cached_property
346
360
  def resource_config(self) -> RC:
347
361
  """Get the resource configuration."""
@@ -374,6 +388,13 @@ class Model[
374
388
  """Get all exportable response handlers."""
375
389
  return self._exports
376
390
 
391
+ def get_export_by_mimetype(self, mimetype: str) -> Export | None:
392
+ """Get an export by mimetype."""
393
+ for export in self._exports:
394
+ if export.mimetype == mimetype:
395
+ return export
396
+ return None
397
+
377
398
  @property
378
399
  def response_handlers(self) -> dict[str, ResponseHandler]:
379
400
  """Get all response handlers from the resource configuration."""
oarepo_runtime/ext.py CHANGED
@@ -177,6 +177,12 @@ class OARepoRuntime:
177
177
  raise ValueError("Need to pass a record instance, got None")
178
178
  return self.get_record_service_for_record_class(type(record))
179
179
 
180
+ def get_model_for_record(self, record: Any) -> Model:
181
+ """Retrieve the associated service for a given record."""
182
+ if record is None:
183
+ raise ValueError("Need to pass a record instance, got None")
184
+ return self.get_model_for_record_class(type(record))
185
+
180
186
  def get_file_service_for_record(self, record: Any) -> FileService | None:
181
187
  """Return the file service for the given record (draft or published)."""
182
188
  model = self.models_by_record_class.get(type(record))
@@ -188,13 +194,16 @@ class OARepoRuntime:
188
194
  return model.file_service
189
195
 
190
196
  def get_record_service_for_record_class(self, record_cls: type[RecordBase]) -> RecordService:
197
+ """Retrieve the service associated with a given record class."""
198
+ return self.get_model_for_record_class(record_cls).service
199
+
200
+ def get_model_for_record_class(self, record_cls: type[RecordBase]) -> Model:
191
201
  """Retrieve the service associated with a given record class."""
192
202
  for t in record_cls.mro():
193
203
  if t is RecordBase:
194
204
  break
195
205
  if t in self.models_by_record_class:
196
- model = self.models_by_record_class[t]
197
- return model.service
206
+ return self.models_by_record_class[t]
198
207
  raise KeyError(f"No service found for record class '{record_cls.__name__}'.")
199
208
 
200
209
  @cached_property
@@ -0,0 +1,365 @@
1
+ #
2
+ # Copyright (c) 2025 CESNET z.s.p.o.
3
+ #
4
+ # This file is a part of oarepo-runtime (see http://github.com/oarepo/oarepo-runtime).
5
+ #
6
+ # oarepo-runtime is free software; you can redistribute it and/or modify it
7
+ # under the terms of the MIT License; see LICENSE file for more details.
8
+ #
9
+ """Signposting functionality.
10
+
11
+ Functions to create a list of signpost links record's landing page, export formats and file contents.
12
+ Separate functions to create a complete linkset for the record item in application/linkset or application/linkset+json format.
13
+ Function to format the linkset into a HTTP Link header.
14
+
15
+ Information about relation types can be found at: https://signposting.org/FAIR/#reltypes
16
+ Excerpt with explanations about relation types:
17
+ author = The target of the link is a URI for an author of the resource that is the origin of the link.
18
+ cite-as = The target of the link is a persistent URI for the resource that is the origin of the link. When accessing the persistent URI, it redirects to that origin resource.
19
+ describedby = The target of the link provides metadata that describes the resource that is the origin of the link. It is the inverse of the describes relation type.
20
+ describes = The origin of the link is a resource that provides metadata that describes the resource that is the target of the link. It is the inverse of the describedby relation type.
21
+ type = The target of the link is the URI for a class of resources to which the resource that is the origin of the link belongs.
22
+ license = The target of the link is the URI of a license that applies to the resource that is the origin of the link.
23
+ item = The origin of the link is a collection of resources and the target of the link is a resource that belongs to that collection. It is the inverse of the collection relation type.
24
+ collection = The origin of the link is a resource that belongs to a collection and the target of the link is the collection to which it belongs. It is the inverse of the item relation type.
25
+
26
+ item <-> collection
27
+ describedby <-> describes
28
+ """ # noqa: E501
29
+
30
+ from __future__ import annotations
31
+
32
+ from collections import defaultdict
33
+ from typing import TYPE_CHECKING, Any, Literal, cast, overload
34
+ from urllib.parse import urljoin
35
+
36
+ if TYPE_CHECKING:
37
+ from invenio_records_resources.services.records.results import RecordItem
38
+ from signposting import AbsoluteURI, LinkRel, Signpost
39
+
40
+ from oarepo_runtime.proxies import current_runtime
41
+ from oarepo_runtime.typing import record_from_result
42
+
43
+
44
+ def signpost_link_to_str(signpost_link: Signpost) -> str:
45
+ """Convert a signpost link to string."""
46
+ link_str = str(signpost_link)
47
+ if link_str[:6] == "Link: ":
48
+ return f"{link_str[6:]}"
49
+ raise ValueError(f"Invalid signpost link: {link_str}") # pragma: no cover
50
+
51
+
52
+ def signpost_link_to_dict(link: Signpost) -> dict[str, Any]:
53
+ """Convert signpost link to a dictionary."""
54
+ link_dict: dict[str, Any] = {"href": link.target}
55
+ if link.type:
56
+ link_dict["type"] = link.type
57
+ return link_dict
58
+
59
+
60
+ def signpost_link_to_additional_link(
61
+ link: Signpost, landing_page_url: str, as_dict: bool = True
62
+ ) -> Signpost | dict[str, Any] | None:
63
+ """Transform signpost link to additional link with inversed relation type..
64
+
65
+ Args:
66
+ link: A signpost link to transform
67
+ landing_page_url: landing page url which will be passed to href
68
+ as_dict: if true, return dict, else return Signpost
69
+
70
+ Returns: Additional link as dict or Signpost or None if relation type does no thave additional link.
71
+
72
+ """
73
+ match link.rel:
74
+ case LinkRel.item:
75
+ if as_dict:
76
+ return {
77
+ "anchor": link.target,
78
+ str(LinkRel.collection): [{"href": landing_page_url, "type": "text/html"}],
79
+ }
80
+ return Signpost(
81
+ rel=LinkRel.collection, target=landing_page_url, media_type="text/html", context=link.target
82
+ )
83
+ case LinkRel.describedby:
84
+ if as_dict:
85
+ return {
86
+ "anchor": link.target,
87
+ str(LinkRel.describes): [{"href": landing_page_url, "type": "text/html"}],
88
+ }
89
+ return Signpost(rel=LinkRel.describes, target=landing_page_url, media_type="text/html", context=link.target)
90
+ case LinkRel.cite_as:
91
+ return None
92
+ # anchor is generated only for item & describedby, not for license
93
+ case _:
94
+ return None
95
+
96
+
97
+ def anchor_signpost_link(signpost_link: Signpost, anchor_url: str) -> Signpost:
98
+ """Add anchor to a signpost link."""
99
+ signpost_link.context = AbsoluteURI(anchor_url)
100
+ return signpost_link
101
+
102
+
103
+ @overload
104
+ def get_additional_links(
105
+ list_of_signpost_links: list[Signpost], landing_page_url: str, as_dict: Literal[True] = True
106
+ ) -> list[dict[str, Any]]: ...
107
+
108
+
109
+ @overload
110
+ def get_additional_links(
111
+ list_of_signpost_links: list[Signpost], landing_page_url: str, as_dict: Literal[False]
112
+ ) -> list[Signpost]: ...
113
+
114
+
115
+ def get_additional_links(
116
+ list_of_signpost_links: list[Signpost], landing_page_url: str, as_dict: bool = True
117
+ ) -> list[Signpost] | list[dict[str, Any]]:
118
+ """Create a list of additional links from a list of signpost links.
119
+
120
+ Args:
121
+ list_of_signpost_links: list of signpost link objects to be formatted
122
+ landing_page_url: landing page url
123
+ as_dict: if true, return a list of signpost link dicts, else return a list of Signpost link objects
124
+ Returns: list of signpost link dicts or list of Signpost link objects
125
+
126
+ """
127
+ results = [
128
+ result
129
+ for signpost_link in list_of_signpost_links
130
+ if (
131
+ result := signpost_link_to_additional_link(
132
+ link=signpost_link, landing_page_url=landing_page_url, as_dict=as_dict
133
+ )
134
+ )
135
+ is not None
136
+ ]
137
+ if as_dict:
138
+ return cast("list[dict[str, Any]]", results)
139
+ return cast("list[Signpost]", results)
140
+
141
+
142
+ def list_of_signpost_links_to_http_header(links_list: list[Signpost]) -> str:
143
+ """Create an HTTP Link header from a list of signpost links.
144
+
145
+ Args:
146
+ links_list: list of signpost link objects to be formatted
147
+
148
+ Returns: signpost header with formatted links.
149
+
150
+ """
151
+ links = [str(link)[6:] for link in links_list if str(link)[:6] == "Link: "]
152
+ return f"Link: {', '.join(links)}"
153
+
154
+
155
+ def create_linkset(datacite_dict: dict, record_item: RecordItem) -> str:
156
+ """Create a linkset for the record item in the application/linkset format.
157
+
158
+ Args:
159
+ datacite_dict: dictionary with datacite data
160
+ record_item: record item, for which signpost links should be generated
161
+
162
+ Returns: linkset in string format
163
+
164
+ """
165
+ landing_page_url = record_item.links.get("self_html")
166
+ # just sanity check, we don't expect this to happen, not covered in tests
167
+ if not landing_page_url: # pragma: no cover
168
+ return ""
169
+ landing_page_links = landing_page_signpost_links_list(datacite_dict, record_item, short=False)
170
+ additional_links: list[Signpost] = get_additional_links(landing_page_links, landing_page_url, as_dict=False)
171
+ anchored_links = [
172
+ anchor_signpost_link(signpost_link, landing_page_url) for signpost_link in landing_page_links
173
+ ] + additional_links
174
+ links = [str(link)[6:] for link in anchored_links if str(link)[:6] == "Link: "]
175
+ return ", ".join(links)
176
+
177
+
178
+ def create_linkset_json(datacite_dict: dict, record_item: RecordItem) -> dict[str, list[dict[str, Any]]]:
179
+ """Create a linkset for the record item in the application/linkset+json format.
180
+
181
+ Args:
182
+ datacite_dict: dictionary with datacite data
183
+ record_item: record item, for which signpost links should be generated
184
+
185
+ Returns: linkset in JSON format
186
+
187
+ """
188
+ landing_page_url = record_item.links.get("self_html")
189
+ # just sanity check, we don't expect this to happen, not covered in tests
190
+ if not landing_page_url: # pragma: no cover
191
+ return {}
192
+ landing_page_links = landing_page_signpost_links_list(datacite_dict, record_item, short=False)
193
+ dict_of_links_by_relation = defaultdict(list)
194
+ for link in landing_page_links:
195
+ dict_of_links_by_relation[str(link.rel)].append(link)
196
+ links_json = defaultdict(list)
197
+ links_json["anchor"] = landing_page_url
198
+
199
+ additional_links: list[dict[str, Any]] = get_additional_links(landing_page_links, landing_page_url)
200
+ for link_relation_from_dict, list_of_links_for_relation in dict_of_links_by_relation.items():
201
+ for link in list_of_links_for_relation:
202
+ links_json[link_relation_from_dict].append(signpost_link_to_dict(link))
203
+
204
+ return {"linkset": [dict(links_json), *[x for x in additional_links if x]]}
205
+
206
+
207
+ def file_content_signpost_links_list(record_item: RecordItem) -> list[Signpost]:
208
+ """Create a list of signpost links for the file content of the record item.
209
+
210
+ Args:
211
+ record_item: record item with the file to generate a signpost link for
212
+
213
+ Returns: list with the signpost link for the file content
214
+
215
+ """
216
+ record = record_from_result(record_item)
217
+ model = current_runtime.get_model_for_record(record)
218
+ landing_page_url = model.ui_url(view_name="record_detail", pid_value=record.pid.pid_value)
219
+ if not landing_page_url: # pragma: no cover
220
+ return []
221
+ return [
222
+ Signpost(
223
+ rel=LinkRel.linkset,
224
+ target=landing_page_url,
225
+ media_type="application/linkset",
226
+ ),
227
+ Signpost(
228
+ rel=LinkRel.linkset,
229
+ target=landing_page_url,
230
+ media_type="application/linkset+json",
231
+ ),
232
+ Signpost(
233
+ rel=LinkRel.collection,
234
+ target=landing_page_url,
235
+ media_type="text/html",
236
+ ),
237
+ ]
238
+
239
+
240
+ def export_format_signpost_links_list(record_item: RecordItem) -> list[Signpost]:
241
+ """Create a list of signpost links for the export format of the record item.
242
+
243
+ Args:
244
+ record_item: record item with the export format to generate a signpost link for
245
+ code: code of the export format
246
+
247
+ Returns: list with the signpost link for the export format
248
+
249
+ """
250
+ return [
251
+ Signpost(
252
+ rel=LinkRel.linkset,
253
+ target=record_item.links["self_html"],
254
+ media_type="application/linkset",
255
+ ),
256
+ Signpost(
257
+ rel=LinkRel.linkset,
258
+ target=record_item.links["self_html"],
259
+ media_type="application/linkset+json",
260
+ ),
261
+ Signpost(rel=LinkRel.describes, target=record_item.links["self_html"], media_type="text/html"),
262
+ ]
263
+
264
+
265
+ def landing_page_signpost_links_list(datacite_dict: dict, record_item: RecordItem, short: bool) -> list[Signpost]:
266
+ """Create a list of signpost links for the landing page of the record item.
267
+
268
+ Args:
269
+ datacite_dict: dictionary with datacite data
270
+ record_item: record item, for which signpost links should be generated
271
+ short: If true, lists only the first three links for relations with greater count
272
+
273
+ Returns: list of signpost links for the landing page
274
+
275
+ """
276
+ signposting_links: list[Signpost] = []
277
+ record = record_from_result(record_item)
278
+ record_data = record_item.data # self.html
279
+ record_files = record_data.get("files", {}).get("entries", {})
280
+ model = current_runtime.get_model_for_record(record)
281
+
282
+ # author - prvni tri
283
+ data = datacite_dict["data"]
284
+ attributes = data["attributes"]
285
+ creators = attributes.get("creators", [])
286
+ if short:
287
+ creators = creators[:3]
288
+ for attribute in creators:
289
+ signposting_links.extend(
290
+ Signpost(rel=LinkRel.author, target=name_identifier["nameIdentifier"])
291
+ for name_identifier in attribute["nameIdentifiers"]
292
+ )
293
+
294
+ # cite-as = DOI
295
+ signposting_links.append(Signpost(rel=LinkRel.cite_as, target=urljoin("https://doi.org/", attributes.get("doi"))))
296
+
297
+ # describedby
298
+ for model_export in model.exports:
299
+ model_export_url = model.ui_url(
300
+ view_name="export", pid_value=record.pid.pid_value, export_format=model_export.code
301
+ )
302
+ # just sanity check, we don't expect this to happen, not covered in tests
303
+ if not model_export_url: # pragma: no cover
304
+ continue
305
+ signposting_links.append(
306
+ Signpost(rel=LinkRel.describedby, target=model_export_url, media_type=model_export.mimetype)
307
+ )
308
+
309
+ # item
310
+ record_file_values = record_files.values()
311
+ if short:
312
+ record_file_values = list(record_file_values)[:3]
313
+ record_files_url = record_item.links.get("files")
314
+ if record_files_url:
315
+ signposting_links.extend(
316
+ Signpost(
317
+ rel=LinkRel.item,
318
+ media_type=record_file.get("mimetype"),
319
+ target=f"{record_files_url}/{record_file.get('key')}",
320
+ )
321
+ for record_file in record_file_values
322
+ )
323
+
324
+ # license
325
+ for attribute in attributes.get("rightsList"):
326
+ # check for schemeUri, rightsIdentifier and 'rightsIdentifierScheme' == SPDX, fallback rightsUri, else nothing
327
+ license_url = attribute.get("rightsUri")
328
+ if (
329
+ attribute.get("schemeUri")
330
+ and attribute.get("rightsIdentifier")
331
+ and attribute.get("rightsIdentifierScheme") == "SPDX"
332
+ ):
333
+ license_url = urljoin(attribute.get("schemeUri"), attribute.get("rightsIdentifier"))
334
+ signposting_links.append(Signpost(rel=LinkRel.license, target=license_url))
335
+
336
+ # type
337
+ schema_org = attributes.get("types", {}).get("schemaOrg")
338
+ if schema_org:
339
+ resource_type_url = "https://schema.org/" + schema_org
340
+ signposting_links.append(Signpost(rel=LinkRel.type, target=resource_type_url))
341
+ signposting_links.append(Signpost(rel=LinkRel.type, target="https://schema.org/AboutPage"))
342
+
343
+ return signposting_links
344
+
345
+
346
+ def record_to_linkset(record_item: RecordItem) -> str:
347
+ """Create a linkset from the record item. Get datacite to build linkset from model exports."""
348
+ record = record_from_result(record_item)
349
+ model = current_runtime.get_model_for_record(record)
350
+ datacite_export = model.get_export_by_mimetype("application/vnd.datacite.datacite+json")
351
+ if not datacite_export:
352
+ return ""
353
+ datacite_dict = datacite_export.serializer.serialize_object(record)
354
+ return create_linkset(datacite_dict, record_item)
355
+
356
+
357
+ def record_to_json_linkset(record_item: RecordItem) -> dict[str, list[dict[str, Any]]]:
358
+ """Create a JSON linkset from the record item. Get datacite to build linkset from model exports."""
359
+ record = record_from_result(record_item)
360
+ model = current_runtime.get_model_for_record(record)
361
+ datacite_export = model.get_export_by_mimetype("application/vnd.datacite.datacite+json")
362
+ if not datacite_export:
363
+ return {}
364
+ datacite_dict = datacite_export.serializer.serialize_object(record)
365
+ return create_linkset_json(datacite_dict, record_item)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oarepo-runtime
3
- Version: 2.0.0.dev38
3
+ Version: 2.0.0.dev39
4
4
  Summary: A set of runtime extensions of Invenio repository
5
5
  Project-URL: Homepage, https://github.com/oarepo/oarepo-runtime
6
6
  License-Expression: MIT
@@ -9,6 +9,7 @@ Requires-Python: <3.14,>=3.13
9
9
  Requires-Dist: langcodes>=3.5.0
10
10
  Requires-Dist: oarepo-invenio-typing-stubs>=0.1.0
11
11
  Requires-Dist: oarepo[rdm,tests]<15,>=14
12
+ Requires-Dist: signposting>=0.9.9
12
13
  Provides-Extra: dev
13
14
  Requires-Dist: pytest>=7.1.2; extra == 'dev'
14
15
  Provides-Extra: oarepo14
@@ -1,7 +1,7 @@
1
- oarepo_runtime/__init__.py,sha256=hHdSN2hRXm0UHFuHSaaokxvEIdJtIoobD_jdybp_vKY,686
2
- oarepo_runtime/api.py,sha256=6AHyFnf0Cg1nfhGaQFMPjynVvrp1UURo8vX5vb5LvC4,14169
1
+ oarepo_runtime/__init__.py,sha256=Hq07N9-oKqUJGYTjwG4TvOXZT2Usdd3GJ-ySd7Y3Wrw,686
2
+ oarepo_runtime/api.py,sha256=hEYmBbqxk0DRfLGikSx4NMMmyDYxUKRAVP1z678iIOw,15000
3
3
  oarepo_runtime/config.py,sha256=RUEPFn_5bKp9Wb0OY-Fb3VK30m35vF5IsLjYaQHhP3g,3838
4
- oarepo_runtime/ext.py,sha256=hA_OmJJHum6W28iPkjxvFrKHzFwi_Ki_0Fy2Mgzl7UQ,8851
4
+ oarepo_runtime/ext.py,sha256=OdhT4gGKK2LpNcJ30cHndCxWZgeHdAk1Ev8bISeX8Qk,9321
5
5
  oarepo_runtime/proxies.py,sha256=x8Y1iTP8QIzSI67s90VR0_5fvXuT1xlJXtAHsaoXFwg,903
6
6
  oarepo_runtime/py.typed,sha256=RznSCjXReEUI9zkmD25E8XniG_MvPpLBF6MyNZA8MmE,42
7
7
  oarepo_runtime/typing.py,sha256=VtINHm4BZ5OJ4KdRAwnFXPZiAEgPRIYTtPC9fIzK1bU,1876
@@ -22,6 +22,7 @@ oarepo_runtime/records/systemfields/relations.py,sha256=EyFTpdglkRCeCtNg3lXTh3H2
22
22
  oarepo_runtime/records/systemfields/selectors.py,sha256=ijVDwAXaXTV5NtcXsrALkhddgCogLNe2eEscFr23qyg,1656
23
23
  oarepo_runtime/resources/__init__.py,sha256=voynQULXoOEviADkbOpekMphZPTAz4IOTg5BF9xPwTM,453
24
24
  oarepo_runtime/resources/config.py,sha256=Lbx1QPWAJ8z1truhYntbnhGGWp2OCcwqKm6BuvPJNT0,1330
25
+ oarepo_runtime/resources/signposting/__init__.py,sha256=USztM57sVm1LWxACxt6o8cy3VZS0Kz-MYrjG8Jm-Ock,14959
25
26
  oarepo_runtime/services/__init__.py,sha256=OGtBgEeaDTyk2RPDNXuKbU9_7egFBZr42SM0gN5FrF4,341
26
27
  oarepo_runtime/services/generators.py,sha256=8Z2QGzob4c2vaaNqhcMZsRybmwtOt30Plgf3EFmcJXw,4622
27
28
  oarepo_runtime/services/results.py,sha256=EwMW1ed7u6uozgOLZpFa07-NKC89hJlHaVSD8-D5ibU,7211
@@ -43,8 +44,8 @@ oarepo_runtime/services/schema/__init__.py,sha256=jgAPI_uKC6Ug4KQWnwQVg3-aNaw-eH
43
44
  oarepo_runtime/services/schema/i18n.py,sha256=9D1zOQaPKAnYzejB0vO-m2BJYnam0N0Lrq4jID7twfE,3174
44
45
  oarepo_runtime/services/schema/i18n_ui.py,sha256=DbusphhGDeaobTt4nuwNgKZ6Houlu4Sv3SuMGkdjRRY,3582
45
46
  oarepo_runtime/services/schema/ui.py,sha256=Y_jBO-fowkpOgceWz8aqJSJAUiAnKLGSIuNpjNLnp8Q,4612
46
- oarepo_runtime-2.0.0.dev38.dist-info/METADATA,sha256=avdajb5WZmRJ1u6EYEABUTGmJiDdGQjPn-lOSDD9aEg,4626
47
- oarepo_runtime-2.0.0.dev38.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
48
- oarepo_runtime-2.0.0.dev38.dist-info/entry_points.txt,sha256=rOfs8R1oXFN_dLH9zAZ6ydkvr83mDajegc6NBIRsCMQ,318
49
- oarepo_runtime-2.0.0.dev38.dist-info/licenses/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
50
- oarepo_runtime-2.0.0.dev38.dist-info/RECORD,,
47
+ oarepo_runtime-2.0.0.dev39.dist-info/METADATA,sha256=yqEwOh9uNtI0K20bSvj-GDXDZJjhZj8comcClUjJaPA,4660
48
+ oarepo_runtime-2.0.0.dev39.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
49
+ oarepo_runtime-2.0.0.dev39.dist-info/entry_points.txt,sha256=rOfs8R1oXFN_dLH9zAZ6ydkvr83mDajegc6NBIRsCMQ,318
50
+ oarepo_runtime-2.0.0.dev39.dist-info/licenses/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
51
+ oarepo_runtime-2.0.0.dev39.dist-info/RECORD,,