oarepo-runtime 1.5.97__py3-none-any.whl → 1.5.100__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.
@@ -4,6 +4,7 @@ from .check import check
4
4
  from .configuration import configuration_command
5
5
  from .fixtures import fixtures
6
6
  from .index import index
7
+ from .permissions import permissions
7
8
  from .validate import validate
8
9
 
9
10
  __all__ = (
@@ -15,4 +16,5 @@ __all__ = (
15
16
  "validate",
16
17
  "fixtures",
17
18
  "configuration_command",
19
+ "permissions",
18
20
  )
@@ -0,0 +1,6 @@
1
+ from .base import permissions
2
+ from .evaluate import evaluate_permissions # noqa
3
+ from .list import list_permissions # noqa
4
+ from .search import search_permissions # noqa
5
+
6
+ __all__ = ["permissions"]
@@ -0,0 +1,25 @@
1
+ from flask import current_app
2
+ from flask_principal import Identity, identity_loaded
3
+ from invenio_access.models import User
4
+
5
+ from ..base import oarepo
6
+
7
+
8
+ @oarepo.group()
9
+ def permissions():
10
+ """Commands for checking and explaining permissions."""
11
+
12
+
13
+ def get_user_and_identity(user_id_or_email):
14
+ try:
15
+ user_id = int(user_id_or_email)
16
+ user = User.query.filter_by(id=user_id).one()
17
+ except ValueError:
18
+ user = User.query.filter_by(email=user_id_or_email).one()
19
+
20
+ identity = Identity(user.id)
21
+ api_app = current_app.wsgi_app.mounts["/api"]
22
+ with api_app.app_context():
23
+ with current_app.test_request_context("/api"):
24
+ identity_loaded.send(api_app, identity=identity)
25
+ return user, identity
@@ -0,0 +1,63 @@
1
+ import json
2
+
3
+ import click
4
+ from invenio_records_permissions.policies.records import RecordPermissionPolicy
5
+ from invenio_records_resources.proxies import current_service_registry
6
+
7
+ from oarepo_runtime.info.permissions.debug import add_debugging
8
+
9
+ from .base import get_user_and_identity, permissions
10
+
11
+
12
+ @permissions.command(name="evaluate")
13
+ @click.argument("user_id_or_email")
14
+ @click.argument("service_name")
15
+ @click.argument("record_id", required=False)
16
+ @click.option("--data", "-d", help="Data to pass to the policy check")
17
+ @click.option("--explain/--no-explain", default=False)
18
+ @click.option("--draft/--published", default=False)
19
+ def evaluate_permissions(
20
+ user_id_or_email: str,
21
+ service_name: str,
22
+ record_id: str | None = None,
23
+ data: str | None = None,
24
+ explain: bool = False,
25
+ draft: bool = False,
26
+ ):
27
+ """Evaluate permissions for a given workflow, community or service."""
28
+ service = current_service_registry.get(service_name)
29
+ user, identity = get_user_and_identity(user_id_or_email)
30
+
31
+ over = {}
32
+ if record_id:
33
+ if draft:
34
+ over["record"] = service.config.draft_cls.pid.resolve(
35
+ record_id, registered_only=False
36
+ )
37
+ else:
38
+ over["record"] = service.config.record_cls.pid.resolve(record_id)
39
+ if data:
40
+ over["data"] = json.loads(data)
41
+
42
+ if explain:
43
+ over["debug_identity"] = identity
44
+ add_debugging()
45
+
46
+ policy_cls = service.config.permission_policy_cls
47
+ click.secho(f"Policy: {policy_cls}")
48
+
49
+ for action_name in dir(policy_cls):
50
+ if not action_name.startswith("can_"):
51
+ continue
52
+
53
+ policy: RecordPermissionPolicy = policy_cls(action_name[4:], **over)
54
+ if explain:
55
+ click.secho()
56
+ click.secho(f"## {action_name}")
57
+ try:
58
+ if policy.allows(identity):
59
+ click.secho(f"{action_name}: True", fg="green")
60
+ else:
61
+ click.secho(f"{action_name}: False", fg="red")
62
+ except Exception as e:
63
+ click.secho(f"{action_name}: {e}", fg="yellow")
@@ -0,0 +1,239 @@
1
+ from typing import get_type_hints
2
+
3
+ import click
4
+
5
+ from oarepo_runtime.info.permissions.debug import add_debugging
6
+
7
+ from .base import permissions
8
+
9
+
10
+ @permissions.command(name="list")
11
+ @click.option("--workflow", "-w", help="Workflow name")
12
+ @click.option("--community", "-c", help="Community name")
13
+ @click.option("--service", "-s", help="Service name")
14
+ def list_permissions(workflow, community, service):
15
+ """List all permissions for a given workflow, community or service."""
16
+ # intentionally import here to enable oarepo-runtime to be used
17
+ # without oarepo-workflows and oarepo-communities
18
+ from invenio_communities.communities.records.api import Community
19
+ from invenio_communities.communities.records.models import CommunityMetadata
20
+ from oarepo_workflows.proxies import current_oarepo_workflows
21
+
22
+ if workflow:
23
+ wf = current_oarepo_workflows.record_workflows[workflow]
24
+ permission_policy = wf.permission_policy_cls
25
+ elif community:
26
+ community_db = CommunityMetadata.query.filter_by(slug=community).one()
27
+ community_obj = Community(community_db.data, model=community_db)
28
+ workflow = community_obj["custom_fields"].get("workflow", "default")
29
+ wf = current_oarepo_workflows.record_workflows[workflow]
30
+ permission_policy = wf.permission_policy_cls
31
+ elif service:
32
+ from invenio_records_resources.proxies import current_service_registry
33
+
34
+ swc = current_service_registry.get(service)
35
+ permission_policy = swc.config.permission_policy_cls
36
+ else:
37
+ raise click.UsageError(
38
+ "You must specify either --workflow, --community or --service."
39
+ )
40
+
41
+ add_debugging()
42
+
43
+ p = permission_policy("read")
44
+
45
+ for action_name in dir(permission_policy):
46
+ if not action_name.startswith("can_"):
47
+ continue
48
+ action_permission_generators = getattr(p, action_name)
49
+ debugs = []
50
+ for x in action_permission_generators:
51
+ debugs.append(x.to_debug_dict())
52
+
53
+ print("")
54
+ print(f"## {action_name}")
55
+ print(get_permission_documentation(permission_policy, action_name))
56
+ for perm in debugs:
57
+ print_permission_markdown(perm)
58
+
59
+
60
+ def is_simple_dict(d):
61
+ return all(isinstance(v, (str, int, float, bool)) for v in d.values())
62
+
63
+
64
+ def format_simple_dict_values(d):
65
+ # returns k=v, k=v, ...
66
+ return ", ".join(f"{k}={v}" for k, v in d.items())
67
+
68
+
69
+ def print_permission_markdown(p, level=0):
70
+ for k, v in p.items():
71
+ prologue = " " * level + f"- {k}"
72
+ if v is None or v == {} or v == []:
73
+ print(prologue)
74
+ continue
75
+ elif isinstance(v, dict):
76
+ if is_simple_dict(v):
77
+ print(f"{prologue}: {format_simple_dict_values(v)}")
78
+ else:
79
+ print(prologue)
80
+ print_permission_markdown(v, level + 1)
81
+ elif isinstance(v, list):
82
+ print(prologue)
83
+ for x in v:
84
+ print_permission_markdown(x, level + 1)
85
+ else:
86
+ print(" " * (level + 1) + f"{v}")
87
+
88
+
89
+ def get_permission_documentation(policy, permission_can_name):
90
+ type_hints = get_type_hints(policy, include_extras=True)
91
+ annotation = type_hints.get(permission_can_name)
92
+ if annotation:
93
+ for md in annotation.__metadata__:
94
+ if isinstance(md, str):
95
+ return md
96
+ ret = default_permissions_documentation.get(permission_can_name, "")
97
+ return "\n".join(x.strip() for x in ret.split("\n"))
98
+
99
+
100
+ default_permissions_documentation = {
101
+ # records
102
+ "can_create": """
103
+ Grants users the ability to create new records in the
104
+ repository, often assigned to roles like submitters, owners, or curators.
105
+ The result of this action is typically a draft record.
106
+ """,
107
+ "can_read": """
108
+ Allows users to view or access records, with access
109
+ possibly dependent on the record's state (e.g., draft or published) and the
110
+ user's role.
111
+ """,
112
+ "can_read_deleted": """
113
+ Permission to view or access records, including soft-deleted ones. This
114
+ permission is used to filter search results. It should include an
115
+ `IfRecordDeleted` permission generator, which grants access to deleted
116
+ records to a subset of users (e.g., owners, curators, etc.). For
117
+ `service.read`, this permission is applied when record is deleted and
118
+ `include_deleted` is passed; otherwise, a `RecordDeletedException` is raised.
119
+ """,
120
+ "can_search": """
121
+ Grants users the ability to search for records within the
122
+ repository, typically available to all users. The search results are
123
+ filtered based on the can_read_deleted permission for published records (/api/datasets)
124
+ and the can_read_draft permission for draft records (/api/user/datasets).
125
+ """,
126
+ "can_update": """
127
+ Grants users the ability to update or modify published records.
128
+ In NRP repositories, this is normally disabled as updates are performed
129
+ on draft records, which are then published.
130
+ """,
131
+ "can_delete": """
132
+ Grants users the ability to delete records, often restricted
133
+ to owners, curators, or specific roles, and dependent on the record's
134
+ state (e.g., draft). For published records, the deletion is performed
135
+ via a specialized request, not directly.
136
+ """,
137
+ "can_new_version": """
138
+ Allows users to create a new version of a record. In NRP,
139
+ this is not used directly but always via a request, which is mostly
140
+ auto-approved.
141
+ """,
142
+ "can_edit": """
143
+ Grants users the ability to modify the metadata of a record. In NRP, this
144
+ is not used directly but always via a request, which is mostly auto-approved.
145
+ """,
146
+ "can_manage": """
147
+ Grants users the ability to manage records. Currently it is used just
148
+ for reindexing records with latest version first (reindex_latest_first)
149
+ service call, not mapped to REST API.
150
+ """,
151
+ "can_manage_record_access": """
152
+ Grants users the ability to manage record access.
153
+ """,
154
+ # draft records
155
+ "can_read_draft": """
156
+ Enables users to view or access draft records, typically equivalent to
157
+ the general 'can_read' permission but specific to drafts.
158
+ """,
159
+ "can_delete_draft": """
160
+ Allows users to delete draft records, typically equivalent to the general
161
+ 'can_delete' permission but specific to drafts.
162
+ """,
163
+ "can_update_draft": """
164
+ Allows users to update draft records. It is typically granted to record
165
+ owner, but can be restricted by the record status (such as records submitted
166
+ to review being locked).
167
+ """,
168
+ "can_publish": """
169
+ Grants users the ability to publish records, making them publicly
170
+ available or finalizing their state. In NRP repositories, this is
171
+ not used directly but always via a request.
172
+ """,
173
+ # files
174
+ "can_read_files": """
175
+ Grants users the ability to view or access file metadata associated with records,
176
+ with access dependent on the record's state and the user's role.
177
+ """,
178
+ "can_create_files": """
179
+ Enables users to create new files within published records in the repository.
180
+ Normally disabled as files are created in draft records and then published.
181
+ """,
182
+ "can_delete_files": """
183
+ Enables users to delete files associated with published records in the repository.
184
+ Normally disabled as files can not be deleted from published records.
185
+ """,
186
+ "can_update_files": """
187
+ Enables users to update or modify files within published records in the repository.
188
+ Normally disabled as files are updated in draft records and then published.
189
+ """,
190
+ "can_commit_files": """
191
+ Allows users to finalize file upload to published records in the repository.
192
+ Normally disabled as files are uploaded to draft records and then published.
193
+ """,
194
+ "can_get_content_files": """
195
+ Allows users to access or retrieve the content of files on records published
196
+ in the repository, with access depending on the record's state and the user's role.
197
+ """,
198
+ "can_manage_files": """
199
+ Grants users the ability to manage files (that is, change if the files are
200
+ required on the record or not).
201
+ """,
202
+ "can_read_deleted_files": """
203
+ Allows users to view or access files associated with deleted records,
204
+ often restricted to specific conditions.
205
+ """,
206
+ "can_set_content_files": """
207
+ Enables users to upload files to published records. Normally disabled as files
208
+ are uploaded to draft records and then published.
209
+ """,
210
+ # draft files
211
+ "can_draft_read_files": """
212
+ Allows users to view or access file metadata associated with draft records,
213
+ typically equivalent to the general 'can_read_files' permission but specific
214
+ to drafts.
215
+ """,
216
+ "can_draft_create_files": """
217
+ Allows users to create files specifically for draft records.
218
+ """,
219
+ "can_draft_get_content_files": """
220
+ Allows users to access or retrieve the content of files on draft records.
221
+ """,
222
+ "can_draft_set_content_files": """
223
+ Allows users to upload files to draft records.
224
+ """,
225
+ "can_draft_commit_files": """
226
+ Allows users to finalize file upload to draft records.
227
+ """,
228
+ "can_draft_update_files": """
229
+ Allows users to update or modify files within draft records.
230
+ """,
231
+ "can_draft_delete_files": """
232
+ Allows users to delete files associated with draft records.
233
+ """,
234
+ # misc
235
+ "can_create_or_update_many": """
236
+ Allows bulk creation or updating of multiple records or files.
237
+ Not used at the moment.
238
+ """,
239
+ }
@@ -0,0 +1,121 @@
1
+ import json
2
+ import sys
3
+
4
+ import click
5
+ import yaml
6
+ from invenio_records_resources.proxies import current_service_registry
7
+
8
+ from oarepo_runtime.info.permissions.debug import add_debugging, merge_communities
9
+
10
+ from .base import get_user_and_identity, permissions
11
+
12
+
13
+ @permissions.command(name="search")
14
+ @click.argument("user_id_or_email")
15
+ @click.argument("service_name")
16
+ @click.option("--explain/--no-explain", default=False)
17
+ @click.option("--user/--published", "user_call", default=False)
18
+ @click.option("--full-query/--query-filters", default=False)
19
+ @click.option("--merge-communities", "do_merge_communities", is_flag=True)
20
+ @click.option("--json/--yaml", "as_json", default=False)
21
+ def search_permissions(
22
+ user_id_or_email,
23
+ service_name,
24
+ explain,
25
+ user_call,
26
+ full_query,
27
+ do_merge_communities,
28
+ as_json,
29
+ ):
30
+ """Get search parameters for a given service."""
31
+ try:
32
+ service = current_service_registry.get(service_name)
33
+ except KeyError:
34
+ raise click.UsageError(
35
+ f"Service {service_name} not found in {current_service_registry._services.keys()}"
36
+ )
37
+ user, identity = get_user_and_identity(user_id_or_email)
38
+
39
+ permission_policy = service.config.permission_policy_cls
40
+
41
+ add_debugging(print_search=explain, print_needs=False, print_excludes=False)
42
+
43
+ if full_query:
44
+ previous_search = service._search
45
+
46
+ class NoExecute:
47
+ def __init__(self, query):
48
+ self.query = query
49
+
50
+ def execute(self):
51
+ return self.query
52
+
53
+ def _patched_search(*args, **kwargs):
54
+ ret = previous_search(*args, **kwargs)
55
+ return NoExecute(ret)
56
+
57
+ def _patched_result_list(self, identity, results, params, **kwargs):
58
+ return results
59
+
60
+ service._search = _patched_search
61
+ service.result_list = _patched_result_list
62
+
63
+ if user_call:
64
+ ret = service.search_drafts(identity)
65
+ else:
66
+ ret = service.search(identity)
67
+ ret = ret.to_dict()
68
+ if do_merge_communities:
69
+ ret = merge_communities(ret)
70
+ ret = {
71
+ "query": ret["query"],
72
+ }
73
+ dump_dict(ret, as_json)
74
+ else:
75
+
76
+ over = {}
77
+ if explain:
78
+ over["debug_identity"] = identity
79
+ print("## Explaining search:")
80
+
81
+ if user_call:
82
+ p = permission_policy("read_draft", identity=identity, **over)
83
+ else:
84
+ p = permission_policy("read_deleted", identity=identity, **over)
85
+ query_filters = p.query_filters
86
+
87
+ print()
88
+ print("## Query filters:")
89
+ for qf in query_filters:
90
+ dict_qf = qf.to_dict()
91
+ if explain:
92
+ dict_qf = merge_communities(dict_qf)
93
+ dump_dict(dict_qf, as_json)
94
+ print(json.dumps(dict_qf, indent=2))
95
+
96
+
97
+ def merge_name(d):
98
+ if isinstance(d, list):
99
+ return [merge_name(x) for x in d]
100
+ if isinstance(d, dict):
101
+ ret = {}
102
+ for k, v in d.items():
103
+ v = merge_name(v)
104
+ if isinstance(v, dict) and "_name" in v:
105
+ _name = v.pop("_name")
106
+ _name = _name.split("@")[0].strip()
107
+ k = f"{k}[{_name}]"
108
+ ret[k] = v
109
+ return ret
110
+ return d
111
+
112
+
113
+ def dump_dict(d, as_json=False):
114
+ if as_json:
115
+ print(json.dumps(d, indent=2))
116
+ else:
117
+ yaml.safe_dump(
118
+ merge_name(json.loads(json.dumps(d))),
119
+ sys.stdout,
120
+ default_flow_style=False,
121
+ )
File without changes
@@ -0,0 +1,191 @@
1
+ """Instrumentors for debugging permissions."""
2
+
3
+ import contextvars
4
+ import inspect
5
+ import json
6
+ import sys
7
+
8
+ from invenio_records_permissions.generators import ConditionalGenerator, Generator
9
+
10
+
11
+ def generator_to_debug_dict(self: Generator):
12
+ ret = {
13
+ "name": self.__class__.__name__,
14
+ }
15
+ ret = {}
16
+ for fld in self.__dict__:
17
+ if fld.startswith("__"):
18
+ continue
19
+ if fld in ("then_", "else_"):
20
+ continue
21
+ value = self.__dict__[fld]
22
+ if not isinstance(value, (str, int, float, bool)):
23
+ value = str(value)
24
+ ret[fld] = value
25
+
26
+ return {self.__class__.__name__: ret}
27
+
28
+
29
+ def conditional_generator_to_debug_dict(self: ConditionalGenerator):
30
+ ret = generator_to_debug_dict(self)
31
+ r = ret[self.__class__.__name__]
32
+ if self.then_:
33
+ r["then"] = [x.to_debug_dict() for x in self.then_]
34
+ if self.else_:
35
+ r["else"] = [x.to_debug_dict() for x in self.else_]
36
+ return ret
37
+
38
+
39
+ def get_all_generators():
40
+ generator_classes = set()
41
+ queue = [Generator]
42
+ while queue:
43
+ gen = queue.pop()
44
+ generator_classes.add(gen)
45
+ for cls in gen.__subclasses__():
46
+ if cls in generator_classes:
47
+ continue
48
+ queue.append(cls)
49
+ return generator_classes
50
+
51
+
52
+ debugging_level = contextvars.ContextVar("debugging_level", default=0)
53
+
54
+
55
+ def debug_needs_output(clz, method_name):
56
+ method = getattr(clz, method_name)
57
+
58
+ def wrapper(self, *args, **kwargs):
59
+ dd = json.dumps(self.to_debug_dict())
60
+ print(f"{' ' * debugging_level.get()}{dd}.{method_name} ->", file=sys.stderr)
61
+ reset = debugging_level.set(debugging_level.get() + 1)
62
+ ret = method(self, *args, **kwargs)
63
+ debugging_level.reset(reset)
64
+ if "debug_identity" in kwargs:
65
+ matched_needs = set(ret) & set(kwargs["debug_identity"].provides)
66
+ print(
67
+ f"{' ' * debugging_level.get()} -> match: {matched_needs}",
68
+ file=sys.stderr,
69
+ )
70
+ else:
71
+ print(f"{' ' * debugging_level.get()} -> {ret}", file=sys.stderr)
72
+ return ret
73
+
74
+ return wrapper
75
+
76
+
77
+ def debug_search_output(clz, method_name, print_search):
78
+ method = getattr(clz, method_name)
79
+
80
+ def wrapper(self, *args, **kwargs):
81
+ dd = json.dumps(self.to_debug_dict())
82
+ if print_search:
83
+ print(f"{' ' * debugging_level.get()}{dd}.{method_name}:", file=sys.stderr)
84
+ reset = debugging_level.set(debugging_level.get() + 2)
85
+ ret = method(self, *args, **kwargs)
86
+ debugging_level.reset(reset)
87
+ if isinstance(ret, list):
88
+ r = merge_communities([x.to_dict() for x in ret])
89
+ elif ret:
90
+ r = merge_communities(ret.to_dict())
91
+ else:
92
+ r = None
93
+ if print_search:
94
+ print(
95
+ f"{' ' * debugging_level.get()}{dd}.{method_name} -> {r}",
96
+ file=sys.stderr,
97
+ )
98
+ return ret
99
+
100
+ wrapper.__module__ = clz.__module__
101
+ wrapper.__qualname__ = f"{clz.__qualname__}.{method_name}"
102
+ wrapper.__name__ = method_name
103
+ return wrapper
104
+
105
+
106
+ def merge_communities(x):
107
+ if isinstance(x, list):
108
+ return [merge_communities(y) for y in x]
109
+ if isinstance(x, dict):
110
+ ret = {k: merge_communities(v) for k, v in x.items()}
111
+ if "parent.communities.default" in ret:
112
+ ret["parent.communities.default"] = "#communities#"
113
+ if "parent.communities.ids" in ret:
114
+ ret["parent.communities.ids"] = "#communities#"
115
+ return ret
116
+ return x
117
+
118
+
119
+ def get_opensearch_caller():
120
+ # Get the current call stack
121
+ stack = inspect.stack()
122
+ # Extract function names from the stack frames
123
+ function_names = []
124
+ state = "skipping_to_opensearch"
125
+ for frame in stack:
126
+ module_name = frame.frame.f_globals["__name__"]
127
+ if state == "skipping_to_opensearch":
128
+ if module_name.startswith("opensearch_dsl.") or module_name.startswith(
129
+ "oarepo_runtime.info"
130
+ ):
131
+ state = "found_opensearch"
132
+ if state == "found_opensearch":
133
+ if not module_name.startswith(
134
+ "opensearch_dsl."
135
+ ) and not module_name.startswith("oarepo_runtime.info"):
136
+ state = "outside_opensearch"
137
+ if state == "outside_opensearch":
138
+ if frame.function == "<lambda>":
139
+ continue
140
+ if "self" in frame.frame.f_locals:
141
+ self_instance = frame.frame.f_locals["self"]
142
+ class_name = self_instance.__class__.__name__
143
+ function_names.append(
144
+ f"{class_name}.{frame.function} @ {frame.filename}:{frame.lineno}"
145
+ )
146
+ else:
147
+ function_names.append(
148
+ f"{frame.function} @ {frame.filename}:{frame.lineno}"
149
+ )
150
+ del frame
151
+
152
+ return function_names
153
+
154
+
155
+ def add_debugging(print_needs=True, print_excludes=True, print_search=True):
156
+ for generator in get_all_generators():
157
+ if issubclass(generator, ConditionalGenerator):
158
+ generator.to_debug_dict = conditional_generator_to_debug_dict
159
+ else:
160
+ generator.to_debug_dict = generator_to_debug_dict
161
+ if print_needs and not hasattr(generator.needs, "__is_debug_instrumented__"):
162
+ generator.needs = debug_needs_output(generator, "needs")
163
+ generator.needs.__is_debug_instrumented__ = True
164
+ if print_excludes and not hasattr(
165
+ generator.excludes, "__is_debug_instrumented__"
166
+ ):
167
+ generator.excludes = debug_needs_output(generator, "excludes")
168
+ generator.excludes.__is_debug_instrumented__ = True
169
+
170
+ if hasattr(generator, "query_filters"):
171
+ if not hasattr(generator.query_filters, "__is_debug_instrumented__"):
172
+ generator.query_filters = debug_search_output(
173
+ generator, "query_filters", print_search
174
+ )
175
+ generator.query_filters.__is_debug_instrumented__ = True
176
+ if hasattr(generator, "query_filter"):
177
+ if not hasattr(generator.query_filter, "__is_debug_instrumented__"):
178
+ generator.query_filter = debug_search_output(
179
+ generator, "query_filter", print_search
180
+ )
181
+ generator.query_filter.__is_debug_instrumented__ = True
182
+ # try to add _name to queries
183
+ from opensearch_dsl.query import Query
184
+
185
+ previous_init = Query.__init__
186
+
187
+ def new_init(self, *args, **kwargs):
188
+ previous_init(self, *args, **kwargs)
189
+ self._params["_name"] = get_opensearch_caller()[0]
190
+
191
+ Query.__init__ = new_init
@@ -80,6 +80,11 @@ class ReadOnlyPermissionPolicy(RecordPermissionPolicy):
80
80
  can_add_community = [SystemProcess()]
81
81
  can_remove_community = [SystemProcess()]
82
82
 
83
+ can_read_deleted = [SystemProcess()]
84
+ can_manage_record_access = [SystemProcess()]
85
+ can_lift_embargo = [SystemProcess()]
86
+
87
+
83
88
 
84
89
  class EveryonePermissionPolicy(RecordPermissionPolicy):
85
90
  """record policy for read only repository"""
@@ -119,12 +124,17 @@ class EveryonePermissionPolicy(RecordPermissionPolicy):
119
124
  can_add_community = [SystemProcess(), AnyUser()]
120
125
  can_remove_community = [SystemProcess(), AnyUser()]
121
126
 
127
+ can_read_deleted = [SystemProcess(), AnyUser()]
128
+ can_manage_record_access = [SystemProcess(), AnyUser()]
129
+ can_lift_embargo = [SystemProcess(), AnyUser()]
130
+
122
131
 
123
132
  class AuthenticatedPermissionPolicy(RecordPermissionPolicy):
124
133
  """record policy for read only repository"""
125
134
 
126
135
  can_search = [SystemProcess(), AuthenticatedUser()]
127
136
  can_read = [SystemProcess(), AnyUser()]
137
+ can_read_deleted = [SystemProcess(), AnyUser()]
128
138
  can_create = [SystemProcess(), AuthenticatedUser()]
129
139
  can_update = [SystemProcess(), AuthenticatedUser()]
130
140
  can_delete = [SystemProcess(), AuthenticatedUser()]
@@ -136,7 +146,6 @@ class AuthenticatedPermissionPolicy(RecordPermissionPolicy):
136
146
  can_commit_files = [SystemProcess(), AuthenticatedUser()]
137
147
  can_read_files = [SystemProcess(), AnyUser()]
138
148
  can_update_files = [SystemProcess(), AuthenticatedUser()]
139
- can_delete_files = [SystemProcess(), AuthenticatedUser()]
140
149
  can_list_files = [SystemProcess(), AuthenticatedUser()]
141
150
  can_manage_files = [SystemProcess(), AuthenticatedUser()]
142
151
 
@@ -157,3 +166,7 @@ class AuthenticatedPermissionPolicy(RecordPermissionPolicy):
157
166
 
158
167
  can_add_community = [SystemProcess(), AuthenticatedUser()]
159
168
  can_remove_community = [SystemProcess(), AuthenticatedUser()]
169
+
170
+ can_delete_files = [SystemProcess(), AuthenticatedUser()]
171
+ can_manage_record_access = [SystemProcess(), AuthenticatedUser()]
172
+ can_lift_embargo = [SystemProcess(), AuthenticatedUser()]
@@ -121,6 +121,9 @@ def prepare_parent_mapping(parent_class, config):
121
121
  if not parent_class:
122
122
  return
123
123
 
124
+ if not config.record_cls.index._name:
125
+ return
126
+
124
127
  script_dir = str(Path(__file__).resolve().parent)
125
128
  path_parts = script_dir.split('/')
126
129
  path_parts = path_parts[:-2]
@@ -1,5 +1,7 @@
1
1
  import copy
2
2
  import logging
3
+ import operator
4
+ from functools import partial, reduce
3
5
  from typing import List
4
6
 
5
7
  from flask import current_app
@@ -8,6 +10,12 @@ from invenio_access.permissions import system_user_id
8
10
  from invenio_app.helpers import obj_or_import_string
9
11
  from invenio_records_resources.services.records.facets import FacetsResponse
10
12
  from invenio_records_resources.services.records.params import FacetsParam
13
+ from invenio_access.permissions import authenticated_user
14
+ from invenio_records_resources.services.records.params.base import ParamInterpreter
15
+ from invenio_search.engine import dsl
16
+ from invenio_rdm_records.records.systemfields.deletion_status import (
17
+ RecordDeletionStatusEnum,
18
+ )
11
19
 
12
20
  log = logging.getLogger(__name__)
13
21
 
@@ -96,7 +104,7 @@ class GroupedFacetsParam(FacetsParam):
96
104
  for f in filters[1:]:
97
105
  _filter &= f
98
106
 
99
- return search.filter(_filter)
107
+ return search.filter(_filter).post_filter(_filter)
100
108
 
101
109
  def apply(self, identity, search, params):
102
110
  """Evaluate the facets on the search."""
@@ -129,3 +137,56 @@ class GroupedFacetsParam(FacetsParam):
129
137
  for group in groups:
130
138
  user_facets.update(self.facet_groups.get(group, {}))
131
139
  return user_facets
140
+
141
+
142
+ class OARepoAllVersionsParam(ParamInterpreter):
143
+ """Evaluates the 'allversions' parameter."""
144
+ def __init__(self, field_names, config):
145
+ """Construct."""
146
+ self.field_names = field_names
147
+ super().__init__(config)
148
+
149
+ @classmethod
150
+ def factory(cls, field_names: list[str]):
151
+ """Create a new filter parameter."""
152
+ return partial(cls, field_names)
153
+
154
+ def apply(self, identity, search, params):
155
+ """Evaluate the allversions parameter on the search."""
156
+ if not params.get("allversions"):
157
+ queries = [dsl.query.Q("term", **{field_name: True}) for field_name in self.field_names]
158
+ query = reduce(operator.or_, queries)
159
+ search = search.filter(query)
160
+ return search
161
+
162
+ class OARepoPublishedRecordsParam(ParamInterpreter):
163
+ """Evaluates the include_deleted parameter."""
164
+
165
+ def apply(self, identity, search, params):
166
+ """Evaluate the include_deleted parameter on the search."""
167
+
168
+ value = params.pop("include_deleted", None)
169
+ # Filter prevents from displaying deleted records on mainsite search
170
+ # deleted records should appear only in admins panel
171
+ if value is None:
172
+ query = dsl.query.Q(
173
+ "bool",
174
+ should=[
175
+ dsl.query.Q(
176
+ "bool",
177
+ must=[
178
+ dsl.query.Q(
179
+ "term",
180
+ deletion_status=RecordDeletionStatusEnum.PUBLISHED.value,
181
+ )
182
+ ],
183
+ ),
184
+ # Drafts does not have deletion_status so this clause is needed to
185
+ # prevent the above clause from filtering out the drafts
186
+ dsl.query.Q(
187
+ "bool", must_not=[dsl.query.Q("exists", field="deletion_status")]
188
+ ),
189
+ ],
190
+ )
191
+ search = search.filter(query)
192
+ return search
@@ -11,6 +11,7 @@ from invenio_rdm_records.records.systemfields.access.field.record import (
11
11
  from invenio_rdm_records.resources.serializers.ui.fields import (
12
12
  UIObjectAccessStatus as InvenioUIObjectAccessStatus,
13
13
  )
14
+ from invenio_rdm_records.services.schemas.versions import VersionsSchema
14
15
  from marshmallow_utils.fields import (
15
16
  BabelGettextDictField,
16
17
  FormatDate,
@@ -181,6 +182,7 @@ class AccessStatusField(ma.fields.Field):
181
182
  class InvenioRDMUISchema(InvenioUISchema, RDMBaseRecordSchema):
182
183
  is_draft = ma.fields.Boolean(dump_only=True)
183
184
  access_status = AccessStatusField(attribute="access", dump_only=True)
185
+ versions = ma.fields.Nested(VersionsSchema, dump_only=True)
184
186
 
185
187
  def hide_tombstone(self, data):
186
188
  """Hide tombstone info if the record isn't deleted and metadata if it is."""
@@ -10,6 +10,7 @@ from invenio_records_resources.proxies import current_service_registry
10
10
  from invenio_records_resources.services.records import (
11
11
  SearchOptions as InvenioSearchOptions,
12
12
  )
13
+ from invenio_drafts_resources.services.records.config import SearchDraftsOptions as InvenioSearchDraftsOptions
13
14
  from invenio_records_resources.services.records.params import (
14
15
  FacetsParam,
15
16
  PaginationParam,
@@ -18,14 +19,16 @@ from invenio_records_resources.services.records.params import (
18
19
  )
19
20
  from invenio_records_resources.services.records.queryparser import SuggestQueryParser
20
21
  from invenio_search.engine import dsl
21
-
22
+ from invenio_drafts_resources.services.records.search_params import AllVersionsParam
22
23
  # TODO: integrate this to invenio_records_resources.services.records and remove SearchOptions class
23
24
  from oarepo_runtime.i18n import lazy_gettext as _
24
25
  from oarepo_runtime.records.systemfields.icu import ICUSuggestField
25
26
  from oarepo_runtime.utils.functools import class_property
26
27
 
27
- from .facets.params import GroupedFacetsParam
28
-
28
+ from .facets.params import GroupedFacetsParam, OARepoAllVersionsParam, OARepoPublishedRecordsParam
29
+ from invenio_drafts_resources.services.records.search_params import AllVersionsParam
30
+ from invenio_rdm_records.services.search_params import PublishedRecordsParam
31
+ from functools import partial
29
32
  try:
30
33
  from invenio_i18n import get_locale
31
34
  except ImportError:
@@ -57,11 +60,19 @@ class SearchOptionsMixin:
57
60
  @class_property
58
61
  def params_interpreters_cls(cls):
59
62
  """Replaces FacetsParam with GroupedFacetsParam."""
63
+ params_replace_map = {FacetsParam: GroupedFacetsParam, AllVersionsParam:
64
+ OARepoAllVersionsParam.factory(["versions.is_latest", "versions.is_latest_draft"]),
65
+ PublishedRecordsParam: OARepoPublishedRecordsParam}
66
+
60
67
  param_interpreters = [*super(SearchOptionsMixin, cls).params_interpreters_cls]
61
68
  # replace FacetsParam with GroupedFacetsParam
62
69
  for idx, interpreter in enumerate(param_interpreters):
63
- if interpreter == FacetsParam:
64
- param_interpreters[idx] = GroupedFacetsParam
70
+ if interpreter in params_replace_map:
71
+ param_interpreters[idx] = params_replace_map[interpreter]
72
+ elif isinstance(interpreter, partial):
73
+ fn = interpreter.func
74
+ if fn in params_replace_map:
75
+ param_interpreters[idx] = params_replace_map[fn]
65
76
  return param_interpreters
66
77
 
67
78
  sort_options = {
@@ -113,6 +124,7 @@ class SearchOptionsDraftMixin(SearchOptionsMixin):
113
124
  }
114
125
 
115
126
 
127
+
116
128
  class SearchOptions(SearchOptionsMixin, InvenioSearchOptions):
117
129
  # TODO: should be changed
118
130
  params_interpreters_cls = [
@@ -122,6 +134,16 @@ class SearchOptions(SearchOptionsMixin, InvenioSearchOptions):
122
134
  GroupedFacetsParam,
123
135
  ]
124
136
 
137
+ class SearchDraftsOptions(SearchOptionsMixin, InvenioSearchDraftsOptions):
138
+ # TODO: should be changed
139
+ params_interpreters_cls = [
140
+ QueryStrParam,
141
+ PaginationParam,
142
+ SortParam,
143
+ GroupedFacetsParam,
144
+ AllVersionsParam.factory("versions.is_latest_draft")
145
+ ]
146
+
125
147
 
126
148
  class RDMSearchOptions(SearchOptionsMixin, BaseRDMSearchOptions):
127
149
  pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: oarepo-runtime
3
- Version: 1.5.97
3
+ Version: 1.5.100
4
4
  Summary: A set of runtime extensions of Invenio repository
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
@@ -5,7 +5,7 @@ oarepo_runtime/profile.py,sha256=QzrQoZncjoN74ZZnpkEKakNk08KCzBU7m6y42RN8AMY,163
5
5
  oarepo_runtime/proxies.py,sha256=NN_WNj1xuKc-OveoZmzvTFlUonNjSmLIGsv_JUcHGls,285
6
6
  oarepo_runtime/tasks.py,sha256=AciXN1fUq6tzfzNSICh1S6XoHGWiuH76bUqXyzTsOPU,140
7
7
  oarepo_runtime/uow.py,sha256=iyF3R2oCPSVUu38GXoxZallgRD-619q1fWTb8sSaeyQ,4412
8
- oarepo_runtime/cli/__init__.py,sha256=daAODgUAB9Nb0O0Kg8vSgJIPKJm92EHMTRuoKwxBCug,373
8
+ oarepo_runtime/cli/__init__.py,sha256=e175jnb1xQlPgMhhgihn3y-lkmlqcMFCnJvQNMZ_fgs,429
9
9
  oarepo_runtime/cli/assets.py,sha256=ZG80xIOKch7rsU_7QlvZxBQAXNQ9ow5xLsUREsl5MEE,4076
10
10
  oarepo_runtime/cli/base.py,sha256=94RBTa8TOSPxEyEUmYLGXaWen-XktP2-MIbTtZSlCZo,544
11
11
  oarepo_runtime/cli/cf.py,sha256=W0JEJK2JqKubQw8qtZJxohmADDRUBode4JZAqYLDGvc,339
@@ -14,6 +14,11 @@ oarepo_runtime/cli/configuration.py,sha256=_iMmESs2dd1Oif95gxgpnkSxc13ymwr82_sTJ
14
14
  oarepo_runtime/cli/fixtures.py,sha256=l6zHpz1adjotrbFy_wcN2TOL8x20i-1jbQmaoEEo-UU,5419
15
15
  oarepo_runtime/cli/index.py,sha256=KH5PArp0fCNbgJI1zSz0pb69U9eyCdnJuy0aMIgf2tg,8685
16
16
  oarepo_runtime/cli/validate.py,sha256=HpSvHQCGHlrdgdpKix9cIlzlBoJEiT1vACZdMnOUGEY,2827
17
+ oarepo_runtime/cli/permissions/__init__.py,sha256=2qufYdUoCb9kG7Zy0gKNW5lpRyqbVQSNsf7shLwrThM,198
18
+ oarepo_runtime/cli/permissions/base.py,sha256=USzA5LNFPpR7tvM_uan70GI7oO6FrGlHzODU_Z4Tl6c,744
19
+ oarepo_runtime/cli/permissions/evaluate.py,sha256=gxkHySd1vM7lSjR-xXzcACkCKDYurl5c567Kq5FVzpM,2086
20
+ oarepo_runtime/cli/permissions/list.py,sha256=TYntLPqHGCcs73FzHX-nEnh-2TtUoNwRN8aJ5358xE4,9853
21
+ oarepo_runtime/cli/permissions/search.py,sha256=2e1NHxNk72T93U3eB6BDoSr-qL_O6aySujw0YB4OQmE,3528
17
22
  oarepo_runtime/datastreams/__init__.py,sha256=_i52Ek9J8DMARST0ejZAZPzUKm55xrrlKlCSO7dl6y4,1008
18
23
  oarepo_runtime/datastreams/asynchronous.py,sha256=JwT-Hx6P7KwV0vSJlxX6kLSIX5vtsekVsA2p_hZpJ_U,7402
19
24
  oarepo_runtime/datastreams/catalogue.py,sha256=D6leq-FPT3RP3SniEAXPm66v3q8ZdQnaUYJ5XM0dIFY,5021
@@ -44,6 +49,8 @@ oarepo_runtime/i18n/__init__.py,sha256=h0knW_HwiyIt5TBHfdGqN7_BBYfpz1Fw6zhVy0C28
44
49
  oarepo_runtime/info/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
50
  oarepo_runtime/info/check.py,sha256=6O5Wjsdorx4eqiBiPU3z33XhCiwPTO_FGkzMDK7UH6I,3049
46
51
  oarepo_runtime/info/views.py,sha256=5ygLgiZilk33OhrnB0h83-MG6K8nsmJFOiJaTBQc43c,21070
52
+ oarepo_runtime/info/permissions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
+ oarepo_runtime/info/permissions/debug.py,sha256=2gak5W64g_AAZoX8cCII2di4G_jBo1ZvnU5ceP_u6z4,6722
47
54
  oarepo_runtime/records/__init__.py,sha256=JUf9_o09_6q4vuG43JzhSeTu7c-m_CVDSmgTQ7epYEo,1776
48
55
  oarepo_runtime/records/dumpers/__init__.py,sha256=OmzNhLdMNKibmCksnj9eTX9xPBG30dziiK3j3bAAp3k,233
49
56
  oarepo_runtime/records/dumpers/edtf_interval.py,sha256=YCShZAoqBQYaxVilEVotS-jXZsxxoXO67yu2urhkaMA,1198
@@ -76,13 +83,13 @@ oarepo_runtime/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
76
83
  oarepo_runtime/services/components.py,sha256=QKrdFmroE1hSrRjRkqinynCTb0KSqBhytTb9FoY0OEE,11023
77
84
  oarepo_runtime/services/generators.py,sha256=j87HitHA_w2awsz0C5IAAJ0qjg9JMtvdO3dvh6FQyfg,250
78
85
  oarepo_runtime/services/results.py,sha256=Ap2mUJHl3V4BSduTrBWPuco0inQVq0QsuCbVhez48uY,5705
79
- oarepo_runtime/services/search.py,sha256=mUycojDo26p54ZZPqXcXmdhUrzAP_eKCWls2XYJSM6c,8186
86
+ oarepo_runtime/services/search.py,sha256=t0WEe2VrbCzZ06-Jgz7C9-pc9y27BqAgTEXldEHskfk,9409
80
87
  oarepo_runtime/services/config/__init__.py,sha256=559w4vphVAipa420OwTsxGUP-b7idoqSIX13FSJyVz0,783
81
88
  oarepo_runtime/services/config/link_conditions.py,sha256=evPRd5XU76Ok4J-08bBfplbHZ019rl74FpJmO8iM5yg,3298
82
- oarepo_runtime/services/config/permissions_presets.py,sha256=YF99Vjh1fGqSEXNw7qIfcUQFvzpE54_BafqkO6wlYHk,6668
89
+ oarepo_runtime/services/config/permissions_presets.py,sha256=jV3Ft7xFdBaeShoa3Q3Q-HHWP-wOX4XYDTRzWwUVL7g,7151
83
90
  oarepo_runtime/services/config/service.py,sha256=s-dVbGkLICpsce6jgu7b5kzYFz9opWjSQFDBgbIhKio,4002
84
91
  oarepo_runtime/services/custom_fields/__init__.py,sha256=_gqMcA_I3rdEZcBtCuDjO4wdVCqFML5NzaccuPx5a3o,2565
85
- oarepo_runtime/services/custom_fields/mappings.py,sha256=Y1d0s9lG_VThkzSNlxAS94_9eOFZe9N2C1hpYDZvnvI,7457
92
+ oarepo_runtime/services/custom_fields/mappings.py,sha256=3CIvjWzVRO3fZ2r-3taK1rwuLH9wj5Lguo-GvsbLBf4,7515
86
93
  oarepo_runtime/services/entity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
94
  oarepo_runtime/services/entity/config.py,sha256=1jfdPrxSbMuKj7eOUNKRWTCPbBPyRV6MrWE4Vgf9rX0,399
88
95
  oarepo_runtime/services/entity/schema.py,sha256=8TBpUFRITaBO7qCMz36cly1Hj4I1nLa9PeSAfWSa2YM,157
@@ -97,7 +104,7 @@ oarepo_runtime/services/facets/enum.py,sha256=3LrShQIt9Vt5mkqUkc6FNxXCW5JEFdPwtG
97
104
  oarepo_runtime/services/facets/facet_groups_names.py,sha256=RR8eeUmD8d9t966JqfhslZnILn_xDSfYlL0hjyJT92Y,468
98
105
  oarepo_runtime/services/facets/max_facet.py,sha256=TZ4KMKKVJHzyU1KgNne4V7IMQPu1ALRpkz61Y0labrc,407
99
106
  oarepo_runtime/services/facets/nested_facet.py,sha256=y0xgjx37HsSj2xW7URxNemYTksD8hpPs7kOEfIBw22k,971
100
- oarepo_runtime/services/facets/params.py,sha256=IiRKbR0jIm67hucj3fINosUAysZQSSKqhusEXXub-hA,4194
107
+ oarepo_runtime/services/facets/params.py,sha256=270La-A9582aDF3D4_bMSLWv4VsPeMiqbb9jA301Q5w,6585
101
108
  oarepo_runtime/services/facets/year_histogram.py,sha256=kdfwx1lgw4UmfjdaqqeElJCB8rAduMH2hy42aZjY37w,6257
102
109
  oarepo_runtime/services/files/__init__.py,sha256=K8MStrEQf_BUhvzhwPTF93Hkhwrd1dtv35LDo7iZeTM,268
103
110
  oarepo_runtime/services/files/components.py,sha256=x6Wd-vvkqTqB1phj2a6h42DNQksN8PuR2XKaOGoNHfw,2400
@@ -120,7 +127,7 @@ oarepo_runtime/services/schema/marshmallow_to_json_schema.py,sha256=VYLnVWHOoaxW
120
127
  oarepo_runtime/services/schema/oneofschema.py,sha256=GnWH4Or_G5M0NgSmCoqMI6PBrJg5AC9RHrcB5QDKRq0,6661
121
128
  oarepo_runtime/services/schema/polymorphic.py,sha256=bAbUoTIeDBiJPYPhpLEKKZekEdkHlpqkmNxk1hN3PDw,564
122
129
  oarepo_runtime/services/schema/rdm.py,sha256=4gi44LIMWS9kWcZfEoHaJSq585qfZfMuUYM5IvL1nQo,531
123
- oarepo_runtime/services/schema/ui.py,sha256=qS9ZnKlo1xib8VJe3uFEff8mCQF5qtjoyOqfa0xOT70,6055
130
+ oarepo_runtime/services/schema/ui.py,sha256=hHbj1S-DW1WqgYX31f6UjarY4wrE-qFLpH3oUhvGLyE,6192
124
131
  oarepo_runtime/services/schema/validation.py,sha256=VFOKSxQLHwFb7bW8BJAFXWe_iTAZFOfqOnb2Ko_Yxxc,2085
125
132
  oarepo_runtime/translations/default_translations.py,sha256=060GBlA1ghWxfeumo6NqxCCZDb-6OezOuF6pr-_GEOQ,104
126
133
  oarepo_runtime/translations/messages.pot,sha256=wr1vDKjsngqS9e5zQmSAsF3qUAtpQ41SUXwzEKRCnWw,2406
@@ -135,9 +142,9 @@ tests/marshmallow_to_json/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
135
142
  tests/marshmallow_to_json/test_datacite_ui_schema.py,sha256=82iLj8nW45lZOUewpWbLX3mpSkpa9lxo-vK-Qtv_1bU,48552
136
143
  tests/marshmallow_to_json/test_simple_schema.py,sha256=izZN9p0v6kovtSZ6AdxBYmK_c6ZOti2_z_wPT_zXIr0,1500
137
144
  tests/pkg_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
138
- oarepo_runtime-1.5.97.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
139
- oarepo_runtime-1.5.97.dist-info/METADATA,sha256=Od7dJAVmd0ukZLCwWRo486I-ymuFjovPPO57ZeoFsrM,4720
140
- oarepo_runtime-1.5.97.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
141
- oarepo_runtime-1.5.97.dist-info/entry_points.txt,sha256=k7O5LZUOGsVeSpB7ulU0txBUNp1CVQG7Q7TJIVTPbzU,491
142
- oarepo_runtime-1.5.97.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
143
- oarepo_runtime-1.5.97.dist-info/RECORD,,
145
+ oarepo_runtime-1.5.100.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
146
+ oarepo_runtime-1.5.100.dist-info/METADATA,sha256=Pv0Sq-lTvfpaoq3baSsGxFfNWXpeMwEQQexb16JrSeQ,4721
147
+ oarepo_runtime-1.5.100.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
148
+ oarepo_runtime-1.5.100.dist-info/entry_points.txt,sha256=k7O5LZUOGsVeSpB7ulU0txBUNp1CVQG7Q7TJIVTPbzU,491
149
+ oarepo_runtime-1.5.100.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
150
+ oarepo_runtime-1.5.100.dist-info/RECORD,,