oarepo-runtime 1.4.11__tar.gz → 1.4.13__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. {oarepo-runtime-1.4.11/oarepo_runtime.egg-info → oarepo-runtime-1.4.13}/PKG-INFO +40 -1
  2. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/README.md +40 -1
  3. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cf/__init__.py +32 -2
  4. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cf/cli.py +2 -11
  5. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cf/mappings.py +22 -35
  6. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cli/index.py +0 -1
  7. oarepo-runtime-1.4.13/oarepo_runtime/records/__init__.py +29 -0
  8. oarepo-runtime-1.4.13/oarepo_runtime/records/icu.py +132 -0
  9. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/errors.py +3 -1
  10. oarepo-runtime-1.4.13/oarepo_runtime/services/icu.py +160 -0
  11. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13/oarepo_runtime.egg-info}/PKG-INFO +40 -1
  12. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime.egg-info/SOURCES.txt +3 -2
  13. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/setup.cfg +1 -1
  14. oarepo-runtime-1.4.11/oarepo_runtime/cf/icu.py +0 -92
  15. oarepo-runtime-1.4.11/oarepo_runtime/services/search.py +0 -108
  16. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/LICENSE +0 -0
  17. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/MANIFEST.in +0 -0
  18. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/__init__.py +0 -0
  19. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cli/__init__.py +0 -0
  20. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cli/assets.py +0 -0
  21. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cli/base.py +0 -0
  22. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cli/check.py +0 -0
  23. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/cli/validate.py +0 -0
  24. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/config/__init__.py +0 -0
  25. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/config/permissions_presets.py +0 -0
  26. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/config/service.py +0 -0
  27. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/__init__.py +0 -0
  28. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/batch.py +0 -0
  29. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/catalogue.py +0 -0
  30. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/cli.py +0 -0
  31. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/config.py +0 -0
  32. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/datastreams.py +0 -0
  33. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/errors.py +0 -0
  34. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/fixtures.py +0 -0
  35. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/readers/__init__.py +0 -0
  36. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/readers/attachments.py +0 -0
  37. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/readers/excel.py +0 -0
  38. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/readers/json.py +0 -0
  39. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/readers/service.py +0 -0
  40. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/readers/yaml.py +0 -0
  41. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/transformers.py +0 -0
  42. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/utils.py +0 -0
  43. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/writers/__init__.py +0 -0
  44. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/writers/attachment.py +0 -0
  45. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/writers/service.py +0 -0
  46. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/writers/validation_errors.py +0 -0
  47. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/datastreams/writers/yaml.py +0 -0
  48. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/drafts/__init__.py +0 -0
  49. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/drafts/systemfields/__init__.py +0 -0
  50. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/drafts/systemfields/has_draftcheck.py +0 -0
  51. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/expansions/__init__.py +0 -0
  52. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/expansions/expandable_fields.py +0 -0
  53. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/expansions/service.py +0 -0
  54. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/ext.py +0 -0
  55. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/ext_config.py +0 -0
  56. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/facets/__init__.py +0 -0
  57. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/facets/base.py +0 -0
  58. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/facets/date.py +0 -0
  59. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/facets/enum.py +0 -0
  60. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/facets/max_facet.py +0 -0
  61. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/facets/nested_facet.py +0 -0
  62. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/facets/params.py +0 -0
  63. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/i18n/__init__.py +0 -0
  64. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/i18n/default_translations.py +0 -0
  65. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/i18n/dumper.py +0 -0
  66. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/i18n/schema.py +0 -0
  67. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/i18n/ui_schema.py +0 -0
  68. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/i18n/validation.py +0 -0
  69. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/marshmallow.py +0 -0
  70. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/polymorphic.py +0 -0
  71. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/profile.py +0 -0
  72. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/__init__.py +0 -0
  73. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/base.py +0 -0
  74. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/components.py +0 -0
  75. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/internal.py +0 -0
  76. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/lookup.py +0 -0
  77. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/mapping.py +0 -0
  78. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/pid_relation.py +0 -0
  79. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/relations/uow.py +0 -0
  80. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/resolvers/__init__.py +0 -0
  81. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/resolvers/proxies.py +0 -0
  82. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/services/__init__.py +0 -0
  83. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/tasks/__init__.py +0 -0
  84. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/tasks/datastreams.py +0 -0
  85. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/translations/cs/LC_MESSAGES/messages.mo +0 -0
  86. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/translations/cs/LC_MESSAGES/messages.po +0 -0
  87. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/translations/en/LC_MESSAGES/messages.po +0 -0
  88. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/translations/messages.pot +0 -0
  89. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/ui/__init__.py +0 -0
  90. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/ui/marshmallow.py +0 -0
  91. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/uow.py +0 -0
  92. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/utils/__init__.py +0 -0
  93. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/utils/path.py +0 -0
  94. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/validation/__init__.py +0 -0
  95. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime/validation/dates.py +0 -0
  96. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime.egg-info/dependency_links.txt +0 -0
  97. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime.egg-info/entry_points.txt +0 -0
  98. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime.egg-info/requires.txt +0 -0
  99. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/oarepo_runtime.egg-info/top_level.txt +0 -0
  100. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/pyproject.toml +0 -0
  101. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/setup.py +0 -0
  102. {oarepo-runtime-1.4.11 → oarepo-runtime-1.4.13}/tests/pkg_data/__init__.py +0 -0
