folio-data-import 0.2.5__tar.gz → 0.2.7__tar.gz
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.
Potentially problematic release.
This version of folio-data-import might be problematic. Click here for more details.
- folio_data_import-0.2.7/PKG-INFO +140 -0
- folio_data_import-0.2.7/README.md +109 -0
- {folio_data_import-0.2.5 → folio_data_import-0.2.7}/pyproject.toml +2 -2
- {folio_data_import-0.2.5 → folio_data_import-0.2.7}/src/folio_data_import/MARCDataImport.py +50 -0
- {folio_data_import-0.2.5 → folio_data_import-0.2.7}/src/folio_data_import/UserImport.py +272 -44
- folio_data_import-0.2.7/src/folio_data_import/marc_preprocessors/__init__.py +1 -0
- folio_data_import-0.2.7/src/folio_data_import/marc_preprocessors/_preprocessors.py +31 -0
- folio_data_import-0.2.5/PKG-INFO +0 -68
- folio_data_import-0.2.5/README.md +0 -38
- {folio_data_import-0.2.5 → folio_data_import-0.2.7}/LICENSE +0 -0
- {folio_data_import-0.2.5 → folio_data_import-0.2.7}/src/folio_data_import/__init__.py +0 -0
- {folio_data_import-0.2.5 → folio_data_import-0.2.7}/src/folio_data_import/__main__.py +0 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: folio_data_import
|
|
3
|
+
Version: 0.2.7
|
|
4
|
+
Summary: A python module to interact with the data importing capabilities of the open-source FOLIO ILS
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Brooks Travis
|
|
7
|
+
Author-email: brooks.travis@gmail.com
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
|
17
|
+
Requires-Dist: flake8-bandit (>=4.1.1,<5.0.0)
|
|
18
|
+
Requires-Dist: flake8-black (>=0.3.6,<0.4.0)
|
|
19
|
+
Requires-Dist: flake8-bugbear (>=24.8.19,<25.0.0)
|
|
20
|
+
Requires-Dist: flake8-docstrings (>=1.7.0,<2.0.0)
|
|
21
|
+
Requires-Dist: flake8-isort (>=6.1.1,<7.0.0)
|
|
22
|
+
Requires-Dist: folioclient (>=0.61.0,<0.62.0)
|
|
23
|
+
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
24
|
+
Requires-Dist: inquirer (>=3.4.0,<4.0.0)
|
|
25
|
+
Requires-Dist: pyhumps (>=3.8.0,<4.0.0)
|
|
26
|
+
Requires-Dist: pymarc (>=5.2.2,<6.0.0)
|
|
27
|
+
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
28
|
+
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# folio_data_import
|
|
32
|
+
|
|
33
|
+
## Description
|
|
34
|
+
|
|
35
|
+
This project is designed to import data into the FOLIO LSP. It provides a simple and efficient way to import data from various sources using FOLIO's REST APIs.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- Import MARC records using FOLIO's Data Import system
|
|
40
|
+
- Import User records using FOLIO's User APIs
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
To install the project using Poetry, follow these steps:
|
|
47
|
+
|
|
48
|
+
1. Clone the repository.
|
|
49
|
+
2. Navigate to the project directory: `$ cd /path/to/folio_data_import`.
|
|
50
|
+
3. Install Poetry if you haven't already: `$ pip install poetry`.
|
|
51
|
+
4. Install the project and its dependencies: `$ poetry install`.
|
|
52
|
+
6. Run the application using Poetry: `$ poetry run python -m folio_data_import --help`.
|
|
53
|
+
|
|
54
|
+
Make sure to activate the virtual environment created by Poetry before running the application.
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
1. Prepare the data to be imported in the specified format.
|
|
59
|
+
2. Run the application and follow the prompts to import the data.
|
|
60
|
+
3. Monitor the import progress and handle any errors or conflicts that may arise.
|
|
61
|
+
|
|
62
|
+
### folio-user-import
|
|
63
|
+
When this package is installed via PyPI or using `poetry install` from this repository, it installs a convenience script in your `$PATH` called `folio-user-import`. To view all command line options for this script, run `folio-user-import -h`. In addition to supporting `mod-user-import`-style JSON objects, this script also allows you to manage service point assignments for users by specifying a `servicePointsUser` object in the JSON object, using service point codes in place of UUIDs in the `defaultServicePointId` and `servicePointIds` fields:
|
|
64
|
+
```
|
|
65
|
+
{
|
|
66
|
+
"username": "checkin-all",
|
|
67
|
+
"barcode": "1728439497039848103",
|
|
68
|
+
"active": true,
|
|
69
|
+
"type": "patron",
|
|
70
|
+
"patronGroup": "staff",
|
|
71
|
+
"departments": [],
|
|
72
|
+
"personal": {
|
|
73
|
+
"lastName": "Admin",
|
|
74
|
+
"firstName": "checkin-all",
|
|
75
|
+
"addresses": [
|
|
76
|
+
{
|
|
77
|
+
"countryId": "HU",
|
|
78
|
+
"addressLine1": "Andrássy Street 1.",
|
|
79
|
+
"addressLine2": "",
|
|
80
|
+
"city": "Budapest",
|
|
81
|
+
"region": "Pest",
|
|
82
|
+
"postalCode": "1061",
|
|
83
|
+
"addressTypeId": "Home",
|
|
84
|
+
"primaryAddress": true
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"preferredContactTypeId": "email"
|
|
88
|
+
},
|
|
89
|
+
"requestPreference": {
|
|
90
|
+
"holdShelf": true,
|
|
91
|
+
"delivery": false,
|
|
92
|
+
"fulfillment": "Hold Shelf"
|
|
93
|
+
}
|
|
94
|
+
"servicePointsUser": {
|
|
95
|
+
"defaultServicePointId": "cd1",
|
|
96
|
+
"servicePointsIds": [
|
|
97
|
+
"cd1",
|
|
98
|
+
"Online",
|
|
99
|
+
"000",
|
|
100
|
+
"cd2"
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
#### Matching Existing Users
|
|
106
|
+
|
|
107
|
+
Unlike mod-user-import, this importer does not require `externalSystemId` as the match point for your objects. If the user objects have `id` values, that will be used, falling back to `externalSystemId`. However, you can also specify `username` or `barcode` as the match point if desired, using the `--user_match_key` argument.
|
|
108
|
+
|
|
109
|
+
#### Preferred Contact Type Mapping
|
|
110
|
+
|
|
111
|
+
Another point of departure from the behavior of `mod-user-import` is the handling of `preferredContactTypeId`. This importer will accept either the `"001", "002", "003"...` values stored by the FOLIO, or the human-friendly strings used by `mod-user-import` (`"mail", "email", "text", "phone", "mobile"`). It will also __*set a customizable default for all users that do not otherwise have a valid value specified*__ (using `--default_preferred_contact_type`), unless a (valid) value is already present in the user record being updated.
|
|
112
|
+
|
|
113
|
+
#### Field Protection (*experimental*)
|
|
114
|
+
|
|
115
|
+
This script offers a rudimentary field protection implementation using custom fields. To enable this functionality, create a text custom field that has the field name `protectedFields`. In this field, you ca specify a comma-separated list of User schema field names, using dot-notation for nested fields. This protection should support all standard fields except addresses within `personal.addresses`. If you include `personal.addresses` in a user record, any existing addresses will be replaced by the new values.
|
|
116
|
+
|
|
117
|
+
##### Example
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
{
|
|
121
|
+
"protectedFields": "customFields.protectedFields,personal.preferredFirstName,barcode,personal.telephone,personal.addresses"
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Would result in `preferredFirstName`, `barcode`, and `telephone` remaining unchanged, regardless of the contents of the incoming records.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
#### How to use:
|
|
129
|
+
1. Generate a JSON lines (one JSON object per line) file of FOLIO user objects in the style of [mod-user-import](https://github.com/folio-org/mod-user-import)
|
|
130
|
+
2. Run the script and specify the required arguments (and any desired optional arguments), including the path to your file of user objects
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
Contributions are welcome! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
140
|
+
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# folio_data_import
|
|
2
|
+
|
|
3
|
+
## Description
|
|
4
|
+
|
|
5
|
+
This project is designed to import data into the FOLIO LSP. It provides a simple and efficient way to import data from various sources using FOLIO's REST APIs.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Import MARC records using FOLIO's Data Import system
|
|
10
|
+
- Import User records using FOLIO's User APIs
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
To install the project using Poetry, follow these steps:
|
|
17
|
+
|
|
18
|
+
1. Clone the repository.
|
|
19
|
+
2. Navigate to the project directory: `$ cd /path/to/folio_data_import`.
|
|
20
|
+
3. Install Poetry if you haven't already: `$ pip install poetry`.
|
|
21
|
+
4. Install the project and its dependencies: `$ poetry install`.
|
|
22
|
+
6. Run the application using Poetry: `$ poetry run python -m folio_data_import --help`.
|
|
23
|
+
|
|
24
|
+
Make sure to activate the virtual environment created by Poetry before running the application.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
1. Prepare the data to be imported in the specified format.
|
|
29
|
+
2. Run the application and follow the prompts to import the data.
|
|
30
|
+
3. Monitor the import progress and handle any errors or conflicts that may arise.
|
|
31
|
+
|
|
32
|
+
### folio-user-import
|
|
33
|
+
When this package is installed via PyPI or using `poetry install` from this repository, it installs a convenience script in your `$PATH` called `folio-user-import`. To view all command line options for this script, run `folio-user-import -h`. In addition to supporting `mod-user-import`-style JSON objects, this script also allows you to manage service point assignments for users by specifying a `servicePointsUser` object in the JSON object, using service point codes in place of UUIDs in the `defaultServicePointId` and `servicePointIds` fields:
|
|
34
|
+
```
|
|
35
|
+
{
|
|
36
|
+
"username": "checkin-all",
|
|
37
|
+
"barcode": "1728439497039848103",
|
|
38
|
+
"active": true,
|
|
39
|
+
"type": "patron",
|
|
40
|
+
"patronGroup": "staff",
|
|
41
|
+
"departments": [],
|
|
42
|
+
"personal": {
|
|
43
|
+
"lastName": "Admin",
|
|
44
|
+
"firstName": "checkin-all",
|
|
45
|
+
"addresses": [
|
|
46
|
+
{
|
|
47
|
+
"countryId": "HU",
|
|
48
|
+
"addressLine1": "Andrássy Street 1.",
|
|
49
|
+
"addressLine2": "",
|
|
50
|
+
"city": "Budapest",
|
|
51
|
+
"region": "Pest",
|
|
52
|
+
"postalCode": "1061",
|
|
53
|
+
"addressTypeId": "Home",
|
|
54
|
+
"primaryAddress": true
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"preferredContactTypeId": "email"
|
|
58
|
+
},
|
|
59
|
+
"requestPreference": {
|
|
60
|
+
"holdShelf": true,
|
|
61
|
+
"delivery": false,
|
|
62
|
+
"fulfillment": "Hold Shelf"
|
|
63
|
+
}
|
|
64
|
+
"servicePointsUser": {
|
|
65
|
+
"defaultServicePointId": "cd1",
|
|
66
|
+
"servicePointsIds": [
|
|
67
|
+
"cd1",
|
|
68
|
+
"Online",
|
|
69
|
+
"000",
|
|
70
|
+
"cd2"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
#### Matching Existing Users
|
|
76
|
+
|
|
77
|
+
Unlike mod-user-import, this importer does not require `externalSystemId` as the match point for your objects. If the user objects have `id` values, that will be used, falling back to `externalSystemId`. However, you can also specify `username` or `barcode` as the match point if desired, using the `--user_match_key` argument.
|
|
78
|
+
|
|
79
|
+
#### Preferred Contact Type Mapping
|
|
80
|
+
|
|
81
|
+
Another point of departure from the behavior of `mod-user-import` is the handling of `preferredContactTypeId`. This importer will accept either the `"001", "002", "003"...` values stored by the FOLIO, or the human-friendly strings used by `mod-user-import` (`"mail", "email", "text", "phone", "mobile"`). It will also __*set a customizable default for all users that do not otherwise have a valid value specified*__ (using `--default_preferred_contact_type`), unless a (valid) value is already present in the user record being updated.
|
|
82
|
+
|
|
83
|
+
#### Field Protection (*experimental*)
|
|
84
|
+
|
|
85
|
+
This script offers a rudimentary field protection implementation using custom fields. To enable this functionality, create a text custom field that has the field name `protectedFields`. In this field, you ca specify a comma-separated list of User schema field names, using dot-notation for nested fields. This protection should support all standard fields except addresses within `personal.addresses`. If you include `personal.addresses` in a user record, any existing addresses will be replaced by the new values.
|
|
86
|
+
|
|
87
|
+
##### Example
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
{
|
|
91
|
+
"protectedFields": "customFields.protectedFields,personal.preferredFirstName,barcode,personal.telephone,personal.addresses"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Would result in `preferredFirstName`, `barcode`, and `telephone` remaining unchanged, regardless of the contents of the incoming records.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
#### How to use:
|
|
99
|
+
1. Generate a JSON lines (one JSON object per line) file of FOLIO user objects in the style of [mod-user-import](https://github.com/folio-org/mod-user-import)
|
|
100
|
+
2. Run the script and specify the required arguments (and any desired optional arguments), including the path to your file of user objects
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## Contributing
|
|
104
|
+
|
|
105
|
+
Contributions are welcome! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
This project is licensed under the [MIT License](LICENSE).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "folio_data_import"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.7"
|
|
4
4
|
description = "A python module to interact with the data importing capabilities of the open-source FOLIO ILS"
|
|
5
5
|
authors = ["Brooks Travis <brooks.travis@gmail.com>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -14,7 +14,7 @@ folio-user-import = "folio_data_import.UserImport:sync_main"
|
|
|
14
14
|
|
|
15
15
|
[tool.poetry.dependencies]
|
|
16
16
|
python = "^3.9"
|
|
17
|
-
folioclient = "^0.
|
|
17
|
+
folioclient = "^0.61.0"
|
|
18
18
|
httpx = "^0.27.2"
|
|
19
19
|
pymarc = "^5.2.2"
|
|
20
20
|
pyhumps = "^3.8.0"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import asyncio
|
|
3
3
|
import glob
|
|
4
|
+
import importlib
|
|
4
5
|
import io
|
|
5
6
|
import os
|
|
6
7
|
import sys
|
|
@@ -73,6 +74,7 @@ class MARCImportJob:
|
|
|
73
74
|
import_profile_name: str,
|
|
74
75
|
batch_size=10,
|
|
75
76
|
batch_delay=0,
|
|
77
|
+
marc_record_preprocessor=None,
|
|
76
78
|
consolidate=False,
|
|
77
79
|
no_progress=False,
|
|
78
80
|
) -> None:
|
|
@@ -84,6 +86,7 @@ class MARCImportJob:
|
|
|
84
86
|
self.batch_size = batch_size
|
|
85
87
|
self.batch_delay = batch_delay
|
|
86
88
|
self.current_retry_timeout = None
|
|
89
|
+
self.marc_record_preprocessor = marc_record_preprocessor
|
|
87
90
|
|
|
88
91
|
async def do_work(self) -> None:
|
|
89
92
|
"""
|
|
@@ -334,6 +337,10 @@ class MARCImportJob:
|
|
|
334
337
|
await self.get_job_status()
|
|
335
338
|
sleep(0.25)
|
|
336
339
|
if record:
|
|
340
|
+
if self.marc_record_preprocessor:
|
|
341
|
+
record = await self.apply_marc_record_preprocessing(
|
|
342
|
+
record, self.marc_record_preprocessor
|
|
343
|
+
)
|
|
337
344
|
self.record_batch.append(record.as_marc())
|
|
338
345
|
counter += 1
|
|
339
346
|
else:
|
|
@@ -343,6 +350,39 @@ class MARCImportJob:
|
|
|
343
350
|
await self.create_batch_payload(counter, total_records, True),
|
|
344
351
|
)
|
|
345
352
|
|
|
353
|
+
@staticmethod
|
|
354
|
+
async def apply_marc_record_preprocessing(record: pymarc.Record, func_or_path) -> pymarc.Record:
|
|
355
|
+
"""
|
|
356
|
+
Apply preprocessing to the MARC record before sending it to FOLIO.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
record (pymarc.Record): The MARC record to preprocess.
|
|
360
|
+
func_or_path (Union[Callable, str]): The preprocessing function or its import path.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
pymarc.Record: The preprocessed MARC record.
|
|
364
|
+
"""
|
|
365
|
+
if isinstance(func_or_path, str):
|
|
366
|
+
try:
|
|
367
|
+
path_parts = func_or_path.rsplit('.')
|
|
368
|
+
module_path, func_name = ".".join(path_parts[:-1]), path_parts[-1]
|
|
369
|
+
module = importlib.import_module(module_path)
|
|
370
|
+
func = getattr(module, func_name)
|
|
371
|
+
except (ImportError, AttributeError) as e:
|
|
372
|
+
print(f"Error importing preprocessing function {func_or_path}: {e}. Skipping preprocessing.")
|
|
373
|
+
return record
|
|
374
|
+
elif callable(func_or_path):
|
|
375
|
+
func = func_or_path
|
|
376
|
+
else:
|
|
377
|
+
print(f"Invalid preprocessing function: {func_or_path}. Skipping preprocessing.")
|
|
378
|
+
return record
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
return func(record)
|
|
382
|
+
except Exception as e:
|
|
383
|
+
print(f"Error applying preprocessing function: {e}. Skipping preprocessing.")
|
|
384
|
+
return record
|
|
385
|
+
|
|
346
386
|
async def create_batch_payload(self, counter, total_records, is_last) -> dict:
|
|
347
387
|
"""
|
|
348
388
|
Create a batch payload for data import.
|
|
@@ -508,6 +548,15 @@ async def main() -> None:
|
|
|
508
548
|
help="The number of seconds to wait between record batches.",
|
|
509
549
|
default=0.0,
|
|
510
550
|
)
|
|
551
|
+
parser.add_argument(
|
|
552
|
+
"--preprocessor",
|
|
553
|
+
type=str,
|
|
554
|
+
help=(
|
|
555
|
+
"The path to a Python module containing a preprocessing function "
|
|
556
|
+
"to apply to each MARC record before sending to FOLIO."
|
|
557
|
+
),
|
|
558
|
+
default=None,
|
|
559
|
+
)
|
|
511
560
|
parser.add_argument(
|
|
512
561
|
"--consolidate",
|
|
513
562
|
action="store_true",
|
|
@@ -570,6 +619,7 @@ async def main() -> None:
|
|
|
570
619
|
args.import_profile_name,
|
|
571
620
|
batch_size=args.batch_size,
|
|
572
621
|
batch_delay=args.batch_delay,
|
|
622
|
+
marc_record_preprocessor=args.preprocessor,
|
|
573
623
|
consolidate=bool(args.consolidate),
|
|
574
624
|
no_progress=bool(args.no_progress),
|
|
575
625
|
).do_work()
|
|
@@ -21,6 +21,14 @@ except AttributeError:
|
|
|
21
21
|
|
|
22
22
|
utc = zoneinfo.ZoneInfo("UTC")
|
|
23
23
|
|
|
24
|
+
# Mapping of preferred contact type IDs to their corresponding values
|
|
25
|
+
PREFERRED_CONTACT_TYPES_MAP = {
|
|
26
|
+
"001": "mail",
|
|
27
|
+
"002": "email",
|
|
28
|
+
"003": "text",
|
|
29
|
+
"004": "phone",
|
|
30
|
+
"005": "mobile",
|
|
31
|
+
}
|
|
24
32
|
|
|
25
33
|
class UserImporter: # noqa: R0902
|
|
26
34
|
"""
|
|
@@ -33,14 +41,15 @@ class UserImporter: # noqa: R0902
|
|
|
33
41
|
self,
|
|
34
42
|
folio_client: folioclient.FolioClient,
|
|
35
43
|
library_name: str,
|
|
36
|
-
user_file_path: Path,
|
|
37
44
|
batch_size: int,
|
|
38
45
|
limit_simultaneous_requests: asyncio.Semaphore,
|
|
39
46
|
logfile: AsyncTextIOWrapper,
|
|
40
47
|
errorfile: AsyncTextIOWrapper,
|
|
41
48
|
http_client: httpx.AsyncClient,
|
|
49
|
+
user_file_path: Path = None,
|
|
42
50
|
user_match_key: str = "externalSystemId",
|
|
43
51
|
only_update_present_fields: bool = False,
|
|
52
|
+
default_preferred_contact_type: str = "002",
|
|
44
53
|
) -> None:
|
|
45
54
|
self.limit_simultaneous_requests = limit_simultaneous_requests
|
|
46
55
|
self.batch_size = batch_size
|
|
@@ -56,10 +65,14 @@ class UserImporter: # noqa: R0902
|
|
|
56
65
|
self.department_map: dict = self.build_ref_data_id_map(
|
|
57
66
|
self.folio_client, "/departments", "departments", "name"
|
|
58
67
|
)
|
|
68
|
+
self.service_point_map: dict = self.build_ref_data_id_map(
|
|
69
|
+
self.folio_client, "/service-points", "servicepoints", "code"
|
|
70
|
+
)
|
|
59
71
|
self.logfile: AsyncTextIOWrapper = logfile
|
|
60
72
|
self.errorfile: AsyncTextIOWrapper = errorfile
|
|
61
73
|
self.http_client: httpx.AsyncClient = http_client
|
|
62
74
|
self.only_update_present_fields: bool = only_update_present_fields
|
|
75
|
+
self.default_preferred_contact_type: str = default_preferred_contact_type
|
|
63
76
|
self.match_key = user_match_key
|
|
64
77
|
self.lock: asyncio.Lock = asyncio.Lock()
|
|
65
78
|
self.logs: dict = {"created": 0, "updated": 0, "failed": 0}
|
|
@@ -87,7 +100,11 @@ class UserImporter: # noqa: R0902
|
|
|
87
100
|
|
|
88
101
|
This method triggers the process of importing users by calling the `process_file` method.
|
|
89
102
|
"""
|
|
90
|
-
|
|
103
|
+
if self.user_file_path:
|
|
104
|
+
with open(self.user_file_path, "r", encoding="utf-8") as openfile:
|
|
105
|
+
await self.process_file(openfile)
|
|
106
|
+
else:
|
|
107
|
+
raise FileNotFoundError("No user objects file provided")
|
|
91
108
|
|
|
92
109
|
async def get_existing_user(self, user_obj) -> dict:
|
|
93
110
|
"""
|
|
@@ -255,13 +272,14 @@ class UserImporter: # noqa: R0902
|
|
|
255
272
|
if mapped_departments:
|
|
256
273
|
user_obj["departments"] = mapped_departments
|
|
257
274
|
|
|
258
|
-
async def update_existing_user(self, user_obj, existing_user) -> Tuple[dict, dict]:
|
|
275
|
+
async def update_existing_user(self, user_obj, existing_user, protected_fields) -> Tuple[dict, dict]:
|
|
259
276
|
"""
|
|
260
277
|
Updates an existing user with the provided user object.
|
|
261
278
|
|
|
262
279
|
Args:
|
|
263
280
|
user_obj (dict): The user object containing the updated user information.
|
|
264
281
|
existing_user (dict): The existing user object to be updated.
|
|
282
|
+
protected_fields (dict): A dictionary containing the protected fields and their values.
|
|
265
283
|
|
|
266
284
|
Returns:
|
|
267
285
|
tuple: A tuple containing the updated existing user object and the API response.
|
|
@@ -270,6 +288,8 @@ class UserImporter: # noqa: R0902
|
|
|
270
288
|
None
|
|
271
289
|
|
|
272
290
|
"""
|
|
291
|
+
await self.set_preferred_contact_type(user_obj, existing_user)
|
|
292
|
+
preferred_contact_type = {"preferredContactTypeId": existing_user.get("personal", {}).pop("preferredContactTypeId")}
|
|
273
293
|
if self.only_update_present_fields:
|
|
274
294
|
new_personal = user_obj.pop("personal", {})
|
|
275
295
|
existing_personal = existing_user.pop("personal", {})
|
|
@@ -290,6 +310,18 @@ class UserImporter: # noqa: R0902
|
|
|
290
310
|
existing_user["personal"] = existing_personal
|
|
291
311
|
else:
|
|
292
312
|
existing_user.update(user_obj)
|
|
313
|
+
if "personal" in existing_user:
|
|
314
|
+
existing_user["personal"].update(preferred_contact_type)
|
|
315
|
+
else:
|
|
316
|
+
existing_user["personal"] = preferred_contact_type
|
|
317
|
+
for key, value in protected_fields.items():
|
|
318
|
+
if type(value) is dict:
|
|
319
|
+
try:
|
|
320
|
+
existing_user[key].update(value)
|
|
321
|
+
except KeyError:
|
|
322
|
+
existing_user[key] = value
|
|
323
|
+
else:
|
|
324
|
+
existing_user[key] = value
|
|
293
325
|
create_update_user = await self.http_client.put(
|
|
294
326
|
self.folio_client.okapi_url + f"/users/{existing_user['id']}",
|
|
295
327
|
headers=self.folio_client.okapi_headers,
|
|
@@ -320,7 +352,41 @@ class UserImporter: # noqa: R0902
|
|
|
320
352
|
self.logs["created"] += 1
|
|
321
353
|
return response.json()
|
|
322
354
|
|
|
323
|
-
async def
|
|
355
|
+
async def set_preferred_contact_type(self, user_obj, existing_user) -> None:
|
|
356
|
+
"""
|
|
357
|
+
Sets the preferred contact type for a user object. If the provided preferred contact type
|
|
358
|
+
is not valid, the default preferred contact type is used, unless the previously existing
|
|
359
|
+
user object has a valid preferred contact type set. In that case, the existing preferred
|
|
360
|
+
contact type is used.
|
|
361
|
+
"""
|
|
362
|
+
if "personal" in user_obj and "preferredContactTypeId" in user_obj["personal"]:
|
|
363
|
+
current_pref_contact = user_obj["personal"].get(
|
|
364
|
+
"preferredContactTypeId", ""
|
|
365
|
+
)
|
|
366
|
+
if mapped_contact_type := dict([(v, k) for k, v in PREFERRED_CONTACT_TYPES_MAP.items()]).get(
|
|
367
|
+
current_pref_contact,
|
|
368
|
+
"",
|
|
369
|
+
):
|
|
370
|
+
existing_user["personal"]["preferredContactTypeId"] = mapped_contact_type
|
|
371
|
+
else:
|
|
372
|
+
existing_user["personal"]["preferredContactTypeId"] = current_pref_contact if current_pref_contact in PREFERRED_CONTACT_TYPES_MAP else self.default_preferred_contact_type
|
|
373
|
+
else:
|
|
374
|
+
print(
|
|
375
|
+
f"Preferred contact type not provided or is not a valid option: {PREFERRED_CONTACT_TYPES_MAP}\n"
|
|
376
|
+
f"Setting preferred contact type to {self.default_preferred_contact_type} or using existing value"
|
|
377
|
+
)
|
|
378
|
+
await self.logfile.write(
|
|
379
|
+
f"Preferred contact type not provided or is not a valid option: {PREFERRED_CONTACT_TYPES_MAP}\n"
|
|
380
|
+
f"Setting preferred contact type to {self.default_preferred_contact_type} or using existing value\n"
|
|
381
|
+
)
|
|
382
|
+
mapped_contact_type = existing_user.get("personal", {}).get(
|
|
383
|
+
"preferredContactTypeId", ""
|
|
384
|
+
) or self.default_preferred_contact_type
|
|
385
|
+
if "personal" not in existing_user:
|
|
386
|
+
existing_user["personal"] = {}
|
|
387
|
+
existing_user["personal"]["preferredContactTypeId"] = mapped_contact_type or self.default_preferred_contact_type
|
|
388
|
+
|
|
389
|
+
async def create_or_update_user(self, user_obj, existing_user, protected_fields, line_number) -> dict:
|
|
324
390
|
"""
|
|
325
391
|
Creates or updates a user based on the given user object and existing user.
|
|
326
392
|
|
|
@@ -334,7 +400,7 @@ class UserImporter: # noqa: R0902
|
|
|
334
400
|
"""
|
|
335
401
|
if existing_user:
|
|
336
402
|
existing_user, update_user = await self.update_existing_user(
|
|
337
|
-
user_obj, existing_user
|
|
403
|
+
user_obj, existing_user, protected_fields
|
|
338
404
|
)
|
|
339
405
|
try:
|
|
340
406
|
update_user.raise_for_status()
|
|
@@ -375,7 +441,7 @@ class UserImporter: # noqa: R0902
|
|
|
375
441
|
|
|
376
442
|
async def process_user_obj(self, user: str) -> dict:
|
|
377
443
|
"""
|
|
378
|
-
Process a user object.
|
|
444
|
+
Process a user object. If not type is found in the source object, type is set to "patron".
|
|
379
445
|
|
|
380
446
|
Args:
|
|
381
447
|
user (str): The user data to be processed, as a json string.
|
|
@@ -386,17 +452,34 @@ class UserImporter: # noqa: R0902
|
|
|
386
452
|
"""
|
|
387
453
|
user_obj = json.loads(user)
|
|
388
454
|
user_obj["type"] = user_obj.get("type", "patron")
|
|
389
|
-
if "personal" in user_obj:
|
|
390
|
-
current_pref_contact = user_obj["personal"].get(
|
|
391
|
-
"preferredContactTypeId", ""
|
|
392
|
-
)
|
|
393
|
-
user_obj["personal"]["preferredContactTypeId"] = (
|
|
394
|
-
current_pref_contact
|
|
395
|
-
if current_pref_contact in ["001", "002", "003"]
|
|
396
|
-
else "002"
|
|
397
|
-
)
|
|
398
455
|
return user_obj
|
|
399
456
|
|
|
457
|
+
async def get_protected_fields(self, existing_user) -> dict:
|
|
458
|
+
"""
|
|
459
|
+
Retrieves the protected fields from the existing user object.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
existing_user (dict): The existing user object.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
dict: A dictionary containing the protected fields and their values.
|
|
466
|
+
"""
|
|
467
|
+
protected_fields = {}
|
|
468
|
+
protected_fields_list = existing_user.get("customFields", {}).get("protectedFields", "").split(",")
|
|
469
|
+
for field in protected_fields_list:
|
|
470
|
+
if len(field.split(".")) > 1:
|
|
471
|
+
field, subfield = field.split(".")
|
|
472
|
+
if field not in protected_fields:
|
|
473
|
+
protected_fields[field] = {}
|
|
474
|
+
protected_fields[field][subfield] = existing_user.get(field, {}).pop(subfield, None)
|
|
475
|
+
if protected_fields[field][subfield] is None:
|
|
476
|
+
protected_fields[field].pop(subfield)
|
|
477
|
+
else:
|
|
478
|
+
protected_fields[field] = existing_user.pop(field, None)
|
|
479
|
+
if protected_fields[field] is None:
|
|
480
|
+
protected_fields.pop(field)
|
|
481
|
+
return protected_fields
|
|
482
|
+
|
|
400
483
|
async def process_existing_user(self, user_obj) -> Tuple[dict, dict, dict, dict]:
|
|
401
484
|
"""
|
|
402
485
|
Process an existing user.
|
|
@@ -410,14 +493,19 @@ class UserImporter: # noqa: R0902
|
|
|
410
493
|
and the existing PU object (existing_pu).
|
|
411
494
|
"""
|
|
412
495
|
rp_obj = user_obj.pop("requestPreference", {})
|
|
496
|
+
spu_obj = user_obj.pop("servicePointsUser")
|
|
413
497
|
existing_user = await self.get_existing_user(user_obj)
|
|
414
498
|
if existing_user:
|
|
415
499
|
existing_rp = await self.get_existing_rp(user_obj, existing_user)
|
|
416
500
|
existing_pu = await self.get_existing_pu(user_obj, existing_user)
|
|
501
|
+
existing_spu = await self.get_existing_spu(existing_user)
|
|
502
|
+
protected_fields = await self.get_protected_fields(existing_user)
|
|
417
503
|
else:
|
|
418
504
|
existing_rp = {}
|
|
419
505
|
existing_pu = {}
|
|
420
|
-
|
|
506
|
+
existing_spu = {}
|
|
507
|
+
protected_fields = {}
|
|
508
|
+
return rp_obj, spu_obj, existing_user, protected_fields, existing_rp, existing_pu, existing_spu
|
|
421
509
|
|
|
422
510
|
async def create_or_update_rp(self, rp_obj, existing_rp, new_user_obj):
|
|
423
511
|
"""
|
|
@@ -528,14 +616,14 @@ class UserImporter: # noqa: R0902
|
|
|
528
616
|
"""
|
|
529
617
|
async with self.limit_simultaneous_requests:
|
|
530
618
|
user_obj = await self.process_user_obj(user)
|
|
531
|
-
rp_obj, existing_user, existing_rp, existing_pu = (
|
|
619
|
+
rp_obj, spu_obj, existing_user, protected_fields, existing_rp, existing_pu, existing_spu = (
|
|
532
620
|
await self.process_existing_user(user_obj)
|
|
533
621
|
)
|
|
534
622
|
await self.map_address_types(user_obj, line_number)
|
|
535
623
|
await self.map_patron_groups(user_obj, line_number)
|
|
536
624
|
await self.map_departments(user_obj, line_number)
|
|
537
625
|
new_user_obj = await self.create_or_update_user(
|
|
538
|
-
user_obj, existing_user, line_number
|
|
626
|
+
user_obj, existing_user, protected_fields, line_number
|
|
539
627
|
)
|
|
540
628
|
if new_user_obj:
|
|
541
629
|
try:
|
|
@@ -572,42 +660,162 @@ class UserImporter: # noqa: R0902
|
|
|
572
660
|
)
|
|
573
661
|
print(pu_error_message)
|
|
574
662
|
await self.logfile.write(pu_error_message + "\n")
|
|
663
|
+
await self.handle_service_points_user(spu_obj, existing_spu, new_user_obj)
|
|
664
|
+
|
|
665
|
+
async def map_service_points(self, spu_obj, existing_user):
|
|
666
|
+
"""
|
|
667
|
+
Maps the service points of a user object using the provided service point map.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
spu_obj (dict): The service-points-user object to update.
|
|
671
|
+
existing_user (dict): The existing user object associated with the spu_obj.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
None
|
|
675
|
+
"""
|
|
676
|
+
if "servicePointsIds" in spu_obj:
|
|
677
|
+
mapped_service_points = []
|
|
678
|
+
for sp in spu_obj.pop("servicePointsIds", []):
|
|
679
|
+
try:
|
|
680
|
+
mapped_service_points.append(self.service_point_map[sp])
|
|
681
|
+
except KeyError:
|
|
682
|
+
print(
|
|
683
|
+
f'Service point "{sp}" not found, excluding service point from user: '
|
|
684
|
+
f'{self.service_point_map}'
|
|
685
|
+
)
|
|
686
|
+
if mapped_service_points:
|
|
687
|
+
spu_obj["servicePointsIds"] = mapped_service_points
|
|
688
|
+
if "defaultServicePointId" in spu_obj:
|
|
689
|
+
sp_code = spu_obj.pop('defaultServicePointId', '')
|
|
690
|
+
try:
|
|
691
|
+
mapped_sp_id = self.service_point_map[sp_code]
|
|
692
|
+
if mapped_sp_id not in spu_obj.get('servicePointsIds', []):
|
|
693
|
+
print(
|
|
694
|
+
f'Default service point "{sp_code}" not found in assigned service points, '
|
|
695
|
+
'excluding default service point from user'
|
|
696
|
+
)
|
|
697
|
+
else:
|
|
698
|
+
spu_obj['defaultServicePointId'] = mapped_sp_id
|
|
699
|
+
except KeyError:
|
|
700
|
+
print(
|
|
701
|
+
f'Default service point "{sp_code}" not found, excluding default service '
|
|
702
|
+
f'point from user: {existing_user["id"]}'
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
async def handle_service_points_user(self, spu_obj, existing_spu, existing_user):
|
|
706
|
+
"""
|
|
707
|
+
Handles processing a service-points-user object for a user.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
spu_obj (dict): The service-points-user object to process.
|
|
711
|
+
existing_spu (dict): The existing service-points-user object, if it exists.
|
|
712
|
+
existing_user (dict): The existing user object associated with the spu_obj.
|
|
713
|
+
"""
|
|
714
|
+
if spu_obj is not None:
|
|
715
|
+
await self.map_service_points(spu_obj, existing_user)
|
|
716
|
+
if existing_spu:
|
|
717
|
+
await self.update_existing_spu(spu_obj, existing_spu)
|
|
718
|
+
else:
|
|
719
|
+
await self.create_new_spu(spu_obj, existing_user)
|
|
575
720
|
|
|
576
|
-
async def
|
|
721
|
+
async def get_existing_spu(self, existing_user):
|
|
722
|
+
"""
|
|
723
|
+
Retrieves the existing service-points-user object for a given user.
|
|
724
|
+
|
|
725
|
+
Args:
|
|
726
|
+
existing_user (dict): The existing user object.
|
|
727
|
+
|
|
728
|
+
Returns:
|
|
729
|
+
dict: The existing service-points-user object.
|
|
730
|
+
"""
|
|
731
|
+
try:
|
|
732
|
+
existing_spu = await self.http_client.get(
|
|
733
|
+
self.folio_client.okapi_url + "/service-points-users",
|
|
734
|
+
headers=self.folio_client.okapi_headers,
|
|
735
|
+
params={"query": f"userId=={existing_user['id']}"},
|
|
736
|
+
)
|
|
737
|
+
existing_spu.raise_for_status()
|
|
738
|
+
existing_spu = existing_spu.json().get("servicePointsUsers", [])
|
|
739
|
+
existing_spu = existing_spu[0] if existing_spu else {}
|
|
740
|
+
except httpx.HTTPError:
|
|
741
|
+
existing_spu = {}
|
|
742
|
+
return existing_spu
|
|
743
|
+
|
|
744
|
+
async def create_new_spu(self, spu_obj, existing_user):
|
|
745
|
+
"""
|
|
746
|
+
Creates a new service-points-user object for a given user.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
spu_obj (dict): The service-points-user object to create.
|
|
750
|
+
existing_user (dict): The existing user object.
|
|
751
|
+
|
|
752
|
+
Returns:
|
|
753
|
+
None
|
|
754
|
+
"""
|
|
755
|
+
spu_obj["userId"] = existing_user["id"]
|
|
756
|
+
response = await self.http_client.post(
|
|
757
|
+
self.folio_client.okapi_url + "/service-points-users",
|
|
758
|
+
headers=self.folio_client.okapi_headers,
|
|
759
|
+
json=spu_obj,
|
|
760
|
+
)
|
|
761
|
+
response.raise_for_status()
|
|
762
|
+
|
|
763
|
+
async def update_existing_spu(self, spu_obj, existing_spu):
|
|
764
|
+
"""
|
|
765
|
+
Updates an existing service-points-user object with the provided service-points-user object.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
spu_obj (dict): The service-points-user object containing the updated values.
|
|
769
|
+
existing_spu (dict): The existing service-points-user object to be updated.
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
None
|
|
773
|
+
"""
|
|
774
|
+
existing_spu.update(spu_obj)
|
|
775
|
+
response = await self.http_client.put(
|
|
776
|
+
self.folio_client.okapi_url + f"/service-points-users/{existing_spu['id']}",
|
|
777
|
+
headers=self.folio_client.okapi_headers,
|
|
778
|
+
json=existing_spu,
|
|
779
|
+
)
|
|
780
|
+
response.raise_for_status()
|
|
781
|
+
|
|
782
|
+
async def process_file(self, openfile) -> None:
|
|
577
783
|
"""
|
|
578
784
|
Process the user object file.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
openfile: The file or file-like object to process.
|
|
579
788
|
"""
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
if len(tasks) == self.batch_size:
|
|
585
|
-
start = time.time()
|
|
586
|
-
await asyncio.gather(*tasks)
|
|
587
|
-
duration = time.time() - start
|
|
588
|
-
async with self.lock:
|
|
589
|
-
message = (
|
|
590
|
-
f"{dt.now().isoformat(sep=' ', timespec='milliseconds')}: "
|
|
591
|
-
f"Batch of {self.batch_size} users processed in {duration:.2f} "
|
|
592
|
-
f"seconds. - Users created: {self.logs['created']} - Users updated: "
|
|
593
|
-
f"{self.logs['updated']} - Users failed: {self.logs['failed']}"
|
|
594
|
-
)
|
|
595
|
-
print(message)
|
|
596
|
-
await self.logfile.write(message + "\n")
|
|
597
|
-
tasks = []
|
|
598
|
-
if tasks:
|
|
789
|
+
tasks = []
|
|
790
|
+
for line_number, user in enumerate(openfile):
|
|
791
|
+
tasks.append(self.process_line(user, line_number))
|
|
792
|
+
if len(tasks) == self.batch_size:
|
|
599
793
|
start = time.time()
|
|
600
794
|
await asyncio.gather(*tasks)
|
|
601
795
|
duration = time.time() - start
|
|
602
796
|
async with self.lock:
|
|
603
797
|
message = (
|
|
604
798
|
f"{dt.now().isoformat(sep=' ', timespec='milliseconds')}: "
|
|
605
|
-
f"Batch of {self.batch_size} users processed in {duration:.2f}
|
|
606
|
-
f"Users created: {self.logs['created']} - Users updated: "
|
|
799
|
+
f"Batch of {self.batch_size} users processed in {duration:.2f} "
|
|
800
|
+
f"seconds. - Users created: {self.logs['created']} - Users updated: "
|
|
607
801
|
f"{self.logs['updated']} - Users failed: {self.logs['failed']}"
|
|
608
802
|
)
|
|
609
803
|
print(message)
|
|
610
804
|
await self.logfile.write(message + "\n")
|
|
805
|
+
tasks = []
|
|
806
|
+
if tasks:
|
|
807
|
+
start = time.time()
|
|
808
|
+
await asyncio.gather(*tasks)
|
|
809
|
+
duration = time.time() - start
|
|
810
|
+
async with self.lock:
|
|
811
|
+
message = (
|
|
812
|
+
f"{dt.now().isoformat(sep=' ', timespec='milliseconds')}: "
|
|
813
|
+
f"Batch of {len(tasks)} users processed in {duration:.2f} seconds. - "
|
|
814
|
+
f"Users created: {self.logs['created']} - Users updated: "
|
|
815
|
+
f"{self.logs['updated']} - Users failed: {self.logs['failed']}"
|
|
816
|
+
)
|
|
817
|
+
print(message)
|
|
818
|
+
await self.logfile.write(message + "\n")
|
|
611
819
|
|
|
612
820
|
|
|
613
821
|
async def main() -> None:
|
|
@@ -626,6 +834,10 @@ async def main() -> None:
|
|
|
626
834
|
--batch_size (int): How many records to process before logging statistics. Default 250.
|
|
627
835
|
--folio_password (str): The FOLIO password.
|
|
628
836
|
--user_match_key (str): The key to use to match users. Default "externalSystemId".
|
|
837
|
+
--report_file_base_path (str): The base path for the log and error files. Default "./".
|
|
838
|
+
--update_only_present_fields (bool): Only update fields that are present in the new user object.
|
|
839
|
+
--default_preferred_contact_type (str): The default preferred contact type to use if the provided \
|
|
840
|
+
value is not valid or not present. Default "002".
|
|
629
841
|
|
|
630
842
|
Raises:
|
|
631
843
|
Exception: If an unknown error occurs during the import process.
|
|
@@ -663,11 +875,26 @@ async def main() -> None:
|
|
|
663
875
|
choices=["externalSystemId", "barcode", "username"],
|
|
664
876
|
default="externalSystemId",
|
|
665
877
|
)
|
|
878
|
+
parser.add_argument(
|
|
879
|
+
"--report_file_base_path",
|
|
880
|
+
help="The base path for the log and error files",
|
|
881
|
+
default="./",
|
|
882
|
+
)
|
|
666
883
|
parser.add_argument(
|
|
667
884
|
"--update_only_present_fields",
|
|
668
885
|
help="Only update fields that are present in the user object",
|
|
669
886
|
action="store_true",
|
|
670
887
|
)
|
|
888
|
+
parser.add_argument(
|
|
889
|
+
"--default_preferred_contact_type",
|
|
890
|
+
help=(
|
|
891
|
+
"The default preferred contact type to use if the provided value is not present or not valid. "
|
|
892
|
+
"Note: '002' is the default, and will be used if the provided value is not valid or not present, "
|
|
893
|
+
"unless the existing user object being updated has a valid preferred contact type set."
|
|
894
|
+
),
|
|
895
|
+
choices=list(PREFERRED_CONTACT_TYPES_MAP.keys()) + list(PREFERRED_CONTACT_TYPES_MAP.values()),
|
|
896
|
+
default="002",
|
|
897
|
+
)
|
|
671
898
|
args = parser.parse_args()
|
|
672
899
|
|
|
673
900
|
library_name = args.library_name
|
|
@@ -692,13 +919,13 @@ async def main() -> None:
|
|
|
692
919
|
folio_client.okapi_headers["x-okapi-tenant"] = args.member_tenant_id
|
|
693
920
|
|
|
694
921
|
user_file_path = Path(args.user_file_path)
|
|
922
|
+
report_file_base_path = Path(args.report_file_base_path)
|
|
695
923
|
log_file_path = (
|
|
696
|
-
|
|
697
|
-
/ "reports"
|
|
924
|
+
report_file_base_path
|
|
698
925
|
/ f"log_user_import_{dt.now(utc).strftime('%Y%m%d_%H%M%S')}.log"
|
|
699
926
|
)
|
|
700
927
|
error_file_path = (
|
|
701
|
-
|
|
928
|
+
report_file_base_path
|
|
702
929
|
/ f"failed_user_import_{dt.now(utc).strftime('%Y%m%d_%H%M%S')}.txt"
|
|
703
930
|
)
|
|
704
931
|
async with aiofiles.open(
|
|
@@ -711,14 +938,15 @@ async def main() -> None:
|
|
|
711
938
|
importer = UserImporter(
|
|
712
939
|
folio_client,
|
|
713
940
|
library_name,
|
|
714
|
-
user_file_path,
|
|
715
941
|
batch_size,
|
|
716
942
|
limit_async_requests,
|
|
717
943
|
logfile,
|
|
718
944
|
errorfile,
|
|
719
945
|
http_client,
|
|
946
|
+
user_file_path,
|
|
720
947
|
args.user_match_key,
|
|
721
948
|
args.update_only_present_fields,
|
|
949
|
+
args.default_preferred_contact_type,
|
|
722
950
|
)
|
|
723
951
|
await importer.do_import()
|
|
724
952
|
except Exception as ee:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._preprocessors import prepend_ppn_prefix_001, strip_999_ff_fields
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import pymarc
|
|
2
|
+
|
|
3
|
+
def prepend_ppn_prefix_001(record: pymarc.Record) -> pymarc.Record:
|
|
4
|
+
"""
|
|
5
|
+
Prepend the PPN prefix to the record's 001 field. Useful when
|
|
6
|
+
importing records from the ABES SUDOC catalog
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
record (pymarc.Record): The MARC record to preprocess.
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
pymarc.Record: The preprocessed MARC record.
|
|
13
|
+
"""
|
|
14
|
+
record['001'].data = '(PPN)' + record['001'].data
|
|
15
|
+
return record
|
|
16
|
+
|
|
17
|
+
def strip_999_ff_fields(record: pymarc.Record) -> pymarc.Record:
|
|
18
|
+
"""
|
|
19
|
+
Strip all 999 fields with ff indicators from the record.
|
|
20
|
+
Useful when importing records exported from another FOLIO system
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
record (pymarc.Record): The MARC record to preprocess.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
pymarc.Record: The preprocessed MARC record.
|
|
27
|
+
"""
|
|
28
|
+
for field in record.get_fields('999'):
|
|
29
|
+
if field.indicators == pymarc.Indicators(*['f', 'f']):
|
|
30
|
+
record.remove_field(field)
|
|
31
|
+
return record
|
folio_data_import-0.2.5/PKG-INFO
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: folio_data_import
|
|
3
|
-
Version: 0.2.5
|
|
4
|
-
Summary: A python module to interact with the data importing capabilities of the open-source FOLIO ILS
|
|
5
|
-
License: MIT
|
|
6
|
-
Author: Brooks Travis
|
|
7
|
-
Author-email: brooks.travis@gmail.com
|
|
8
|
-
Requires-Python: >=3.9,<4.0
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
|
16
|
-
Requires-Dist: flake8-bandit (>=4.1.1,<5.0.0)
|
|
17
|
-
Requires-Dist: flake8-black (>=0.3.6,<0.4.0)
|
|
18
|
-
Requires-Dist: flake8-bugbear (>=24.8.19,<25.0.0)
|
|
19
|
-
Requires-Dist: flake8-docstrings (>=1.7.0,<2.0.0)
|
|
20
|
-
Requires-Dist: flake8-isort (>=6.1.1,<7.0.0)
|
|
21
|
-
Requires-Dist: folioclient (>=0.60.5,<0.61.0)
|
|
22
|
-
Requires-Dist: httpx (>=0.27.2,<0.28.0)
|
|
23
|
-
Requires-Dist: inquirer (>=3.4.0,<4.0.0)
|
|
24
|
-
Requires-Dist: pyhumps (>=3.8.0,<4.0.0)
|
|
25
|
-
Requires-Dist: pymarc (>=5.2.2,<6.0.0)
|
|
26
|
-
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
27
|
-
Requires-Dist: tqdm (>=4.66.5,<5.0.0)
|
|
28
|
-
Description-Content-Type: text/markdown
|
|
29
|
-
|
|
30
|
-
# folio_data_import
|
|
31
|
-
|
|
32
|
-
## Description
|
|
33
|
-
|
|
34
|
-
This project is designed to import data into the FOLIO LSP. It provides a simple and efficient way to import data from various sources using FOLIO's REST APIs.
|
|
35
|
-
|
|
36
|
-
## Features
|
|
37
|
-
|
|
38
|
-
- Import MARC records using FOLIO's Data Import system
|
|
39
|
-
- Import User records using FOLIO's User APIs
|
|
40
|
-
|
|
41
|
-
## Installation
|
|
42
|
-
|
|
43
|
-
## Installation
|
|
44
|
-
|
|
45
|
-
To install the project using Poetry, follow these steps:
|
|
46
|
-
|
|
47
|
-
1. Clone the repository.
|
|
48
|
-
2. Navigate to the project directory: `$ cd /path/to/folio_data_import`.
|
|
49
|
-
3. Install Poetry if you haven't already: `$ pip install poetry`.
|
|
50
|
-
4. Install the project dependencies: `$ poetry install`.
|
|
51
|
-
6. Run the application using Poetry: `$ poetry run python -m folio_data_import --help`.
|
|
52
|
-
|
|
53
|
-
Make sure to activate the virtual environment created by Poetry before running the application.
|
|
54
|
-
|
|
55
|
-
## Usage
|
|
56
|
-
|
|
57
|
-
1. Prepare the data to be imported in the specified format.
|
|
58
|
-
2. Run the application and follow the prompts to import the data.
|
|
59
|
-
3. Monitor the import progress and handle any errors or conflicts that may arise.
|
|
60
|
-
|
|
61
|
-
## Contributing
|
|
62
|
-
|
|
63
|
-
Contributions are welcome! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request.
|
|
64
|
-
|
|
65
|
-
## License
|
|
66
|
-
|
|
67
|
-
This project is licensed under the [MIT License](LICENSE).
|
|
68
|
-
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# folio_data_import
|
|
2
|
-
|
|
3
|
-
## Description
|
|
4
|
-
|
|
5
|
-
This project is designed to import data into the FOLIO LSP. It provides a simple and efficient way to import data from various sources using FOLIO's REST APIs.
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- Import MARC records using FOLIO's Data Import system
|
|
10
|
-
- Import User records using FOLIO's User APIs
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
To install the project using Poetry, follow these steps:
|
|
17
|
-
|
|
18
|
-
1. Clone the repository.
|
|
19
|
-
2. Navigate to the project directory: `$ cd /path/to/folio_data_import`.
|
|
20
|
-
3. Install Poetry if you haven't already: `$ pip install poetry`.
|
|
21
|
-
4. Install the project dependencies: `$ poetry install`.
|
|
22
|
-
6. Run the application using Poetry: `$ poetry run python -m folio_data_import --help`.
|
|
23
|
-
|
|
24
|
-
Make sure to activate the virtual environment created by Poetry before running the application.
|
|
25
|
-
|
|
26
|
-
## Usage
|
|
27
|
-
|
|
28
|
-
1. Prepare the data to be imported in the specified format.
|
|
29
|
-
2. Run the application and follow the prompts to import the data.
|
|
30
|
-
3. Monitor the import progress and handle any errors or conflicts that may arise.
|
|
31
|
-
|
|
32
|
-
## Contributing
|
|
33
|
-
|
|
34
|
-
Contributions are welcome! If you have any ideas, suggestions, or bug reports, please open an issue or submit a pull request.
|
|
35
|
-
|
|
36
|
-
## License
|
|
37
|
-
|
|
38
|
-
This project is licensed under the [MIT License](LICENSE).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|