kp2bw 2.0.0__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.
kp2bw-2.0.0/PKG-INFO ADDED
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.3
2
+ Name: kp2bw
3
+ Version: 2.0.0
4
+ Summary: Imports and existing KeePass db with REF fields into Bitwarden
5
+ Keywords: bitwarden,cli,keepass,migration,password-manager
6
+ Author: Kaj Kowalski
7
+ Author-email: Kaj Kowalski <info@kajkowalski.nl>
8
+ Classifier: Environment :: Console
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: Topic :: Security
14
+ Classifier: Topic :: Utilities
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: pykeepass>=4.1.1.post1
17
+ Requires-Python: >=3.14
18
+ Project-URL: Changelog, https://github.com/kjanat/kp2bw/blob/master/CHANGELOG.md
19
+ Project-URL: Issues, https://github.com/kjanat/kp2bw/issues
20
+ Project-URL: Repository, https://github.com/kjanat/kp2bw
21
+ Description-Content-Type: text/markdown
22
+
23
+ # KP2BW - KeePass 2.x to Bitwarden Converter
24
+
25
+ > Fork of [jampe/kp2bw], modernized.
26
+
27
+ Migrates KeePass databases to Bitwarden via the `bw` CLI, with advantages over
28
+ the built-in Bitwarden importer:
29
+
30
+ - **Encrypted in-memory transfer** -- data never hits disk unencrypted (except
31
+ attachments, which are cleaned up after upload)
32
+ - **KeePass REF resolution** -- username/password references are resolved:
33
+ matching credentials merge URLs into one entry; differing ones create new
34
+ entries
35
+ - **Passkey migration** -- KeePassXC FIDO2/passkey credentials
36
+ (`KPEX_PASSKEY_*`) are converted to Bitwarden `fido2Credentials`
37
+ - **Custom properties & attachments** -- imported as Bitwarden custom fields or
38
+ attachments (values > 10k chars auto-upload as files)
39
+ - **Long notes handling** -- notes exceeding 10k chars are uploaded as
40
+ `notes.txt` attachments
41
+ - **Idempotent** -- safe to run multiple times without duplicating entries
42
+ - **Nested folders** -- KeePass folder hierarchy is recreated in Bitwarden
43
+ - **Recycle Bin filtering** -- deleted entries are automatically excluded
44
+ - **Expiry awareness** -- expired entries are marked `[EXPIRED]` in notes;
45
+ optionally skip them entirely with `--skip-expired`
46
+ - **Metadata preservation** -- KeePass tags, expiry dates, and created/modified
47
+ timestamps are stored as Bitwarden custom fields
48
+ - **Tag filtering** -- import only entries matching specific tags
49
+ - **Organization & collection support** -- upload into a Bitwarden organization
50
+ with automatic or manual collection assignment
51
+ - **Full UTF-8 & cross-platform** -- works on Windows, macOS, and Linux
52
+
53
+ ## Installation
54
+
55
+ ```bash
56
+ # install with:
57
+ uv tool install kp2bw
58
+ kp2bw passwords.kdbx
59
+
60
+ # or run directly without installing:
61
+ uvx kp2bw
62
+ ```
63
+
64
+ or from a GitHub URL:
65
+
66
+ ```bash
67
+ # install with:
68
+ uv tool install git+https://github.com/kjanat/kp2bw
69
+ kp2bw passwords.kdbx
70
+
71
+ # run directly without installing:
72
+ uvx --from git+https://github.com/kjanat/kp2bw kp2bw passwords.kdbx
73
+ ```
74
+
75
+ ## Prerequisites
76
+
77
+ Install the [Bitwarden CLI] and log in once before using `kp2bw`:
78
+
79
+ ```bash
80
+ # optional: point to a self-hosted instance
81
+ bw config server https://your-domain.com/
82
+
83
+ # log in (only needed once; kp2bw uses `bw unlock` afterwards)
84
+ bw login <user>
85
+ ```
86
+
87
+ ## Usage
88
+
89
+ ```console
90
+ kp2bw [-h] [-k KEEPASS_PASSWORD] [-K KEEPASS_KEYFILE] [-b BITWARDEN_PASSWORD]
91
+ [-o BITWARDEN_ORG] [-c BITWARDEN_COLLECTION] [-t TAG [TAG ...]]
92
+ [--path-to-name | --no-path-to-name] [--path-to-name-skip N]
93
+ [--skip-expired | --no-skip-expired]
94
+ [--include-recycle-bin | --no-include-recycle-bin]
95
+ [--metadata | --no-metadata] [-y] [-v] [-V | --version] keepass_file
96
+ ```
97
+
98
+ | Flag | Description | Env var |
99
+ | -------------------------------------- | -------------------------------------------------------------- | ------------------------------------- |
100
+ | `keepass_file` | Path to your KeePass 2.x database | - |
101
+ | `-k, --keepass-password` | KeePass password (prompted if omitted) | `KP2BW_KEEPASS_PASSWORD` |
102
+ | `-K, --keepass-keyfile` | KeePass key file | `KP2BW_KEEPASS_KEYFILE` |
103
+ | `-b, --bitwarden-password` | Bitwarden password (prompted if omitted) | `KP2BW_BITWARDEN_PASSWORD` |
104
+ | `-o, --bitwarden-org` | Bitwarden Organization ID | `KP2BW_BITWARDEN_ORG` |
105
+ | `-c, --bitwarden-collection` | Collection ID, or `auto` to derive from top-level folder names | `KP2BW_BITWARDEN_COLLECTION` |
106
+ | `-t, --import-tags` | Only import entries with these tags | `KP2BW_IMPORT_TAGS` (comma-separated) |
107
+ | `--path-to-name` / `--no-path-to-name` | Prepend folder path to entry names (default: off) | `KP2BW_PATH_TO_NAME` |
108
+ | `--path-to-name-skip` | Skip first N folders in path prefix (default: 1) | `KP2BW_PATH_TO_NAME_SKIP` |
109
+ | `--skip-expired` | Skip entries that have expired in KeePass | `KP2BW_SKIP_EXPIRED` |
110
+ | `--include-recycle-bin` | Include Recycle Bin entries (excluded by default) | `KP2BW_INCLUDE_RECYCLE_BIN` |
111
+ | `--metadata` / `--no-metadata` | Toggle KeePass metadata as custom fields (default: on) | `KP2BW_MIGRATE_METADATA` |
112
+ | `-y, --yes` | Skip the Bitwarden CLI setup confirmation prompt | `KP2BW_YES` |
113
+ | `-v, --verbose` | Verbose output | `KP2BW_VERBOSE` |
114
+ | `-V, --version` | Print the installed `kp2bw` version and exit | - |
115
+
116
+ Configuration precedence is always: CLI flag > environment variable > built-in default.
117
+
118
+ ## Troubleshooting
119
+
120
+ ### "Invalid master password" on `bw unlock`
121
+
122
+ If your password contains special shell characters (`?`, `>`, `&`, etc.), wrap
123
+ it in double quotes when prompted. See jampe/kp2bw#10 and
124
+ libkeepass/pykeepass#254 for details.
125
+
126
+ [jampe/kp2bw]: https://github.com/jampe/kp2bw
127
+ [Bitwarden CLI]: https://bitwarden.com/help/cli/
kp2bw-2.0.0/README.md ADDED
@@ -0,0 +1,105 @@
1
+ # KP2BW - KeePass 2.x to Bitwarden Converter
2
+
3
+ > Fork of [jampe/kp2bw], modernized.
4
+
5
+ Migrates KeePass databases to Bitwarden via the `bw` CLI, with advantages over
6
+ the built-in Bitwarden importer:
7
+
8
+ - **Encrypted in-memory transfer** -- data never hits disk unencrypted (except
9
+ attachments, which are cleaned up after upload)
10
+ - **KeePass REF resolution** -- username/password references are resolved:
11
+ matching credentials merge URLs into one entry; differing ones create new
12
+ entries
13
+ - **Passkey migration** -- KeePassXC FIDO2/passkey credentials
14
+ (`KPEX_PASSKEY_*`) are converted to Bitwarden `fido2Credentials`
15
+ - **Custom properties & attachments** -- imported as Bitwarden custom fields or
16
+ attachments (values > 10k chars auto-upload as files)
17
+ - **Long notes handling** -- notes exceeding 10k chars are uploaded as
18
+ `notes.txt` attachments
19
+ - **Idempotent** -- safe to run multiple times without duplicating entries
20
+ - **Nested folders** -- KeePass folder hierarchy is recreated in Bitwarden
21
+ - **Recycle Bin filtering** -- deleted entries are automatically excluded
22
+ - **Expiry awareness** -- expired entries are marked `[EXPIRED]` in notes;
23
+ optionally skip them entirely with `--skip-expired`
24
+ - **Metadata preservation** -- KeePass tags, expiry dates, and created/modified
25
+ timestamps are stored as Bitwarden custom fields
26
+ - **Tag filtering** -- import only entries matching specific tags
27
+ - **Organization & collection support** -- upload into a Bitwarden organization
28
+ with automatic or manual collection assignment
29
+ - **Full UTF-8 & cross-platform** -- works on Windows, macOS, and Linux
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ # install with:
35
+ uv tool install kp2bw
36
+ kp2bw passwords.kdbx
37
+
38
+ # or run directly without installing:
39
+ uvx kp2bw
40
+ ```
41
+
42
+ or from a GitHub URL:
43
+
44
+ ```bash
45
+ # install with:
46
+ uv tool install git+https://github.com/kjanat/kp2bw
47
+ kp2bw passwords.kdbx
48
+
49
+ # run directly without installing:
50
+ uvx --from git+https://github.com/kjanat/kp2bw kp2bw passwords.kdbx
51
+ ```
52
+
53
+ ## Prerequisites
54
+
55
+ Install the [Bitwarden CLI] and log in once before using `kp2bw`:
56
+
57
+ ```bash
58
+ # optional: point to a self-hosted instance
59
+ bw config server https://your-domain.com/
60
+
61
+ # log in (only needed once; kp2bw uses `bw unlock` afterwards)
62
+ bw login <user>
63
+ ```
64
+
65
+ ## Usage
66
+
67
+ ```console
68
+ kp2bw [-h] [-k KEEPASS_PASSWORD] [-K KEEPASS_KEYFILE] [-b BITWARDEN_PASSWORD]
69
+ [-o BITWARDEN_ORG] [-c BITWARDEN_COLLECTION] [-t TAG [TAG ...]]
70
+ [--path-to-name | --no-path-to-name] [--path-to-name-skip N]
71
+ [--skip-expired | --no-skip-expired]
72
+ [--include-recycle-bin | --no-include-recycle-bin]
73
+ [--metadata | --no-metadata] [-y] [-v] [-V | --version] keepass_file
74
+ ```
75
+
76
+ | Flag | Description | Env var |
77
+ | -------------------------------------- | -------------------------------------------------------------- | ------------------------------------- |
78
+ | `keepass_file` | Path to your KeePass 2.x database | - |
79
+ | `-k, --keepass-password` | KeePass password (prompted if omitted) | `KP2BW_KEEPASS_PASSWORD` |
80
+ | `-K, --keepass-keyfile` | KeePass key file | `KP2BW_KEEPASS_KEYFILE` |
81
+ | `-b, --bitwarden-password` | Bitwarden password (prompted if omitted) | `KP2BW_BITWARDEN_PASSWORD` |
82
+ | `-o, --bitwarden-org` | Bitwarden Organization ID | `KP2BW_BITWARDEN_ORG` |
83
+ | `-c, --bitwarden-collection` | Collection ID, or `auto` to derive from top-level folder names | `KP2BW_BITWARDEN_COLLECTION` |
84
+ | `-t, --import-tags` | Only import entries with these tags | `KP2BW_IMPORT_TAGS` (comma-separated) |
85
+ | `--path-to-name` / `--no-path-to-name` | Prepend folder path to entry names (default: off) | `KP2BW_PATH_TO_NAME` |
86
+ | `--path-to-name-skip` | Skip first N folders in path prefix (default: 1) | `KP2BW_PATH_TO_NAME_SKIP` |
87
+ | `--skip-expired` | Skip entries that have expired in KeePass | `KP2BW_SKIP_EXPIRED` |
88
+ | `--include-recycle-bin` | Include Recycle Bin entries (excluded by default) | `KP2BW_INCLUDE_RECYCLE_BIN` |
89
+ | `--metadata` / `--no-metadata` | Toggle KeePass metadata as custom fields (default: on) | `KP2BW_MIGRATE_METADATA` |
90
+ | `-y, --yes` | Skip the Bitwarden CLI setup confirmation prompt | `KP2BW_YES` |
91
+ | `-v, --verbose` | Verbose output | `KP2BW_VERBOSE` |
92
+ | `-V, --version` | Print the installed `kp2bw` version and exit | - |
93
+
94
+ Configuration precedence is always: CLI flag > environment variable > built-in default.
95
+
96
+ ## Troubleshooting
97
+
98
+ ### "Invalid master password" on `bw unlock`
99
+
100
+ If your password contains special shell characters (`?`, `>`, `&`, etc.), wrap
101
+ it in double quotes when prompted. See jampe/kp2bw#10 and
102
+ libkeepass/pykeepass#254 for details.
103
+
104
+ [jampe/kp2bw]: https://github.com/jampe/kp2bw
105
+ [Bitwarden CLI]: https://bitwarden.com/help/cli/
@@ -0,0 +1,64 @@
1
+ [project]
2
+ name = "kp2bw"
3
+ version = "2.0.0"
4
+ description = "Imports and existing KeePass db with REF fields into Bitwarden"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ keywords = ["bitwarden", "cli", "keepass", "migration", "password-manager"]
8
+ classifiers = [
9
+ "Environment :: Console",
10
+ "Operating System :: OS Independent",
11
+ "Programming Language :: Python :: 3",
12
+ "Programming Language :: Python :: 3 :: Only",
13
+ "Programming Language :: Python :: 3.14",
14
+ "Topic :: Security",
15
+ "Topic :: Utilities",
16
+ "Typing :: Typed",
17
+ ]
18
+ dependencies = ["pykeepass>=4.1.1.post1"]
19
+
20
+ [[project.authors]]
21
+ name = "Kaj Kowalski"
22
+ email = "info@kajkowalski.nl"
23
+
24
+ [project.urls]
25
+ Changelog = "https://github.com/kjanat/kp2bw/blob/master/CHANGELOG.md"
26
+ Issues = "https://github.com/kjanat/kp2bw/issues"
27
+ Repository = "https://github.com/kjanat/kp2bw"
28
+
29
+ [project.scripts]
30
+ kp2bw = "kp2bw.cli:main"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ { include-group = "lint" },
35
+ { include-group = "stubs" },
36
+ ]
37
+ lint = [
38
+ "basedpyright>=1.38.1",
39
+ "ruff>=0.15.2",
40
+ "tombi>=0.7.32",
41
+ "ty>=0.0.18",
42
+ ]
43
+ stubs = ["pykeepass-stubs"]
44
+
45
+ [build-system]
46
+ requires = ["uv_build>=0.10.4,<0.11.0"]
47
+ build-backend = "uv_build"
48
+
49
+ [tool.pyright]
50
+ venvPath = "."
51
+ venv = ".venv"
52
+ typeCheckingMode = "strict"
53
+ pythonVersion = "3.14"
54
+
55
+ [tool.ruff]
56
+ preview = true
57
+ target-version = "py314"
58
+
59
+ [tool.uv.sources]
60
+ pykeepass-stubs = { workspace = true }
61
+
62
+ [tool.uv.workspace]
63
+ members = ["packages/*"]
64
+ exclude = []
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version("kp2bw")
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,247 @@
1
+ import base64
2
+ import json
3
+ import logging
4
+ import os
5
+ import platform
6
+ import shutil
7
+ from itertools import groupby
8
+ from subprocess import STDOUT, CalledProcessError, check_output
9
+ from typing import Any
10
+
11
+ from pykeepass import Attachment
12
+
13
+ from .exceptions import BitwardenClientError
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class BitwardenClient:
19
+ TEMPORARY_ATTACHMENT_FOLDER: str = "attachment-temp"
20
+
21
+ _orgId: str | None
22
+ _key: str
23
+ _folders: dict[str, str]
24
+ _folder_entries: dict[str | None, list[str]]
25
+ _colls: dict[str, str] | None
26
+
27
+ def __init__(self, password: str, org_id: str | None) -> None:
28
+ # check for bw cli installation
29
+ if "bitwarden" not in self._exec("bw"):
30
+ raise BitwardenClientError(
31
+ "Bitwarden Cli not installed! See https://help.bitwarden.com/article/cli/#download--install for help"
32
+ )
33
+
34
+ # save org
35
+ self._orgId = org_id
36
+
37
+ # login
38
+ self._key = self._exec(f'bw unlock "{password}" --raw')
39
+ if "error" in self._key:
40
+ raise BitwardenClientError(
41
+ "Could not unlock the Bitwarden db. Is the Master Password correct and are bw cli tools set up correctly?"
42
+ )
43
+
44
+ # make sure data is up to date
45
+ if "Syncing complete." not in self._exec_with_session("bw sync"):
46
+ raise BitwardenClientError(
47
+ "Could not sync the local state to your Bitwarden server"
48
+ )
49
+
50
+ # get folder list
51
+ self._folders = {
52
+ folder["name"]: folder["id"]
53
+ for folder in json.loads(self._exec_with_session("bw list folders"))
54
+ }
55
+
56
+ # get existing entries
57
+ self._folder_entries = self._get_existing_folder_entries()
58
+
59
+ # get existing collections
60
+ if org_id:
61
+ self._colls = {
62
+ coll["name"]: coll["id"]
63
+ for coll in json.loads(
64
+ self._exec_with_session(
65
+ f"bw list org-collections --organizationid {org_id}"
66
+ )
67
+ )
68
+ }
69
+ else:
70
+ self._colls = None
71
+
72
+ def __del__(self) -> None:
73
+ # cleanup temp directory
74
+ self._remove_temporary_attachment_folder()
75
+
76
+ def _create_temporary_attachment_folder(self) -> None:
77
+ if not os.path.isdir(self.TEMPORARY_ATTACHMENT_FOLDER):
78
+ os.mkdir(self.TEMPORARY_ATTACHMENT_FOLDER)
79
+
80
+ def _remove_temporary_attachment_folder(self) -> None:
81
+ if os.path.isdir(self.TEMPORARY_ATTACHMENT_FOLDER):
82
+ shutil.rmtree(self.TEMPORARY_ATTACHMENT_FOLDER)
83
+
84
+ def _exec(self, command: str) -> str:
85
+ output: bytes
86
+ try:
87
+ logger.debug("-- Executing Bitwarden CLI command")
88
+ output = check_output(command, stderr=STDOUT, shell=True)
89
+ except CalledProcessError as e:
90
+ logger.debug(
91
+ f" |- Bitwarden CLI command failed with exit code {e.returncode}"
92
+ )
93
+ if isinstance(e.output, bytes):
94
+ output = e.output
95
+ else:
96
+ output = str(e.output).encode("utf-8", "ignore")
97
+
98
+ logger.debug(f" |- Received {len(output)} bytes from Bitwarden CLI")
99
+ return output.decode("utf-8", "ignore")
100
+
101
+ def _get_existing_folder_entries(self) -> dict[str | None, list[str]]:
102
+ folder_id_lookup_helper: dict[str, str] = {
103
+ folder_id: folder_name for folder_name, folder_id in self._folders.items()
104
+ }
105
+ items: list[dict[str, Any]] = json.loads(
106
+ self._exec_with_session("bw list items")
107
+ )
108
+
109
+ # fix None folderIds for entries without folders
110
+ for item in items:
111
+ if not item["folderId"]:
112
+ item["folderId"] = ""
113
+
114
+ items.sort(key=lambda item: item["folderId"])
115
+ return {
116
+ folder_id_lookup_helper.get(folder_id): [entry["name"] for entry in entries]
117
+ for folder_id, entries in groupby(items, key=lambda item: item["folderId"])
118
+ }
119
+
120
+ def _exec_with_session(self, command: str) -> str:
121
+ return self._exec(f"{command} --session '{self._key}'")
122
+
123
+ def has_folder(self, folder: str | None) -> bool:
124
+ return folder in self._folders
125
+
126
+ def _get_platform_dependent_echo_str(self, string: str) -> str:
127
+ if platform.system() == "Windows":
128
+ return f"echo {string}"
129
+ else:
130
+ return f"echo '{string}'"
131
+
132
+ def create_folder(self, folder: str | None) -> None:
133
+ if not folder or self.has_folder(folder):
134
+ return
135
+
136
+ data: dict[str, str] = {"name": folder}
137
+ data_b64 = base64.b64encode(json.dumps(data).encode("UTF-8")).decode("UTF-8")
138
+
139
+ output = self._exec_with_session(
140
+ f"{self._get_platform_dependent_echo_str(data_b64)} | bw create folder"
141
+ )
142
+
143
+ output_obj: dict[str, str] = json.loads(output)
144
+
145
+ self._folders[output_obj["name"]] = output_obj["id"]
146
+
147
+ def create_entry(self, folder: str | None, entry: dict[str, Any]) -> str:
148
+ # check if already exists
149
+ if (
150
+ folder in self._folder_entries
151
+ and entry["name"] in self._folder_entries[folder]
152
+ ):
153
+ logger.info(
154
+ f"-- Entry {entry['name']} already exists in folder {folder}. skipping..."
155
+ )
156
+ return "skip"
157
+
158
+ # create folder if exists
159
+ if folder:
160
+ self.create_folder(folder)
161
+
162
+ # set id
163
+ entry["folderId"] = self._folders[folder]
164
+
165
+ json_str = json.dumps(entry)
166
+
167
+ # convert string to base64
168
+ json_b64 = base64.b64encode(json_str.encode("UTF-8")).decode("UTF-8")
169
+
170
+ return self._exec_with_session(
171
+ f"{self._get_platform_dependent_echo_str(json_b64)} | bw create item"
172
+ )
173
+
174
+ def create_attachment(
175
+ self, item_id: str, attachment: tuple[str, str] | Attachment
176
+ ) -> str:
177
+ # store attachment on disk
178
+ filename: str
179
+ data: bytes
180
+ if not isinstance(attachment, Attachment):
181
+ # long custom property — tuple[str, str]
182
+ filename = attachment[0] + ".txt"
183
+ data = attachment[1].encode("UTF-8")
184
+ else:
185
+ # real kp attachment
186
+ if attachment.filename is None:
187
+ logger.warning("Attachment has no filename, using fallback")
188
+ filename = "attachment"
189
+ else:
190
+ filename = attachment.filename
191
+ data = attachment.data
192
+
193
+ # make sure temporary attachment folder exists
194
+ self._create_temporary_attachment_folder()
195
+
196
+ path_to_file_on_disk = os.path.join(self.TEMPORARY_ATTACHMENT_FOLDER, filename)
197
+ with open(path_to_file_on_disk, "wb") as f:
198
+ _ = f.write(data)
199
+
200
+ try:
201
+ output = self._exec_with_session(
202
+ f'bw create attachment --file "{path_to_file_on_disk}" --itemid {item_id}'
203
+ )
204
+ finally:
205
+ os.remove(path_to_file_on_disk)
206
+
207
+ return output
208
+
209
+ def create_org_get_collection(self, collectionname: str | None) -> str | None:
210
+ if not collectionname:
211
+ return None
212
+
213
+ if self._colls is None:
214
+ self._colls = {}
215
+
216
+ # check for existing
217
+ if self._colls.get(collectionname):
218
+ return self._colls.get(collectionname)
219
+
220
+ # get template
221
+ entry: dict[str, Any] = json.loads(
222
+ self._exec_with_session("bw get template org-collection")
223
+ )
224
+
225
+ # set org and Name
226
+ entry["name"] = collectionname
227
+ entry["organizationId"] = self._orgId
228
+
229
+ json_str = json.dumps(entry)
230
+
231
+ # convert string to base64
232
+ json_b64 = base64.b64encode(json_str.encode("UTF-8")).decode("UTF-8")
233
+
234
+ output = self._exec_with_session(
235
+ f"{self._get_platform_dependent_echo_str(json_b64)} | bw create org-collection --organizationid {self._orgId}"
236
+ )
237
+ if not output:
238
+ return None
239
+ data: dict[str, Any] = json.loads(output)
240
+ if not data["id"]:
241
+ return None
242
+ new_coll_id: str = data["id"]
243
+
244
+ # store in cache
245
+ self._colls[collectionname] = new_coll_id
246
+
247
+ return new_coll_id