docuware-client 0.5.3__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.
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2022, Stefan Schönberger <mail@sniner.dev>
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,297 @@
1
+ Metadata-Version: 2.4
2
+ Name: docuware-client
3
+ Version: 0.5.3
4
+ Summary: DocuWare REST-API client
5
+ License: BSD-3-Clause
6
+ License-File: LICENSE
7
+ Author: Stefan Schönberger
8
+ Author-email: mail@sniner.dev
9
+ Requires-Python: >=3.9
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
20
+ Project-URL: Repository, https://github.com/sniner/docuware-client
21
+ Description-Content-Type: text/markdown
22
+
23
+ # docuware-client
24
+
25
+ This is a client library for the REST API of [DocuWare][1] DMS. Since
26
+ [DocuWare's documentation][2] regarding the REST API is very sparse (at the
27
+ time these lines were written), this client serves only a part of the API's
28
+ functionality.
29
+
30
+ Please keep in mind: **This software is not related to DocuWare.** It is a work
31
+ in progress, may yield unexpected results, and almost certainly contains bugs.
32
+
33
+ > ⚠️ Starting with version 0.5.0, OAuth2 authentication is the new default.
34
+ > Unless you explicitly request cookie authentication with
35
+ > `dw.login(..., cookie_auth=True)`, OAuth2 will be used. OAuth2 authentication
36
+ > has been available since DocuWare 7.10, and
37
+ > [cookie authentication will be discontinued](https://start.docuware.com/blog/product-news/docuware-sdk-discontinuation-of-cookie-authentication)
38
+ > with DocuWare 7.11.
39
+
40
+
41
+ ## Usage
42
+
43
+ First you have to log in and create a persistent session:
44
+
45
+ ```python
46
+ import json
47
+ import pathlib
48
+ import docuware
49
+
50
+ dw = docuware.Client("http://localhost")
51
+ session = dw.login("username", "password", "organization")
52
+ with open(".session", "w") as f:
53
+ json.dump(session, f)
54
+ ```
55
+
56
+ From then on you have to reuse the session, otherwise you will be locked out of
57
+ the DocuWare service for a period of time (10 minutes or even longer). As the
58
+ session cookie may change on subsequent logins, update the session file on
59
+ every login.
60
+
61
+ ```python
62
+ session_file = pathlib.Path(".session")
63
+ if session_file.exists():
64
+ with open(session_file) as f:
65
+ session = json.load(f)
66
+ else:
67
+ session = None
68
+ dw = docuware.Client("http://localhost")
69
+ session = dw.login("username", "password", "organization", saved_session=session)
70
+ with open(session_file, "w") as f:
71
+ json.dump(session, f)
72
+ ```
73
+
74
+ Iterate over the organizations and file cabinets:
75
+
76
+ ```python
77
+ for org in dw.organizations:
78
+ print(org)
79
+ for fc in org.file_cabinets:
80
+ print(" ", fc)
81
+ ```
82
+
83
+ If you already know the ID or name of the objects, you can also access them
84
+ directly.
85
+
86
+ ```python
87
+ org = dw.organization("1")
88
+ fc = org.file_cabinet("Archive")
89
+ ```
90
+
91
+ Now some examples of how to search for documents. First you need a search
92
+ dialog:
93
+
94
+ ```python
95
+ # Let's use the first one:
96
+ dlg = fc.search_dialog()
97
+ # Or a specific search dialog:
98
+ dlg = fc.search_dialog("Default search dialog")
99
+ ```
100
+
101
+ Each search term consists of a field name and a search pattern. Each search
102
+ dialog knows its fields:
103
+
104
+ ```python
105
+ for field in dlg.fields.values():
106
+ print("Id =", field.id)
107
+ print("Length=", field.length)
108
+ print("Name =", field.name)
109
+ print("Type =", field.type)
110
+ print("-------")
111
+ ```
112
+
113
+ Let's search for some documents:
114
+
115
+ ```python
116
+ # Search for DOCNO equal to '123456':
117
+ for result in dlg.search("DOCNO=123456"):
118
+ print(result)
119
+ # Search for two patterns alternatively:
120
+ for result in dlg.search(["DOCNO=123456", "DOCNO=654321"], operation=docuware.OR):
121
+ print(result)
122
+ # Search for documents in a date range (01-31 January 2023):
123
+ for result in dlg.search("DWSTOREDATETIME=2023-01-01T00:00:00,2023-02-01T00:00:00")
124
+ print(result)
125
+ ```
126
+
127
+ Please note that search terms may also contain metacharacters such as `*`, `(`,
128
+ `)`, which may need to be escaped when searching for these characters
129
+ themselves.
130
+
131
+ ```python
132
+ for result in dlg.search("DOCTYPE=Invoice \\(incoming\\)"):
133
+ print(result)
134
+ ```
135
+
136
+ Search terms can be as simple as a single string, but can also be more complex.
137
+ The following two queries are equivalent:
138
+
139
+ ```python
140
+ dlg.search(["FIELD1=TERM1,TERM2", "FIELD2=TERM3"])
141
+ dlg.search({"FIELD1": ["TERM1", "TERM2"], "FIELD2": ["TERM3"]})
142
+ ```
143
+
144
+ The result of a search is always an iterator over the search results, even if
145
+ no result was obtained. Each individual search result holds a `document`
146
+ attribute, which gives access to the document in the archive. The document
147
+ itself can be downloaded as a whole or only individual attachments.
148
+
149
+ ```python
150
+ for result in dlg.search("DOCNO=123456"):
151
+ doc = result.document
152
+ # Download the complete document ...
153
+ data, content_type, filename = doc.download(keep_annotations=True)
154
+ docuware.write_binary_file(data, filename)
155
+ # ... or individual attachments (or sections, as DocuWare calls them)
156
+ for att in doc.attachments:
157
+ data, content_type, filename = att.download()
158
+ docuware.write_binary_file(data, filename)
159
+ ```
160
+
161
+ Create data entry in file cabinet:
162
+ ```python
163
+ data = {
164
+ "FIELD1": "value1",
165
+ "FIELD2": "value2",
166
+ }
167
+ response = fc.create_data_entry(data)
168
+ ```
169
+
170
+ _Subject to rewrite:_ Update data fields of document. The search parameter must
171
+ return a single document. Use a loop to execute this function on multiple
172
+ documents:
173
+
174
+ ```python
175
+ fields = {
176
+ "FIELD1": "value1",
177
+ "FIELD2": 99999
178
+ }
179
+ response = fc.update_data_entry(["FIELD1=TERM1,TERM2", "FIELD2=TERM3"], user_fields)
180
+ ```
181
+
182
+ Delete documents:
183
+
184
+ ```python
185
+ dlg = fc.search_dialog()
186
+ for result in dlg.search(["FIELD1=TERM1,TERM2", "FIELD2=TERM3"]):
187
+ document = result.document
188
+ document.delete()
189
+ ```
190
+
191
+ Users and groups of an organisation can be accessed and managed:
192
+
193
+ ```python
194
+ # Iterate over the list of users and groups:
195
+ for user in org.users:
196
+ print(user)
197
+ for group in org.groups:
198
+ print(group)
199
+
200
+ # Find a specific user:
201
+ user = org.users["John Doe"] # or: org.users.get("John Doe")
202
+
203
+ # Add a user to a group:
204
+ group = org.groups["Managers"] # or: org.groups.get("Managers")
205
+ group.add_user(user)
206
+ # or
207
+ user.add_to_group(group)
208
+
209
+ # Deactivate user:
210
+ user.active = False # or True to activate user
211
+
212
+ # Create a new user:
213
+ user = docuware.User(first_name="John", last_name="Doe")
214
+ org.users.add(user, password="123456")
215
+ ```
216
+
217
+
218
+ ## CLI usage
219
+
220
+ This package also includes a simple CLI program for collecting information
221
+ about the archive and searching and downloading documents or attachments.
222
+
223
+ First you need to log in:
224
+
225
+ ```console
226
+ $ dw-client login --url http://localhost/ --username "Doe, John" --password FooBar --organization "Doe Inc."
227
+ ```
228
+
229
+ The credentials and the session cookie are stored in the `.credentials` and
230
+ `.session` files in the current directory.
231
+
232
+ Of course, `--help` will give you a list of all options:
233
+
234
+ ```console
235
+ $ dw-client --help
236
+ ```
237
+
238
+ Some search examples (Bash shell syntax):
239
+
240
+ ```console
241
+ $ dw-client search --file-cabinet Archive Customer=Foo\*
242
+ $ dw-client search --file-cabinet Archive DocNo=123456 "DocType=Invoice \\(incoming\\)"
243
+ $ dw-client search --file-cabinet Archive DocDate=2022-02-14
244
+ ```
245
+
246
+ Downloading documents:
247
+
248
+ ```console
249
+ $ dw-client search --file-cabinet Archive Customer=Foo\* --download document --annotations
250
+ ```
251
+
252
+ Downloading attachments (or sections):
253
+
254
+ ```console
255
+ $ dw-client search --file-cabinet Archive DocNo=123456 --download attachments
256
+ ```
257
+
258
+ Some information about your DocuWare installation:
259
+
260
+ ```console
261
+ $ dw-client info
262
+ ```
263
+
264
+ Listing all organizations, file cabinets and dialogs at once:
265
+
266
+ ```console
267
+ $ dw-client list
268
+ ```
269
+
270
+ A more specific list, only one file cabinet:
271
+
272
+ ```console
273
+ $ dw-client list --file-cabinet Archive
274
+ ```
275
+
276
+ You can also display a (partial) selection of the contents of individual fields:
277
+
278
+ ```console
279
+ $ dw-client list --file-cabinet Archive --dialog custom --field DocNo
280
+ ```
281
+
282
+
283
+ ## Further reading
284
+
285
+ * Entry point to [DocuWare's official documentation][2] of the REST API.
286
+ * Notable endpoint: `/DocuWare/Platform/Content/PlatformLinkModel.pdf`
287
+
288
+
289
+ ## License
290
+
291
+ This work is released under the BSD 3 license. You may use and redistribute
292
+ this software as long as the copyright notice is preserved.
293
+
294
+
295
+ [1]: https://docuware.com/
296
+ [2]: https://developer.docuware.com/rest/index.html
297
+
@@ -0,0 +1,274 @@
1
+ # docuware-client
2
+
3
+ This is a client library for the REST API of [DocuWare][1] DMS. Since
4
+ [DocuWare's documentation][2] regarding the REST API is very sparse (at the
5
+ time these lines were written), this client serves only a part of the API's
6
+ functionality.
7
+
8
+ Please keep in mind: **This software is not related to DocuWare.** It is a work
9
+ in progress, may yield unexpected results, and almost certainly contains bugs.
10
+
11
+ > ⚠️ Starting with version 0.5.0, OAuth2 authentication is the new default.
12
+ > Unless you explicitly request cookie authentication with
13
+ > `dw.login(..., cookie_auth=True)`, OAuth2 will be used. OAuth2 authentication
14
+ > has been available since DocuWare 7.10, and
15
+ > [cookie authentication will be discontinued](https://start.docuware.com/blog/product-news/docuware-sdk-discontinuation-of-cookie-authentication)
16
+ > with DocuWare 7.11.
17
+
18
+
19
+ ## Usage
20
+
21
+ First you have to log in and create a persistent session:
22
+
23
+ ```python
24
+ import json
25
+ import pathlib
26
+ import docuware
27
+
28
+ dw = docuware.Client("http://localhost")
29
+ session = dw.login("username", "password", "organization")
30
+ with open(".session", "w") as f:
31
+ json.dump(session, f)
32
+ ```
33
+
34
+ From then on you have to reuse the session, otherwise you will be locked out of
35
+ the DocuWare service for a period of time (10 minutes or even longer). As the
36
+ session cookie may change on subsequent logins, update the session file on
37
+ every login.
38
+
39
+ ```python
40
+ session_file = pathlib.Path(".session")
41
+ if session_file.exists():
42
+ with open(session_file) as f:
43
+ session = json.load(f)
44
+ else:
45
+ session = None
46
+ dw = docuware.Client("http://localhost")
47
+ session = dw.login("username", "password", "organization", saved_session=session)
48
+ with open(session_file, "w") as f:
49
+ json.dump(session, f)
50
+ ```
51
+
52
+ Iterate over the organizations and file cabinets:
53
+
54
+ ```python
55
+ for org in dw.organizations:
56
+ print(org)
57
+ for fc in org.file_cabinets:
58
+ print(" ", fc)
59
+ ```
60
+
61
+ If you already know the ID or name of the objects, you can also access them
62
+ directly.
63
+
64
+ ```python
65
+ org = dw.organization("1")
66
+ fc = org.file_cabinet("Archive")
67
+ ```
68
+
69
+ Now some examples of how to search for documents. First you need a search
70
+ dialog:
71
+
72
+ ```python
73
+ # Let's use the first one:
74
+ dlg = fc.search_dialog()
75
+ # Or a specific search dialog:
76
+ dlg = fc.search_dialog("Default search dialog")
77
+ ```
78
+
79
+ Each search term consists of a field name and a search pattern. Each search
80
+ dialog knows its fields:
81
+
82
+ ```python
83
+ for field in dlg.fields.values():
84
+ print("Id =", field.id)
85
+ print("Length=", field.length)
86
+ print("Name =", field.name)
87
+ print("Type =", field.type)
88
+ print("-------")
89
+ ```
90
+
91
+ Let's search for some documents:
92
+
93
+ ```python
94
+ # Search for DOCNO equal to '123456':
95
+ for result in dlg.search("DOCNO=123456"):
96
+ print(result)
97
+ # Search for two patterns alternatively:
98
+ for result in dlg.search(["DOCNO=123456", "DOCNO=654321"], operation=docuware.OR):
99
+ print(result)
100
+ # Search for documents in a date range (01-31 January 2023):
101
+ for result in dlg.search("DWSTOREDATETIME=2023-01-01T00:00:00,2023-02-01T00:00:00")
102
+ print(result)
103
+ ```
104
+
105
+ Please note that search terms may also contain metacharacters such as `*`, `(`,
106
+ `)`, which may need to be escaped when searching for these characters
107
+ themselves.
108
+
109
+ ```python
110
+ for result in dlg.search("DOCTYPE=Invoice \\(incoming\\)"):
111
+ print(result)
112
+ ```
113
+
114
+ Search terms can be as simple as a single string, but can also be more complex.
115
+ The following two queries are equivalent:
116
+
117
+ ```python
118
+ dlg.search(["FIELD1=TERM1,TERM2", "FIELD2=TERM3"])
119
+ dlg.search({"FIELD1": ["TERM1", "TERM2"], "FIELD2": ["TERM3"]})
120
+ ```
121
+
122
+ The result of a search is always an iterator over the search results, even if
123
+ no result was obtained. Each individual search result holds a `document`
124
+ attribute, which gives access to the document in the archive. The document
125
+ itself can be downloaded as a whole or only individual attachments.
126
+
127
+ ```python
128
+ for result in dlg.search("DOCNO=123456"):
129
+ doc = result.document
130
+ # Download the complete document ...
131
+ data, content_type, filename = doc.download(keep_annotations=True)
132
+ docuware.write_binary_file(data, filename)
133
+ # ... or individual attachments (or sections, as DocuWare calls them)
134
+ for att in doc.attachments:
135
+ data, content_type, filename = att.download()
136
+ docuware.write_binary_file(data, filename)
137
+ ```
138
+
139
+ Create data entry in file cabinet:
140
+ ```python
141
+ data = {
142
+ "FIELD1": "value1",
143
+ "FIELD2": "value2",
144
+ }
145
+ response = fc.create_data_entry(data)
146
+ ```
147
+
148
+ _Subject to rewrite:_ Update data fields of document. The search parameter must
149
+ return a single document. Use a loop to execute this function on multiple
150
+ documents:
151
+
152
+ ```python
153
+ fields = {
154
+ "FIELD1": "value1",
155
+ "FIELD2": 99999
156
+ }
157
+ response = fc.update_data_entry(["FIELD1=TERM1,TERM2", "FIELD2=TERM3"], user_fields)
158
+ ```
159
+
160
+ Delete documents:
161
+
162
+ ```python
163
+ dlg = fc.search_dialog()
164
+ for result in dlg.search(["FIELD1=TERM1,TERM2", "FIELD2=TERM3"]):
165
+ document = result.document
166
+ document.delete()
167
+ ```
168
+
169
+ Users and groups of an organisation can be accessed and managed:
170
+
171
+ ```python
172
+ # Iterate over the list of users and groups:
173
+ for user in org.users:
174
+ print(user)
175
+ for group in org.groups:
176
+ print(group)
177
+
178
+ # Find a specific user:
179
+ user = org.users["John Doe"] # or: org.users.get("John Doe")
180
+
181
+ # Add a user to a group:
182
+ group = org.groups["Managers"] # or: org.groups.get("Managers")
183
+ group.add_user(user)
184
+ # or
185
+ user.add_to_group(group)
186
+
187
+ # Deactivate user:
188
+ user.active = False # or True to activate user
189
+
190
+ # Create a new user:
191
+ user = docuware.User(first_name="John", last_name="Doe")
192
+ org.users.add(user, password="123456")
193
+ ```
194
+
195
+
196
+ ## CLI usage
197
+
198
+ This package also includes a simple CLI program for collecting information
199
+ about the archive and searching and downloading documents or attachments.
200
+
201
+ First you need to log in:
202
+
203
+ ```console
204
+ $ dw-client login --url http://localhost/ --username "Doe, John" --password FooBar --organization "Doe Inc."
205
+ ```
206
+
207
+ The credentials and the session cookie are stored in the `.credentials` and
208
+ `.session` files in the current directory.
209
+
210
+ Of course, `--help` will give you a list of all options:
211
+
212
+ ```console
213
+ $ dw-client --help
214
+ ```
215
+
216
+ Some search examples (Bash shell syntax):
217
+
218
+ ```console
219
+ $ dw-client search --file-cabinet Archive Customer=Foo\*
220
+ $ dw-client search --file-cabinet Archive DocNo=123456 "DocType=Invoice \\(incoming\\)"
221
+ $ dw-client search --file-cabinet Archive DocDate=2022-02-14
222
+ ```
223
+
224
+ Downloading documents:
225
+
226
+ ```console
227
+ $ dw-client search --file-cabinet Archive Customer=Foo\* --download document --annotations
228
+ ```
229
+
230
+ Downloading attachments (or sections):
231
+
232
+ ```console
233
+ $ dw-client search --file-cabinet Archive DocNo=123456 --download attachments
234
+ ```
235
+
236
+ Some information about your DocuWare installation:
237
+
238
+ ```console
239
+ $ dw-client info
240
+ ```
241
+
242
+ Listing all organizations, file cabinets and dialogs at once:
243
+
244
+ ```console
245
+ $ dw-client list
246
+ ```
247
+
248
+ A more specific list, only one file cabinet:
249
+
250
+ ```console
251
+ $ dw-client list --file-cabinet Archive
252
+ ```
253
+
254
+ You can also display a (partial) selection of the contents of individual fields:
255
+
256
+ ```console
257
+ $ dw-client list --file-cabinet Archive --dialog custom --field DocNo
258
+ ```
259
+
260
+
261
+ ## Further reading
262
+
263
+ * Entry point to [DocuWare's official documentation][2] of the REST API.
264
+ * Notable endpoint: `/DocuWare/Platform/Content/PlatformLinkModel.pdf`
265
+
266
+
267
+ ## License
268
+
269
+ This work is released under the BSD 3 license. You may use and redistribute
270
+ this software as long as the copyright notice is preserved.
271
+
272
+
273
+ [1]: https://docuware.com/
274
+ [2]: https://developer.docuware.com/rest/index.html
@@ -0,0 +1,9 @@
1
+ from docuware.client import DocuwareClient
2
+ from docuware.dialogs import SearchDialog
3
+ from docuware.errors import *
4
+ from docuware.filecabinet import FileCabinet
5
+ from docuware.organization import Organization
6
+ from docuware.users import User, Group
7
+ from docuware.utils import unique_filename, write_binary_file, random_password
8
+
9
+ Client = DocuwareClient
@@ -0,0 +1,80 @@
1
+ """
2
+ Case-insensitive dictionary.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from collections.abc import MutableMapping, ItemsView, KeysView, ValuesView
7
+ from typing import Any, Dict, Generator, Generic, Iterator, Optional, Tuple, TypeVar
8
+
9
+ VT = TypeVar('VT')
10
+
11
+ class CaseInsensitiveDict(MutableMapping[str, VT], Generic[VT]):
12
+ def __init__(self, initial_values: Any = None, **kwargs: VT) -> None:
13
+ self._items: Dict[str, Tuple[str, VT]] = dict()
14
+ if initial_values is not None:
15
+ if isinstance(initial_values, (dict, CaseInsensitiveDict)):
16
+ for key, value in initial_values.items():
17
+ self.__setitem__(key, value)
18
+ elif isinstance(initial_values, (list, tuple)):
19
+ for (key, value) in initial_values:
20
+ self.__setitem__(key, value)
21
+ else:
22
+ raise TypeError("Unsupported type for initial_values")
23
+ for key, value in kwargs.items():
24
+ self.__setitem__(key, value)
25
+
26
+ @staticmethod
27
+ def _strip_case(key: Any) -> str:
28
+ return str(key).casefold()
29
+
30
+ def __contains__(self, key: Any) -> bool:
31
+ return self._items.__contains__(self._strip_case(key))
32
+
33
+ def __getitem__(self, key: str) -> VT:
34
+ try:
35
+ return self._items[self._strip_case(key)][1]
36
+ except KeyError:
37
+ raise KeyError(key)
38
+
39
+ def __setitem__(self, key: str, value: VT) -> None:
40
+ self._items[self._strip_case(key)] = (key, value)
41
+
42
+ def __iter__(self) -> Generator[str, None, None]:
43
+ return (item[0] for item in self._items.values())
44
+
45
+ def __delitem__(self, key: str) -> None:
46
+ del self._items[self._strip_case(key)]
47
+
48
+ def __len__(self) -> int:
49
+ return len(self._items)
50
+
51
+ def __eq__(self, other: Any) -> bool:
52
+ if not isinstance(other, CaseInsensitiveDict):
53
+ return NotImplemented
54
+ return dict(self.case_insensitive_items()) == dict(other.case_insensitive_items())
55
+
56
+ def get(self, key: str, default: Optional[VT] = None) -> Optional[VT]:
57
+ try:
58
+ return self.__getitem__(key)
59
+ except KeyError:
60
+ return default
61
+
62
+ def items(self) -> ItemsView[str, VT]:
63
+ return ItemsView(self)
64
+
65
+ def keys(self) -> KeysView[str]:
66
+ return KeysView(self)
67
+
68
+ def values(self) -> ValuesView[VT]:
69
+ return ValuesView(self)
70
+
71
+ def case_insensitive_items(self) -> Generator[Tuple[str, VT], None, None]:
72
+ return ((k, v[1]) for (k, v) in self._items.items())
73
+
74
+ def copy(self) -> CaseInsensitiveDict[VT]:
75
+ return CaseInsensitiveDict(list(self._items.values()))
76
+
77
+ def __repr__(self) -> str:
78
+ return str(dict(self.items()))
79
+
80
+ # vim: set et sw=4 ts=4: