oarepo-runtime 1.4.11__py3-none-any.whl → 1.4.13__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.
@@ -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.11
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
@@ -101,3 +101,42 @@ This module provides a marshmallow validator for date strings.
101
101
 
102
102
  Provides interface and definitions for loading
103
103
  preconfigured permission sets to service config.
104
+
105
+ ## ICU sort and suggestions
106
+
107
+ To use ICU sort and suggestion custom fields, provide the following configuration
108
+ to `oarepo-model-builder` (or put this stuff to your custom superclasses).
109
+
110
+ ```yaml
111
+ record:
112
+ imports:
113
+ - import: invenio_records_resources.records.api.Record
114
+ alias: InvenioRecord
115
+ - import: oarepo_runtime.records.SystemFieldDumperExt
116
+ - import: oarepo_runtime.records.icu.ICUSortField
117
+ - import: oarepo_runtime.records.icu.ICUSuggestField
118
+ extra-code: |-2
119
+ # extra custom fields for testing ICU sorting and suggesting
120
+ sort = ICUSortField(source_field="metadata.title")
121
+ suggest = ICUSuggestField(source_field="metadata.title")
122
+ search-options:
123
+ base-classes:
124
+ - I18nSearchOptions
125
+ imports:
126
+ - import: oarepo_runtime.services.icu.I18nSearchOptions
127
+ - import: oarepo_runtime.services.icu.ICUSuggestParser
128
+ - import: oarepo_runtime.services.icu.ICUSortOptions
129
+ sort-options-field: extra_sort_options
130
+ extra-code: |-2
131
+ suggest_parser_cls = ICUSuggestParser("records2")
132
+ sort_options = ICUSortOptions("records2")
133
+
134
+ record-dumper:
135
+ extensions:
136
+ - SystemFieldDumperExt()
137
+ ```
138
+
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=MYS2RryxtsiQV_6omQDRoMzbdPH0uNLxGipwrHLsmBM,2531
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=b_4YOR9lWpPGorcl1dbdGDgMO3U24uIQdBYm2LiQmUY,3504
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.11.dist-info/LICENSE,sha256=h2uWz0OaB3EN-J1ImdGJZzc7yvfQjvHVYdUhQ-H7ypY,1064
88
- oarepo_runtime-1.4.11.dist-info/METADATA,sha256=lIeOcoRE9fZBuRdEN6g3oUlZwg-Z6Gbkn67YPPggNk4,2859
89
- oarepo_runtime-1.4.11.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
90
- oarepo_runtime-1.4.11.dist-info/entry_points.txt,sha256=C32W4eT-8OypMCfwOO5WREioVKSneDfY51D78Uvdbp0,231
91
- oarepo_runtime-1.4.11.dist-info/top_level.txt,sha256=bHhlkT1_RQC4IkfTQCqA3iN4KCB6cSFQlsXpQMSP-bE,21
92
- oarepo_runtime-1.4.11.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,92 +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, name, language, source_field, country=None, variant=None, field_args=None
9
- ):
10
- super().__init__(name=name, field_args=field_args)
11
- self.language = language
12
- self.country = country
13
- self.variant = variant
14
- self.source_field = source_field
15
-
16
- @property
17
- def mapping(self):
18
- ret = {
19
- "type": "icu_collation_keyword",
20
- "index": False,
21
- "language": self.language,
22
- }
23
- if self.country:
24
- ret["country"] = self.country
25
-
26
- if self.variant:
27
- ret["variant"] = self.variant
28
-
29
- return ret
30
-
31
- @property
32
- def field(self):
33
- return None
34
-
35
- def dump(self, record, cf_key="custom_fields"):
36
- ret = []
37
- for l in lookup_key(record, self.source_field):
38
- ret.append(l.value)
39
- record.setdefault(cf_key, {})[self.name] = ret
40
-
41
- def load(self, record, cf_key="custom_fields"):
42
- record.pop(cf_key, None)
43
-
44
-
45
- class ICUSuggestCF(BaseCF):
46
- def __init__(self, name, language, source_field, field_args=None):
47
- super().__init__(name=name, field_args=field_args)
48
- self.language = language
49
- self.source_field = source_field
50
-
51
- @property
52
- def mapping(self):
53
- ret = {
54
- "type": "text",
55
- "fields": {
56
- "original": {
57
- "type": "search_as_you_type",
58
- },
59
- "no_accent": {
60
- "type": "search_as_you_type",
61
- "analyzer": "accent_removal_analyzer",
62
- },
63
- },
64
- }
65
- return ret
66
-
67
- @property
68
- def mapping_settings(self):
69
- return {
70
- "analysis": {
71
- "analyzer": {
72
- "accent_removal_analyzer": {
73
- "type": "custom",
74
- "tokenizer": "standard",
75
- "filter": ["lowercase", "asciifolding"],
76
- }
77
- }
78
- }
79
- }
80
-
81
- @property
82
- def field(self):
83
- return None
84
-
85
- def dump(self, record, cf_key="custom_fields"):
86
- ret = []
87
- for l in lookup_key(record, self.source_field):
88
- ret.append(l.value)
89
- record.setdefault(cf_key, {})[self.name] = ret
90
-
91
- def load(self, record, cf_key="custom_fields"):
92
- record.pop(cf_key, None)
@@ -1,108 +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
- try:
15
- from invenio_i18n import get_locale
16
- except ImportError:
17
- from invenio_i18n.babel import get_locale
18
-
19
-
20
- class SearchOptions(InvenioSearchOptions):
21
- sort_options = {
22
- "title": dict(
23
- title=_("By Title"),
24
- fields=["metadata.title"], # ES defaults to desc on `_score` field
25
- ),
26
- "bestmatch": dict(
27
- title=_("Best match"),
28
- fields=["_score"], # ES defaults to desc on `_score` field
29
- ),
30
- "newest": dict(
31
- title=_("Newest"),
32
- fields=["-created"],
33
- ),
34
- "oldest": dict(
35
- title=_("Oldest"),
36
- fields=["created"],
37
- ),
38
- }
39
-
40
-
41
- @dataclasses.dataclass
42
- class SuggestField:
43
- field: str
44
- boost: int
45
- use_ngrams: bool = True
46
- boost_exact: float = 5
47
- boost_2gram: float = 1
48
- boost_3gram: float = 1
49
- boost_prefix: float = 1
50
-
51
-
52
- class I18nSearchOptions(SearchOptions):
53
- SORT_CUSTOM_FIELD_NAME = None
54
- SUGGEST_CUSTOM_FIELD_NAME = None
55
- SUGGEST_SEARCH_AS_YOU_TYPE_FIELDS: Tuple[SuggestField] = (
56
- SuggestField("metadata.title", 1, use_ngrams=False),
57
- SuggestField("id", 1, use_ngrams=False),
58
- )
59
- extra_sort_options = {}
60
-
61
- @classproperty
62
- def sort_options(clz):
63
- ret = {**super().sort_options, **clz.extra_sort_options}
64
- if not clz.SORT_CUSTOM_FIELD_NAME:
65
- return ret
66
-
67
- # transform the sort options by the current language
68
- locale = get_locale()
69
- if not locale:
70
- return ret
71
- language = locale.language
72
- for cf in current_app.config[clz.SORT_CUSTOM_FIELD_NAME]:
73
- if cf.name == language:
74
- ret["title"]["fields"] = [f"sort.{cf.name}"]
75
- break
76
- return ret
77
-
78
- @classproperty
79
- def suggest_parser_cls(clz):
80
- search_as_you_type_fields: List[SuggestField] = []
81
- locale = get_locale()
82
-
83
- if clz.SUGGEST_CUSTOM_FIELD_NAME and locale:
84
- language = locale.language
85
- for cf in current_app.config[clz.SUGGEST_CUSTOM_FIELD_NAME]:
86
- if cf.name == language:
87
- search_as_you_type_fields.append(
88
- SuggestField(f"suggest.{language}.original", 2)
89
- )
90
- search_as_you_type_fields.append(
91
- SuggestField(f"suggest.{language}.no_accent", 1)
92
- )
93
- if not search_as_you_type_fields:
94
- search_as_you_type_fields = clz.SUGGEST_SEARCH_AS_YOU_TYPE_FIELDS # noqa
95
-
96
- fields = []
97
- for fld in search_as_you_type_fields:
98
- fields.append(f"{fld.field}^{fld.boost * fld.boost_exact}")
99
- if fld.use_ngrams:
100
- fields.append(f"{fld.field}._2gram^{fld.boost * fld.boost_2gram}")
101
- fields.append(f"{fld.field}._3gram^{fld.boost * fld.boost_3gram}")
102
- fields.append(
103
- f"{fld.field}._index_prefix^{fld.boost * fld.boost_prefix}"
104
- )
105
-
106
- return SuggestQueryParser.factory(
107
- fields=fields,
108
- )