oarepo-runtime 1.4.12__py3-none-any.whl → 1.4.13__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,45 @@
1
+ from typing import List
2
+
1
3
  from flask import current_app
2
4
  from invenio_records.systemfields import DictField, SystemField
5
+ from invenio_records_resources.services.custom_fields import BaseCF
6
+
7
+ from oarepo_runtime.records import MappingSystemFieldMixin
3
8
 
4
9
 
5
- class CustomFieldsMixin:
10
+ class CustomFieldsMixin(MappingSystemFieldMixin):
6
11
  def __init__(self, config_key, *args, **kwargs) -> None:
7
12
  super().__init__(*args, **kwargs)
8
13
  self.config_key = config_key
9
14
 
15
+ @property
16
+ def mapping(self):
17
+ custom_fields: List[BaseCF] = current_app.config[self.config_key]
18
+ return {cf.name: cf.mapping for cf in custom_fields}
19
+
20
+ @property
21
+ def mapping_settings(self):
22
+ return {}
23
+
24
+ def search_dump(self, data):
25
+ custom_fields = current_app.config.get(self.config_key, {})
26
+
27
+ for cf in custom_fields:
28
+ cf.dump(data, cf_key=self.key)
29
+ return data
30
+
31
+ def search_load(self, data):
32
+ custom_fields = current_app.config.get(self.config_key, {})
33
+
34
+ for cf in custom_fields:
35
+ cf.load(data, cf_key=self.key)
36
+ return data
37
+
10
38
 
11
39
  class CustomFields(CustomFieldsMixin, DictField):
12
- pass
40
+ @property
41
+ def mapping(self):
42
+ return {self.key: {"type": "object", "properties": super().mapping}}
13
43
 
14
44
 
15
45
  class InlinedCustomFields(CustomFieldsMixin, SystemField):
oarepo_runtime/cf/cli.py CHANGED
@@ -12,15 +12,6 @@ def cf():
12
12
 
13
13
 
14
14
  @cf.command(name="init", help="Prepare custom fields in indices")
15
- @click.option(
16
- "-f",
17
- "--field-name",
18
- "field_names",
19
- type=str,
20
- required=False,
21
- multiple=True,
22
- help="A custom field name to create. If not provided, all custom fields will be created.",
23
- )
24
15
  @with_appcontext
25
- def init(field_names):
26
- prepare_cf_indices(field_names)
16
+ def init():
17
+ prepare_cf_indices()
@@ -2,23 +2,18 @@ import inspect
2
2
  from typing import Iterable, List
3
3
 
4
4
  import click
5
- from flask import current_app
5
+ import deepmerge
6
6
  from invenio_records_resources.proxies import current_service_registry
7
- from invenio_records_resources.services.custom_fields import BaseCF
8
7
  from invenio_records_resources.services.custom_fields.mappings import (
9
8
  Mapping as InvenioMapping,
10
9
  )
11
- from invenio_records_resources.services.custom_fields.validate import (
12
- validate_custom_fields,
13
- )
14
10
  from invenio_records_resources.services.records.config import RecordServiceConfig
15
11
  from invenio_records_resources.services.records.service import RecordService
16
12
  from invenio_search import current_search_client
17
13
  from invenio_search.engine import dsl, search
18
14
  from invenio_search.utils import build_alias_name
19
15
 
20
- from oarepo_runtime.cf import CustomFieldsMixin
21
- import deepmerge
16
+ from oarepo_runtime.records import MappingSystemFieldMixin
22
17
 
23
18
 
24
19
  class Mapping(InvenioMapping):
@@ -69,34 +64,22 @@ class Mapping(InvenioMapping):
69
64
 
70
65
  # pieces taken from https://github.com/inveniosoftware/invenio-rdm-records/blob/master/invenio_rdm_records/cli.py
71
66
  # as cf initialization is not supported directly in plain invenio
72
- def prepare_cf_indices(field_names: List[str] = None):
67
+ def prepare_cf_indices():
73
68
  service: RecordService
74
69
  for service in current_service_registry._services.values():
75
70
  config: RecordServiceConfig = service.config
76
- prepare_cf_index(config, field_names)
71
+ prepare_cf_index(config)
77
72
 
78
73
 
79
- def prepare_cf_index(config: RecordServiceConfig, field_names: List[str] = None):
74
+ def prepare_cf_index(config: RecordServiceConfig):
80
75
  record_class = getattr(config, "record_cls", None)
81
76
  if not record_class:
82
77
  return
83
78
 
84
- # try to get custom fields from the record class
85
- # validate them
86
- for field_name, available_fields in get_custom_fields(record_class):
87
- validate_custom_fields(
88
- given_fields=field_names, available_fields=available_fields, namespaces=[]
89
- )
90
-
79
+ for fld in get_mapping_fields(record_class):
91
80
  # get mapping
92
- properties = Mapping.properties_for_fields(
93
- field_names, available_fields, field_name=field_name
94
- )
95
- settings = Mapping.settings_for_fields(
96
- field_names, available_fields, field_name=field_name
97
- )
98
- if not properties:
99
- continue
81
+ mapping = fld.mapping
82
+ settings = fld.mapping_settings
100
83
 
101
84
  # upload mapping
102
85
  try:
@@ -106,12 +89,7 @@ def prepare_cf_index(config: RecordServiceConfig, field_names: List[str] = None)
106
89
  ),
107
90
  using=current_search_client,
108
91
  )
109
- if settings:
110
- record_index.close()
111
- record_index.put_settings(body=settings)
112
- record_index.open()
113
- if properties:
114
- record_index.put_mapping(body={"properties": properties})
92
+ update_index(record_index, settings, mapping)
115
93
 
116
94
  if hasattr(config, "draft_cls"):
117
95
  draft_index = dsl.Index(
@@ -120,15 +98,24 @@ def prepare_cf_index(config: RecordServiceConfig, field_names: List[str] = None)
120
98
  ),
121
99
  using=current_search_client,
122
100
  )
123
- draft_index.put_mapping(body={"properties": properties})
101
+ update_index(draft_index, settings, mapping)
124
102
 
125
103
  except search.RequestError as e:
126
104
  click.secho("An error occurred while creating custom fields.", fg="red")
127
105
  click.secho(e.info["error"]["reason"], fg="red")
128
106
 
129
107
 
130
- def get_custom_fields(record_class) -> Iterable[List[BaseCF]]:
108
+ def update_index(record_index, settings, mapping):
109
+ if settings:
110
+ record_index.close()
111
+ record_index.put_settings(body=settings)
112
+ record_index.open()
113
+ if mapping:
114
+ record_index.put_mapping(body={"properties": mapping})
115
+
116
+
117
+ def get_mapping_fields(record_class) -> Iterable[MappingSystemFieldMixin]:
131
118
  for cfg_name, cfg_value in inspect.getmembers(
132
- record_class, lambda x: isinstance(x, CustomFieldsMixin)
119
+ record_class, lambda x: isinstance(x, MappingSystemFieldMixin)
133
120
  ):
134
- yield cfg_value._key, current_app.config[cfg_value.config_key]
121
+ yield cfg_value
@@ -1,4 +1,3 @@
1
- import itertools
2
1
  import sys
3
2
 
4
3
  import click