@@ -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
@@ -65,4 +65,43 @@ This module provides a marshmallow validator for date strings.
65
65
  ## Config
66
66
 
67
67
  Provides interface and definitions for loading
68
- preconfigured permission sets to service config.
68
+ preconfigured permission sets to service config.
69
+
70
+ ## ICU sort and suggestions
71
+
72
+ To use ICU sort and suggestion custom fields, provide the following configuration
73
+ to `oarepo-model-builder` (or put this stuff to your custom superclasses).
74
+
75
+ ```yaml
76
+ record:
77
+ imports:
78
+ - import: invenio_records_resources.records.api.Record
79
+ alias: InvenioRecord
80
+ - import: oarepo_runtime.records.SystemFieldDumperExt
81
+ - import: oarepo_runtime.records.icu.ICUSortField
82
+ - import: oarepo_runtime.records.icu.ICUSuggestField
83
+ extra-code: |-2
84
+ # extra custom fields for testing ICU sorting and suggesting
85
+ sort = ICUSortField(source_field="metadata.title")
86
+ suggest = ICUSuggestField(source_field="metadata.title")
87
+ search-options:
88
+ base-classes:
89
+ - I18nSearchOptions
90
+ imports:
91
+ - import: oarepo_runtime.services.icu.I18nSearchOptions
92
+ - import: oarepo_runtime.services.icu.ICUSuggestParser
93
+ - import: oarepo_runtime.services.icu.ICUSortOptions
94
+ sort-options-field: extra_sort_options
95
+ extra-code: |-2
96
+ suggest_parser_cls = ICUSuggestParser("records2")
97
+ sort_options = ICUSortOptions("records2")
98
+
99
+ record-dumper:
100
+ extensions:
101
+ - SystemFieldDumperExt()
102
+ ```
103
+
104
+ Run `invenio oarepo cf init` to initialize custom fields,
105
+ `invenio oarepo index reindex` if you already have data
106
+ inside the repository and from this moment on,
107
+ `/records?sort=title` and `/records?suggest=abc` should work
@@ -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):
@@ -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
@@ -19,7 +19,6 @@ oarepo_runtime.egg-info/requires.txt
19
19
  oarepo_runtime.egg-info/top_level.txt
20
20
  oarepo_runtime/cf/__init__.py
21
21
  oarepo_runtime/cf/cli.py
22
- oarepo_runtime/cf/icu.py
23
22
  oarepo_runtime/cf/mappings.py
24
23
  oarepo_runtime/cli/__init__.py
25
24
  oarepo_runtime/cli/assets.py
@@ -70,6 +69,8 @@ oarepo_runtime/i18n/dumper.py
70
69
  oarepo_runtime/i18n/schema.py
71
70
  oarepo_runtime/i18n/ui_schema.py
72
71
  oarepo_runtime/i18n/validation.py
72
+ oarepo_runtime/records/__init__.py
73
+ oarepo_runtime/records/icu.py
73
74
  oarepo_runtime/relations/__init__.py
74
75
  oarepo_runtime/relations/base.py
75
76
  oarepo_runtime/relations/components.py
@@ -82,7 +83,7 @@ oarepo_runtime/relations/uow.py
82
83
  oarepo_runtime/resolvers/__init__.py
83
84
  oarepo_runtime/resolvers/proxies.py
84
85
  oarepo_runtime/services/__init__.py
85
- oarepo_runtime/services/search.py
86
+ oarepo_runtime/services/icu.py
86
87
  oarepo_runtime/tasks/__init__.py
87
88
  oarepo_runtime/tasks/datastreams.py
88
89
  oarepo_runtime/translations/messages.pot
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = oarepo-runtime
3
- version = 1.4.11
3
+ version = 1.4.13
4
4
  description = A set of runtime extensions of Invenio repository
5
5
  authors = Alzbeta Pokorna
6
6
  readme = README.md
@@ -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
- )
File without changes