@@ -0,0 +1,29 @@
1
+ import inspect
2
+
3
+ from invenio_records.dumpers import SearchDumperExt
4
+
5
+
6
+ class MappingSystemFieldMixin:
7
+ @property
8
+ def mapping(self):
9
+ return {}
10
+
11
+ @property
12
+ def mapping_settings(self):
13
+ return {}
14
+
15
+
16
+ class SystemFieldDumperExt(SearchDumperExt):
17
+ def dump(self, record, data):
18
+ """Dump custom fields."""
19
+ for cf in inspect.getmembers(
20
+ record, lambda x: isinstance(x, MappingSystemFieldMixin)
21
+ ):
22
+ cf[1].search_dump(data)
23
+
24
+ def load(self, data, record_cls):
25
+ """Load custom fields."""
26
+ for cf in inspect.getmembers(
27
+ record_cls, lambda x: isinstance(x, MappingSystemFieldMixin)
28
+ ):
29
+ cf[1].search_load(data)
@@ -0,0 +1,132 @@
1
+ from functools import cached_property
2
+ from typing import Dict
3
+
4
+ from flask import current_app
5
+ from invenio_records.systemfields import SystemField
6
+
7
+ from oarepo_runtime.records import MappingSystemFieldMixin
8
+ from oarepo_runtime.relations.lookup import lookup_key
9
+
10
+
11
+ class ICUField(SystemField):
12
+ """
13
+ A system field that acts as an opensearch "proxy" to another field.
14
+ It creates a top-level mapping field with the same name and copies
15
+ content of {another field}.language into {mapping field}.language.
16
+
17
+ The language accessor can be modified by overriding get_values method.
18
+ """
19
+
20
+ def __init__(self, *, source_field, key=None):
21
+ super().__init__(key)
22
+ self.source_field = source_field
23
+
24
+ @cached_property
25
+ def languages(self) -> Dict[str, Dict]:
26
+ icu_languages = current_app.config.get("OAREPO_ICU_LANGUAGES", {})
27
+ if icu_languages:
28
+ return icu_languages
29
+
30
+ primary_language = current_app.config.get("BABEL_DEFAULT_LOCALE", "en")
31
+ # list of tuples [lang, title], just take lang
32
+ babel_languages = [x[0] for x in current_app.config.get("I18N_LANGUAGES", [])]
33
+
34
+ return {primary_language: {}, **{k: {} for k in babel_languages}}
35
+
36
+ def get_values(self, data, language):
37
+ ret = []
38
+ for l in lookup_key(data, f"{self.source_field}"):
39
+ if isinstance(l.value, str):
40
+ ret.append(l.value)
41
+ elif isinstance(l.value, dict):
42
+ val = l.value.get(language)
43
+ if val:
44
+ ret.append(val)
45
+ return ret
46
+
47
+ def search_dump(self, data):
48
+ ret = {}
49
+ for lang in self.languages:
50
+ ret[lang] = self.get_values(data, lang)
51
+ data[self.attr_name] = ret
52
+
53
+ def search_load(self, data):
54
+ data.pop(self.attr_name, None)
55
+
56
+ def __get__(self, instance, owner):
57
+ return self
58
+
59
+
60
+ class ICUSortField(MappingSystemFieldMixin, ICUField):
61
+ """
62
+ A field that adds icu sorting field
63
+ """
64
+
65
+ def __init__(self, *, source_field, key=None):
66
+ super().__init__(source_field=source_field, key=key)
67
+
68
+ @property
69
+ def mapping(self):
70
+ return {
71
+ self.attr_name: {
72
+ "type": "object",
73
+ "properties": {
74
+ lang: {
75
+ "type": "icu_collation_keyword",
76
+ "index": False,
77
+ "language": lang,
78
+ **setting.get("collation", {}),
79
+ }
80
+ for lang, setting in self.languages.items()
81
+ },
82
+ },
83
+ }
84
+
85
+
86
+ class ICUSuggestField(MappingSystemFieldMixin, ICUField):
87
+ """
88
+ A field that adds icu-aware suggestion field
89
+ """
90
+
91
+ def __init__(self, source_field, key=None):
92
+ super().__init__(source_field=source_field, key=key)
93
+
94
+ @property
95
+ def mapping(self):
96
+ return {
97
+ self.attr_name: {
98
+ "type": "object",
99
+ "properties": {
100
+ lang: setting.get(
101
+ "suggest",
102
+ {
103
+ "type": "text",
104
+ "fields": {
105
+ "original": {
106
+ "type": "search_as_you_type",
107
+ },
108
+ "no_accent": {
109
+ "type": "search_as_you_type",
110
+ "analyzer": "accent_removal_analyzer",
111
+ },
112
+ },
113
+ },
114
+ )
115
+ for lang, setting in self.languages.items()
116
+ },
117
+ },
118
+ }
119
+
120
+ @property
121
+ def mapping_settings(self):
122
+ return {
123
+ "analysis": {
124
+ "analyzer": {
125
+ "accent_removal_analyzer": {
126
+ "type": "custom",
127
+ "tokenizer": "standard",
128
+ "filter": ["lowercase", "asciifolding"],
129
+ }
130
+ }
131
+ }
132
+ }
@@ -12,5 +12,7 @@ class MultipleInvalidRelationErrors(Exception):
12
12
  """
13
13
 
14
14
  def __init__(self, errors):
15
- super().__init__("; ".join([f"{e[0]}: {type(e[1]).__name__}({e[1]})" for e in errors]))
15
+ super().__init__(
16
+ "; ".join([f"{e[0]}: {type(e[1]).__name__}({e[1]})" for e in errors])
17
+ )
16
18
  self.errors = errors
@@ -0,0 +1,160 @@
1
+ import dataclasses
2
+ import inspect
3
+ from typing import List
4
+
5
+ # TODO: integrate this to invenio_records_resources.services.records and remove SearchOptions class
6
+ from flask_babelex import lazy_gettext as _
7
+ from invenio_records_resources.proxies import current_service_registry
8
+ from invenio_records_resources.services.records import (
9
+ SearchOptions as InvenioSearchOptions,
10
+ )
11
+ from invenio_records_resources.services.records.queryparser import SuggestQueryParser
12
+
13
+ from oarepo_runtime.records.icu import ICUSuggestField
14
+
15
+ try:
16
+ from invenio_i18n import get_locale
17
+ except ImportError:
18
+ from invenio_i18n.babel import get_locale
19
+
20
+
21
+ class SearchOptions(InvenioSearchOptions):
22
+ sort_options = {
23
+ "title": dict(
24
+ title=_("By Title"),
25
+ fields=["metadata.title"], # ES defaults to desc on `_score` field
26
+ ),
27
+ "bestmatch": dict(
28
+ title=_("Best match"),
29
+ fields=["_score"], # ES defaults to desc on `_score` field
30
+ ),
31
+ "newest": dict(
32
+ title=_("Newest"),
33
+ fields=["-created"],
34
+ ),
35
+ "oldest": dict(
36
+ title=_("Oldest"),
37
+ fields=["created"],
38
+ ),
39
+ }
40
+
41
+
42
+ @dataclasses.dataclass
43
+ class SuggestField:
44
+ field: str
45
+ boost: int
46
+ use_ngrams: bool = True
47
+ boost_exact: float = 5
48
+ boost_2gram: float = 1
49
+ boost_3gram: float = 1
50
+ boost_prefix: float = 1
51
+
52
+
53
+ class ICUSuggestParser:
54
+ def __init__(
55
+ self,
56
+ record_cls_or_service_name,
57
+ extra_fields: List[SuggestField] = None,
58
+ default_fields: List[SuggestField] = None,
59
+ ):
60
+ self.record_cls_or_service_name = record_cls_or_service_name
61
+ self.extra_fields = extra_fields or []
62
+ self.default_fields = default_fields or []
63
+
64
+ @property
65
+ def record_cls(self):
66
+ if not isinstance(self.record_cls_or_service_name, str):
67
+ return self.record_cls_or_service_name
68
+ return current_service_registry.get(
69
+ self.record_cls_or_service_name
70
+ ).config.record_cls
71
+
72
+ def __get__(self, instance, owner):
73
+ search_as_you_type_fields: List[SuggestField] = []
74
+ locale = get_locale()
75
+ if locale:
76
+ language = locale.language
77
+
78
+ for fld_name, fld in inspect.getmembers(
79
+ self.record_cls, lambda x: isinstance(x, ICUSuggestField)
80
+ ):
81
+ search_as_you_type_fields.append(
82
+ SuggestField(f"{fld_name}.{language}.original", 2)
83
+ )
84
+ search_as_you_type_fields.append(
85
+ SuggestField(f"{fld_name}.{language}.no_accent", 1)
86
+ )
87
+
88
+ if not search_as_you_type_fields:
89
+ search_as_you_type_fields.extend(self.default_fields)
90
+
91
+ search_as_you_type_fields.extend(self.extra_fields)
92
+
93
+ fields = []
94
+ for fld in search_as_you_type_fields:
95
+ fields.append(f"{fld.field}^{fld.boost * fld.boost_exact}")
96
+ if fld.use_ngrams:
97
+ fields.append(f"{fld.field}._2gram^{fld.boost * fld.boost_2gram}")
98
+ fields.append(f"{fld.field}._3gram^{fld.boost * fld.boost_3gram}")
99
+ fields.append(
100
+ f"{fld.field}._index_prefix^{fld.boost * fld.boost_prefix}"
101
+ )
102
+
103
+ return SuggestQueryParser.factory(
104
+ fields=fields,
105
+ )
106
+
107
+
108
+ @dataclasses.dataclass
109
+ class SortField:
110
+ option_name: str = "title"
111
+ icu_sort_field: str = "sort"
112
+ title: str = "Title"
113
+
114
+
115
+ class ICUSortOptions:
116
+ def __init__(self, record_cls_or_service_name, fields: List[SortField] = None):
117
+ self.record_cls_or_service_name = record_cls_or_service_name
118
+ self.fields = fields or [SortField()]
119
+
120
+ @property
121
+ def record_cls(self):
122
+ if not isinstance(self.record_cls_or_service_name, str):
123
+ return self.record_cls_or_service_name
124
+ return current_service_registry.get(
125
+ self.record_cls_or_service_name
126
+ ).config.record_cls
127
+
128
+ def __get__(self, instance, owner):
129
+ if not inspect.isclass(owner):
130
+ owner = type(owner)
131
+ super_options = {}
132
+ for mro in list(owner.mro())[1:]:
133
+ if hasattr(mro, "sort_options"):
134
+ super_options = mro.sort_options
135
+ break
136
+
137
+ ret = {
138
+ **super_options,
139
+ **getattr(owner, "extra_sort_options", {}),
140
+ }
141
+
142
+ # transform the sort options by the current language
143
+ locale = get_locale()
144
+ if not locale:
145
+ return ret
146
+
147
+ language = locale.language
148
+
149
+ for sort_field in self.fields:
150
+ icu_field = getattr(self.record_cls, sort_field.icu_sort_field)
151
+ ret[sort_field.option_name] = {
152
+ "title": sort_field.title,
153
+ "fields": [f"{icu_field.key}.{language}"],
154
+ }
155
+ return ret
156
+
157
+
158
+ class I18nSearchOptions(SearchOptions):
159
+ extra_sort_options = {}
160
+ record_cls = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: oarepo-runtime
3
- Version: 1.4.12
3
+ Version: 1.4.13
4
4
  Summary: A set of runtime extensions of Invenio repository
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
@@ -106,71 +106,37 @@ preconfigured permission sets to service config.
106
106
 
107
107
  To use ICU sort and suggestion custom fields, provide the following configuration
108
108
  to `oarepo-model-builder` (or put this stuff to your custom superclasses).
109
- Please rename those RUNTIME_TEST_XXX to your constants
110
109
 
111
110
  ```yaml
112
111
  record:
113
112
  imports:
114
- - import: oarepo_runtime.cf.CustomFields
115
113
  - import: invenio_records_resources.records.api.Record
116
114
  alias: InvenioRecord
117
- - import: invenio_records_resources.records.dumpers.CustomFieldsDumperExt
115
+ - import: oarepo_runtime.records.SystemFieldDumperExt
116
+ - import: oarepo_runtime.records.icu.ICUSortField
117
+ - import: oarepo_runtime.records.icu.ICUSuggestField
118
118
  extra-code: |-2
119
119
  # extra custom fields for testing ICU sorting and suggesting
120
- sort = CustomFields(
121
- "RUNTIME_TEST_SORT_CF",
122
- "sort",
123
- clear_none=True,
124
- create_if_missing=True,
125
- )
126
- suggest = CustomFields(
127
- "RUNTIME_TEST_SUGGEST_CF",
128
- "suggest",
129
- clear_none=True,
130
- create_if_missing=True,
131
- )
120
+ sort = ICUSortField(source_field="metadata.title")
121
+ suggest = ICUSuggestField(source_field="metadata.title")
132
122
  search-options:
133
123
  base-classes:
134
124
  - I18nSearchOptions
135
125
  imports:
136
- - import: oarepo_runtime.services.search.I18nSearchOptions
126
+ - import: oarepo_runtime.services.icu.I18nSearchOptions
127
+ - import: oarepo_runtime.services.icu.ICUSuggestParser
128
+ - import: oarepo_runtime.services.icu.ICUSortOptions
137
129
  sort-options-field: extra_sort_options
138
130
  extra-code: |-2
139
- SORT_CUSTOM_FIELD_NAME = "RUNTIME_TEST_SORT_CF"
140
- SUGGEST_CUSTOM_FIELD_NAME = "RUNTIME_TEST_SUGGEST_CF"
131
+ suggest_parser_cls = ICUSuggestParser("records2")
132
+ sort_options = ICUSortOptions("records2")
141
133
 
142
134
  record-dumper:
143
135
  extensions:
144
- - CustomFieldsDumperExt("RUNTIME_TEST_SORT_CF", "sort")
145
- - CustomFieldsDumperExt("RUNTIME_TEST_SUGGEST_CF", "suggest")
136
+ - SystemFieldDumperExt()
146
137
  ```
147
138
 
148
- Then add the constants to your invenio.cfg
149
-
150
- ```python
151
- from oarepo_runtime.cf.icu import ICUSortCF, ICUSuggestCF
152
-
153
- RUNTIME_TEST_SORT_CF = [
154
- ICUSortCF(
155
- language = "cs",
156
- opensearch_language = "czech",
157
- source_field = "metadata.title",
158
- # cf_name=None, # name of generated custom field, default is <language>
159
- # country=None,
160
- # variant=None,
161
- # sort_option=None, # default is last part of <source_field>
162
- )
163
- ]
164
- RUNTIME_TEST_SUGGEST_CF = [
165
- ICUSuggestCF(
166
- language="cs",
167
- opensearch_language="czech",
168
- source_field="metadata.title",
169
- # cf_name = None # name of generated custom field, default is <language>
170
- )
171
- ]
172
- ```
173
-
174
- Then run `invenio oarepo cf init` to initialize custom fields,
175
- `invenio oarepo index reindex` if you already have data inside the repository
176
- and from this moment on, `/records?sort=title`
139
+ Run `invenio oarepo cf init` to initialize custom fields,
140
+ `invenio oarepo index reindex` if you already have data
141
+ inside the repository and from this moment on,
142
+ `/records?sort=title` and `/records?suggest=abc` should work
@@ -5,15 +5,14 @@ oarepo_runtime/marshmallow.py,sha256=BaRh9Z07h2DznYMyYxiTmSfe4EJaeXvTY8lKVyvVGa4
5
5
  oarepo_runtime/polymorphic.py,sha256=CkvXVUiXbrsLWFgoNnjjpUviQyzRMCmpsD3GWfV0WZA,494
6
6
  oarepo_runtime/profile.py,sha256=QzrQoZncjoN74ZZnpkEKakNk08KCzBU7m6y42RN8AMY,1637
7
7
  oarepo_runtime/uow.py,sha256=TqVYF1N24WTVIjf97mlb-EH-BtIZCVVLNS4bqZt4XLs,3201
8
- oarepo_runtime/cf/__init__.py,sha256=z6PcNsz2Xtp6ozDeZ25plYjmjKaKHXoovd_uzjKzRFs,1322
9
- oarepo_runtime/cf/cli.py,sha256=Q4WJKf8fL18Sl1Imbr1viilEnLLsJqcc-us77sCdtEs,552
10
- oarepo_runtime/cf/icu.py,sha256=DPFOgje9L1a2DmOtDvb-rKmjWezAp_veJFkhfKBU9pg,2920
11
- oarepo_runtime/cf/mappings.py,sha256=uCLyMEchJPtQLR5jUQKZmuZkWAMRXLk5JnNOH30E3Y0,4901
8
+ oarepo_runtime/cf/__init__.py,sha256=-YhE1JnHrX-qS_IT_1Lh5_OQ2OMbErQyZ3mPxLbiYqw,2241
9
+ oarepo_runtime/cf/cli.py,sha256=gUD4tRV57ixpgQxECN-eporJnC9etBfHE-FNKEOUUsY,316
10
+ oarepo_runtime/cf/mappings.py,sha256=6KKiS93tMPZOcmdBBvfXh3VOAh5Kw8xdXh2ER2PP0Xo,4200
12
11
  oarepo_runtime/cli/__init__.py,sha256=-WGXmjHoSqiApR_LvYnZTimuL-frR7SynrsSklnjb3A,221
13
12
  oarepo_runtime/cli/assets.py,sha256=XLZTnsGb88O5N8R2D3AYpZqtnO4JrbybUtRLKnL1p3w,2430
14
13
  oarepo_runtime/cli/base.py,sha256=xZMsR2rati5Mz0DZzmnlhVI7E6ePCfnOiTayrxT9cWU,259
15
14
  oarepo_runtime/cli/check.py,sha256=zQ6txhfN6LWvdWgRa3ZFdOifbuazoRQd-7ml0qwIBWg,2358
16
- oarepo_runtime/cli/index.py,sha256=fZMMw0Z4vwJ9fe7g8tb4wyoAMKeushxORvIUgxWQA40,4701
15
+ oarepo_runtime/cli/index.py,sha256=bZHxsJDzCv96_6n27lmLE9ssTgx3NViq9lTeRUJBv-4,4684
17
16
  oarepo_runtime/cli/validate.py,sha256=HpSvHQCGHlrdgdpKix9cIlzlBoJEiT1vACZdMnOUGEY,2827
18
17
  oarepo_runtime/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
18
  oarepo_runtime/config/permissions_presets.py,sha256=zApeA-2DYAlD--SzVz3vq_OFjq48Ko0pe08e4o2vxr4,6114
@@ -58,10 +57,12 @@ oarepo_runtime/i18n/dumper.py,sha256=PbNFCLsiH4XV3E1v8xga_fzlcEImHy8OXn_UKh_8VBU
58
57
  oarepo_runtime/i18n/schema.py,sha256=wJLyE4Ew4mqEGoMpM6JOsyyqGCV_R4YgWQUm45AHVPU,1184
59
58
  oarepo_runtime/i18n/ui_schema.py,sha256=_iMUkHI8fPQT9hJziSMqYRyGmozgJrY5qQo1Y7Fmuo4,2085
60
59
  oarepo_runtime/i18n/validation.py,sha256=fyMTi2Rw-KiHv7c7HN61zGxRVa9sAjAEEkAL5wUyKNo,236
60
+ oarepo_runtime/records/__init__.py,sha256=jzKWn4jaVKZvysPZHtqWE_SWExh_4DBz7YSgflFyYoM,721
61
+ oarepo_runtime/records/icu.py,sha256=h8RkqAO8d0LOqj27IVa-B-Bqs08na0lffqczJZHTgUU,4137
61
62
  oarepo_runtime/relations/__init__.py,sha256=bDAgxl_LdKsqpGG3qluxAkQnn5u2ItJngnHQKkqzlkE,373
62
63
  oarepo_runtime/relations/base.py,sha256=I5XANA-fFbiH3xQ1s7x1NVJFaiSRKCc6-xyNy1LEfpw,8739
63
64
  oarepo_runtime/relations/components.py,sha256=J9rvzaAoRDbSVuA01hIOlXKQP-OE5VJI5w5xuMsFO70,602
64
- oarepo_runtime/relations/errors.py,sha256=n898MgnkF2T7iOG-VJCOV0of27mJrCNCOLqbeKUv2rA,530
65
+ oarepo_runtime/relations/errors.py,sha256=VtlOKq9MEUeJ4IsiZhY7lWoshrusA_RL4SOHe2titno,552
65
66
  oarepo_runtime/relations/internal.py,sha256=OTp8iJqyl80sWDk0Q0AK42l6UsxZDABspVU_GwWza9o,1556
66
67
  oarepo_runtime/relations/lookup.py,sha256=wi3jPfOedazOmhOMrgu50PUETc1jfSdpmjK0wvOFsEM,848
67
68
  oarepo_runtime/relations/mapping.py,sha256=jwFCWCnW8hb44ZZBeVf96QR79S9FT2hvm4L8EuCBo5U,1277
@@ -70,7 +71,7 @@ oarepo_runtime/relations/uow.py,sha256=KrN8B-wVbmb0kOErxb7bAhPmOR6-mMRgBr-ab-ir6
70
71
  oarepo_runtime/resolvers/__init__.py,sha256=kTlvSiympib59YQV7wEKpIXGprPWRuvxLIwmeeQdUec,89
71
72
  oarepo_runtime/resolvers/proxies.py,sha256=egtT7uXL91KswWI7gqUIaz1vWIHezdsiI_M-xRKXWww,547
72
73
  oarepo_runtime/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
73
- oarepo_runtime/services/search.py,sha256=eMOosBgDAsscm8YpU7ckhaD9iBNnjE6z7-QWevwV-PI,3692
74
+ oarepo_runtime/services/icu.py,sha256=5uxCHEBIEBlKo8IO4y_GA6InYVPAq6sPmWXWeBOXLA8,4969
74
75
  oarepo_runtime/tasks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
76
  oarepo_runtime/tasks/datastreams.py,sha256=_EJZuHg2UoIJCrrOo0J7e8uCSoXIpJsRJnsb8sorWJc,10810
76
77
  oarepo_runtime/translations/messages.pot,sha256=NfZakWEYMZE_6HvnKxO7B61UnHnWgtKSvSKMJQ6msK0,971
@@ -84,9 +85,9 @@ oarepo_runtime/utils/path.py,sha256=V1NVyk3m12_YLbj7QHYvUpE1wScO78bYsX1LOLeXDkI,
84
85
  oarepo_runtime/validation/__init__.py,sha256=lU7DgZq8pGD5Pa-QqL9gvLsib3IYtM-Y56k-NwHrPG0,166
85
86
  oarepo_runtime/validation/dates.py,sha256=fahqKGDdIYWux5ZeoljrEe8VD2fDZR9VpfvYmTYAmpw,1050
86
87
  tests/pkg_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
- oarepo_runtime-1.4.12.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
88
- oarepo_runtime-1.4.12.dist-info/METADATA,sha256=lmrnBtECyezkWgVcZNI_OJT2O7-AFMY9sDf8rERu5Bc,5179
89
- oarepo_runtime-1.4.12.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
90
- oarepo_runtime-1.4.12.dist-info/entry_points.txt,sha256=C32W4eT-8OypMCfwOO5WREioVKSneDfY51D78Uvdbp0,231
91
- oarepo_runtime-1.4.12.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
92
- oarepo_runtime-1.4.12.dist-info/RECORD,,
88
+ oarepo_runtime-1.4.13.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
89
+ oarepo_runtime-1.4.13.dist-info/METADATA,sha256=qQgSqdIgdpLdcP8i5I8j9B3b_bhyHLdJGUxZGYu4u3Y,4288
90
+ oarepo_runtime-1.4.13.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
91
+ oarepo_runtime-1.4.13.dist-info/entry_points.txt,sha256=C32W4eT-8OypMCfwOO5WREioVKSneDfY51D78Uvdbp0,231
92
+ oarepo_runtime-1.4.13.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
93
+ oarepo_runtime-1.4.13.dist-info/RECORD,,
oarepo_runtime/cf/icu.py DELETED
@@ -1,105 +0,0 @@
1
- from invenio_records_resources.services.custom_fields import BaseCF
2
-
3
- from oarepo_runtime.relations.lookup import lookup_key
4
-
5
-
6
- class ICUSortCF(BaseCF):
7
- def __init__(
8
- self,
9
- language,
10
- opensearch_language,
11
- source_field,
12
- cf_name=None,
13
- country=None,
14
- variant=None,
15
- sort_option=None,
16
- field_args=None,
17
- ):
18
- super().__init__(name=cf_name or language, field_args=field_args)
19
- self.opensearch_language = opensearch_language
20
- self.country = country
21
- self.variant = variant
22
- self.source_field = source_field
23
- self.sort_option = sort_option or source_field.rsplit(".", maxsplit=1)[-1]
24
- self.language = language
25
-
26
- @property
27
- def mapping(self):
28
- ret = {
29
- "type": "icu_collation_keyword",
30
- "index": False,
31
- "language": self.opensearch_language,
32
- }
33
- if self.country:
34
- ret["country"] = self.country
35
-
36
- if self.variant:
37
- ret["variant"] = self.variant
38
-
39
- return ret
40
-
41
- @property
42
- def field(self):
43
- return None
44
-
45
- def dump(self, record, cf_key="custom_fields"):
46
- ret = []
47
- for l in lookup_key(record, self.source_field):
48
- ret.append(l.value)
49
- record.setdefault(cf_key, {})[self.name] = ret
50
-
51
- def load(self, record, cf_key="custom_fields"):
52
- record.pop(cf_key, None)
53
-
54
-
55
- class ICUSuggestCF(BaseCF):
56
- def __init__(
57
- self, language, opensearch_language, source_field, cf_name=None, field_args=None
58
- ):
59
- super().__init__(name=cf_name or language, field_args=field_args)
60
- self.opensearch_language = opensearch_language
61
- self.source_field = source_field
62
- self.language = language
63
-
64
- @property
65
- def mapping(self):
66
- ret = {
67
- "type": "text",
68
- "fields": {
69
- "original": {
70
- "type": "search_as_you_type",
71
- },
72
- "no_accent": {
73
- "type": "search_as_you_type",
74
- "analyzer": "accent_removal_analyzer",
75
- },
76
- },
77
- }
78
- return ret
79
-
80
- @property
81
- def mapping_settings(self):
82
- return {
83
- "analysis": {
84
- "analyzer": {
85
- "accent_removal_analyzer": {
86
- "type": "custom",
87
- "tokenizer": "standard",
88
- "filter": ["lowercase", "asciifolding"],
89
- }
90
- }
91
- }
92
- }
93
-
94
- @property
95
- def field(self):
96
- return None
97
-
98
- def dump(self, record, cf_key="custom_fields"):
99
- ret = []
100
- for l in lookup_key(record, self.source_field):
101
- ret.append(l.value)
102
- record.setdefault(cf_key, {})[self.name] = ret
103
-
104
- def load(self, record, cf_key="custom_fields"):
105
- record.pop(cf_key, None)
@@ -1,112 +0,0 @@
1
- import dataclasses
2
- from typing import Tuple, List
3
-
4
- from invenio_records_resources.services.records import (
5
- SearchOptions as InvenioSearchOptions,
6
- )
7
-
8
- # TODO: integrate this to invenio_records_resources.services.records
9
- from flask_babelex import lazy_gettext as _
10
- from invenio_records_resources.services.records.queryparser import SuggestQueryParser
11
- from sqlalchemy.util import classproperty
12
- from flask import current_app
13
-
14
- from oarepo_runtime.cf.icu import ICUSortCF
15
-
16
- try:
17
- from invenio_i18n import get_locale
18
- except ImportError:
19
- from invenio_i18n.babel import get_locale
20
-
21
-
22
- class SearchOptions(InvenioSearchOptions):
23
- sort_options = {
24
- "title": dict(
25
- title=_("By Title"),
26
- fields=["metadata.title"], # ES defaults to desc on `_score` field
27
- ),
28
- "bestmatch": dict(
29
- title=_("Best match"),
30
- fields=["_score"], # ES defaults to desc on `_score` field
31
- ),
32
- "newest": dict(
33
- title=_("Newest"),
34
- fields=["-created"],
35
- ),
36
- "oldest": dict(
37
- title=_("Oldest"),
38
- fields=["created"],
39
- ),
40
- }
41
-
42
-
43
- @dataclasses.dataclass
44
- class SuggestField:
45
- field: str
46
- boost: int
47
- use_ngrams: bool = True
48
- boost_exact: float = 5
49
- boost_2gram: float = 1
50
- boost_3gram: float = 1
51
- boost_prefix: float = 1
52
-
53
-
54
- class I18nSearchOptions(SearchOptions):
55
- SORT_CUSTOM_FIELD_NAME = None
56
- SUGGEST_CUSTOM_FIELD_NAME = None
57
- SUGGEST_SEARCH_AS_YOU_TYPE_FIELDS: Tuple[SuggestField] = (
58
- SuggestField("metadata.title", 1, use_ngrams=False),
59
- SuggestField("id", 1, use_ngrams=False),
60
- )
61
- extra_sort_options = {}
62
-
63
- @classproperty
64
- def sort_options(clz):
65
- ret = {**super().sort_options, **clz.extra_sort_options}
66
- if not clz.SORT_CUSTOM_FIELD_NAME:
67
- return ret
68
-
69
- # transform the sort options by the current language
70
- locale = get_locale()
71
- if not locale:
72
- return ret
73
- language = locale.language
74
- cf: ICUSortCF
75
- for cf in current_app.config[clz.SORT_CUSTOM_FIELD_NAME]:
76
- if cf.language == language:
77
- if cf.sort_option not in ret:
78
- ret[cf.sort_option] = {"title": _(cf.sort_option.capitalize())}
79
- ret[cf.sort_option]["fields"] = [f"sort.{cf.name}"]
80
- return ret
81
-
82
- @classproperty
83
- def suggest_parser_cls(clz):
84
- search_as_you_type_fields: List[SuggestField] = []
85
- locale = get_locale()
86
-
87
- if clz.SUGGEST_CUSTOM_FIELD_NAME and locale:
88
- language = locale.language
89
- for cf in current_app.config[clz.SUGGEST_CUSTOM_FIELD_NAME]:
90
- if cf.language == language:
91
- search_as_you_type_fields.append(
92
- SuggestField(f"suggest.{cf.name}.original", 2)
93
- )
94
- search_as_you_type_fields.append(
95
- SuggestField(f"suggest.{cf.name}.no_accent", 1)
96
- )
97
- if not search_as_you_type_fields:
98
- search_as_you_type_fields = clz.SUGGEST_SEARCH_AS_YOU_TYPE_FIELDS # noqa
99
-
100
- fields = []
101
- for fld in search_as_you_type_fields:
102
- fields.append(f"{fld.field}^{fld.boost * fld.boost_exact}")
103
- if fld.use_ngrams:
104
- fields.append(f"{fld.field}._2gram^{fld.boost * fld.boost_2gram}")
105
- fields.append(f"{fld.field}._3gram^{fld.boost * fld.boost_3gram}")
106
- fields.append(
107
- f"{fld.field}._index_prefix^{fld.boost * fld.boost_prefix}"
108
- )
109
-
110
- return SuggestQueryParser.factory(
111
- fields=fields,
112
- )