girder-jsonforms 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.
Files changed (26) hide show
  1. girder_jsonforms-2.0.0/.github/workflows/release.yml +102 -0
  2. girder_jsonforms-2.0.0/.gitignore +133 -0
  3. girder_jsonforms-2.0.0/LICENSE +27 -0
  4. girder_jsonforms-2.0.0/PKG-INFO +33 -0
  5. girder_jsonforms-2.0.0/README.md +7 -0
  6. girder_jsonforms-2.0.0/girder_jsonforms/__init__.py +83 -0
  7. girder_jsonforms-2.0.0/girder_jsonforms/lib/__init__.py +0 -0
  8. girder_jsonforms-2.0.0/girder_jsonforms/lib/google_drive.py +157 -0
  9. girder_jsonforms-2.0.0/girder_jsonforms/lib/jq.py +134 -0
  10. girder_jsonforms-2.0.0/girder_jsonforms/models/__init__.py +0 -0
  11. girder_jsonforms-2.0.0/girder_jsonforms/models/entry.py +245 -0
  12. girder_jsonforms-2.0.0/girder_jsonforms/models/form.py +287 -0
  13. girder_jsonforms-2.0.0/girder_jsonforms/rest/__init__.py +0 -0
  14. girder_jsonforms-2.0.0/girder_jsonforms/rest/entry.py +126 -0
  15. girder_jsonforms-2.0.0/girder_jsonforms/rest/form.py +362 -0
  16. girder_jsonforms-2.0.0/girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs +88 -0
  17. girder_jsonforms-2.0.0/girder_jsonforms/web_client/dist/style.css +1 -0
  18. girder_jsonforms-2.0.0/girder_jsonforms.egg-info/PKG-INFO +33 -0
  19. girder_jsonforms-2.0.0/girder_jsonforms.egg-info/SOURCES.txt +24 -0
  20. girder_jsonforms-2.0.0/girder_jsonforms.egg-info/dependency_links.txt +1 -0
  21. girder_jsonforms-2.0.0/girder_jsonforms.egg-info/entry_points.txt +2 -0
  22. girder_jsonforms-2.0.0/girder_jsonforms.egg-info/not-zip-safe +1 -0
  23. girder_jsonforms-2.0.0/girder_jsonforms.egg-info/requires.txt +4 -0
  24. girder_jsonforms-2.0.0/girder_jsonforms.egg-info/top_level.txt +1 -0
  25. girder_jsonforms-2.0.0/setup.cfg +4 -0
  26. girder_jsonforms-2.0.0/setup.py +35 -0
@@ -0,0 +1,102 @@
1
+ name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
2
+
3
+ on: push
4
+
5
+ jobs:
6
+ build:
7
+ name: Build distribution 📦
8
+ runs-on: ubuntu-latest
9
+
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - name: Set up Python
13
+ uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.x"
16
+ - name: Set up Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: "18"
20
+ - name: Install pypa/build
21
+ run: >-
22
+ python3 -m
23
+ pip install
24
+ build
25
+ --user
26
+ - name: Build js component in package/web_client
27
+ run: >-
28
+ cd $(find . -maxdepth 2 -mindepth 2 -type d -name web_client)
29
+ && npm ci
30
+ && npm run build
31
+ - name: Build a binary wheel and a source tarball
32
+ run: python3 -m build
33
+ - name: install check-manifest
34
+ run: >-
35
+ python3 -m
36
+ pip install
37
+ check-manifest
38
+ --user
39
+ - name: Check the package manifest
40
+ run: check-manifest -vvv
41
+ - name: Store the distribution packages
42
+ uses: actions/upload-artifact@v4
43
+ with:
44
+ name: python-package-distributions
45
+ path: dist/
46
+
47
+ publish-to-pypi:
48
+ name: >-
49
+ Publish Python 🐍 distribution 📦 to PyPI
50
+ if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes
51
+ needs:
52
+ - build
53
+ runs-on: ubuntu-latest
54
+ environment:
55
+ name: pypi
56
+ url: https://pypi.org/p/girder-jsonforms
57
+ permissions:
58
+ id-token: write # IMPORTANT: mandatory for trusted publishing
59
+
60
+ steps:
61
+ - name: Download all the dists
62
+ uses: actions/download-artifact@v4
63
+ with:
64
+ name: python-package-distributions
65
+ path: dist/
66
+ - name: Publish distribution 📦 to PyPI
67
+ uses: pypa/gh-action-pypi-publish@release/v1
68
+
69
+ github-release:
70
+ name: >-
71
+ Sign the Python 🐍 distribution 📦 with Sigstore
72
+ and upload them to GitHub Release
73
+ needs:
74
+ - publish-to-pypi
75
+ runs-on: ubuntu-latest
76
+
77
+ permissions:
78
+ contents: write # IMPORTANT: mandatory for making GitHub Releases
79
+ id-token: write # IMPORTANT: mandatory for sigstore
80
+
81
+ steps:
82
+ - name: Download all the dists
83
+ uses: actions/download-artifact@v4
84
+ with:
85
+ name: python-package-distributions
86
+ path: dist/
87
+ - name: Sign the dists with Sigstore
88
+ uses: sigstore/gh-action-sigstore-python@v3.0.0
89
+ with:
90
+ inputs: >-
91
+ ./dist/*.tar.gz
92
+ ./dist/*.whl
93
+ - name: Upload artifact signatures to GitHub Release
94
+ env:
95
+ GITHUB_TOKEN: ${{ github.token }}
96
+ # Upload to GitHub Release using the `gh` CLI.
97
+ # `dist/` contains the built packages, and the
98
+ # sigstore-produced signatures and certificates.
99
+ run: >-
100
+ gh release upload
101
+ '${{ github.ref_name }}' dist/**
102
+ --repo '${{ github.repository }}'
@@ -0,0 +1,133 @@
1
+ Testing
2
+
3
+ *ala*
4
+
5
+ # Byte-compiled / optimized / DLL files
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+
10
+ # C extensions
11
+ *.so
12
+
13
+ # Distribution / packaging
14
+ .Python
15
+ build/
16
+ develop-eggs/
17
+ dist/
18
+ downloads/
19
+ eggs/
20
+ .eggs/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ pip-wheel-metadata/
26
+ share/python-wheels/
27
+ *.egg-info/
28
+ .installed.cfg
29
+ *.egg
30
+ MANIFEST
31
+
32
+ # PyInstaller
33
+ # Usually these files are written by a python script from a template
34
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
35
+ *.manifest
36
+ *.spec
37
+
38
+ # Installer logs
39
+ pip-log.txt
40
+ pip-delete-this-directory.txt
41
+
42
+ # Unit test / coverage reports
43
+ htmlcov/
44
+ .tox/
45
+ .nox/
46
+ .coverage
47
+ .coverage.*
48
+ .cache
49
+ nosetests.xml
50
+ coverage.xml
51
+ *.cover
52
+ *.py,cover
53
+ .hypothesis/
54
+ .pytest_cache/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Django stuff:
61
+ *.log
62
+ local_settings.py
63
+ db.sqlite3
64
+ db.sqlite3-journal
65
+
66
+ # Flask stuff:
67
+ instance/
68
+ .webassets-cache
69
+
70
+ # Scrapy stuff:
71
+ .scrapy
72
+
73
+ # Sphinx documentation
74
+ docs/_build/
75
+
76
+ # PyBuilder
77
+ target/
78
+
79
+ # Jupyter Notebook
80
+ .ipynb_checkpoints
81
+
82
+ # IPython
83
+ profile_default/
84
+ ipython_config.py
85
+
86
+ # pyenv
87
+ .python-version
88
+
89
+ # pipenv
90
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
91
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
92
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
93
+ # install all needed dependencies.
94
+ #Pipfile.lock
95
+
96
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
97
+ __pypackages__/
98
+
99
+ # Celery stuff
100
+ celerybeat-schedule
101
+ celerybeat.pid
102
+
103
+ # SageMath parsed files
104
+ *.sage.py
105
+
106
+ # Environments
107
+ .env
108
+ .venv
109
+ env/
110
+ venv/
111
+ ENV/
112
+ env.bak/
113
+ venv.bak/
114
+
115
+ # Spyder project settings
116
+ .spyderproject
117
+ .spyproject
118
+
119
+ # Rope project settings
120
+ .ropeproject
121
+
122
+ # mkdocs documentation
123
+ /site
124
+
125
+ # mypy
126
+ .mypy_cache/
127
+ .dmypy.json
128
+ dmypy.json
129
+
130
+ # Pyre type checker
131
+ .pyre/
132
+
133
+ node_modules/
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2024, data-exp-lab
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of girder_wholetale nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.2
2
+ Name: girder-jsonforms
3
+ Version: 2.0.0
4
+ Summary: Girder plugin adding forms based on JSON-editor
5
+ License: BSD-3-Clause
6
+ Classifier: Development Status :: 4 - Beta
7
+ Classifier: Environment :: Web Environment
8
+ Classifier: License :: OSI Approved :: BSD License
9
+ Classifier: Operating System :: POSIX :: Linux
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: girder>=5.0.0a5.dev0
16
+ Requires-Dist: google-api-python-client
17
+ Requires-Dist: google-auth-oauthlib
18
+ Requires-Dist: pandas
19
+ Dynamic: classifier
20
+ Dynamic: description
21
+ Dynamic: description-content-type
22
+ Dynamic: license
23
+ Dynamic: requires-dist
24
+ Dynamic: requires-python
25
+ Dynamic: summary
26
+
27
+ ### Girder JSONForms Plugin
28
+
29
+ This plugin provides a JSON Forms support for Girder. It is based on the [json-editor](https://github.com/json-editor/json-editor) library. The plugin allows to create JSON forms for data input and editing.
30
+
31
+ ## Usage
32
+
33
+ The plugin extends girder with two models: `Form` and `FormEntry` (with REST routes `/form` and `/entry` respectively). The `Form` model is used to define the form schema and the `FormEntry` model is used to store the form data. `FormEntries` are primarily kept in girder's mongo database, but can also be stored as JSON dumps in girder hierarchy and/or in Google Drive. The plugin provides a REST API to create, update, delete and retrieve forms and form entries. The plugin also provides a web UI component to fill and view form entries. Form schema is expected to have a single unique key field, which is used to identify the form entry (`uniqueField` key on a Form).
@@ -0,0 +1,7 @@
1
+ ### Girder JSONForms Plugin
2
+
3
+ This plugin provides a JSON Forms support for Girder. It is based on the [json-editor](https://github.com/json-editor/json-editor) library. The plugin allows to create JSON forms for data input and editing.
4
+
5
+ ## Usage
6
+
7
+ The plugin extends girder with two models: `Form` and `FormEntry` (with REST routes `/form` and `/entry` respectively). The `Form` model is used to define the form schema and the `FormEntry` model is used to store the form data. `FormEntries` are primarily kept in girder's mongo database, but can also be stored as JSON dumps in girder hierarchy and/or in Google Drive. The plugin provides a REST API to create, update, delete and retrieve forms and form entries. The plugin also provides a web UI component to fill and view form entries. Form schema is expected to have a single unique key field, which is used to identify the form entry (`uniqueField` key on a Form).
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ import json
4
+ import logging
5
+ import os
6
+
7
+ from girder import events
8
+ from girder.constants import AccessType
9
+ from girder.models.file import File
10
+ from girder.models.item import Item
11
+ from girder.plugin import GirderPlugin, registerPluginStaticContent
12
+ from girder.utility.model_importer import ModelImporter
13
+
14
+ from .lib.google_drive import authenticate_gdrive, upload_file_to_gdrive
15
+ from .models.entry import FormEntry as FormEntryModel
16
+ from .models.form import Form as FormModel
17
+ from .rest.entry import FormEntry
18
+ from .rest.form import Form
19
+
20
+ GDRIVE_SERVICE = None
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def annotate_uploads(event):
25
+ info = event.info
26
+ if "reference" not in info:
27
+ return
28
+
29
+ try:
30
+ reference = json.loads(info["reference"])
31
+ except (ValueError, TypeError):
32
+ return
33
+
34
+ if reference.get("annotate"):
35
+ parent = Item().load(
36
+ info["file"]["itemId"], level=AccessType.WRITE, user=info["currentUser"]
37
+ )
38
+ reference.pop("file", None)
39
+ reference.pop("annotate", None)
40
+ Item().setMetadata(parent, reference)
41
+
42
+
43
+ def upload_to_gdrive(event):
44
+ global GDRIVE_SERVICE
45
+ info = event.info
46
+ file = info["file"]
47
+
48
+ with File().open(file) as fh:
49
+ gdrive_file_id = upload_file_to_gdrive(
50
+ GDRIVE_SERVICE,
51
+ info["gdriveFolderId"],
52
+ info["path"],
53
+ fh,
54
+ mimetype=file["mimeType"],
55
+ )
56
+ parent = Item().load(
57
+ file["itemId"], level=AccessType.WRITE, user=info["currentUser"]
58
+ )
59
+ Item().setMetadata(parent, {"gdriveFileId": gdrive_file_id})
60
+
61
+
62
+ class JSONFormsPlugin(GirderPlugin):
63
+ DISPLAY_NAME = "JSON Forms"
64
+
65
+ def load(self, info):
66
+ ModelImporter.registerModel("form", FormModel, plugin="jsonforms")
67
+ ModelImporter.registerModel("entry", FormEntryModel, plugin="jsonforms")
68
+ global GDRIVE_SERVICE
69
+ try:
70
+ GDRIVE_SERVICE = authenticate_gdrive()
71
+ except ValueError:
72
+ logger.exception("Failed to authenticate with Google Drive")
73
+ info["apiRoot"].form = Form()
74
+ info["apiRoot"].entry = FormEntry()
75
+ events.bind("data.process", "jsonforms", annotate_uploads)
76
+ events.bind("gdrive.upload", "jsonforms", upload_to_gdrive)
77
+ registerPluginStaticContent(
78
+ plugin="jsonforms",
79
+ css=["/style.css"],
80
+ js=["/girder-plugin-jsonforms.umd.cjs"],
81
+ staticDir=os.path.join(os.path.dirname(__file__), "web_client", "dist"),
82
+ tree=info["serverRoot"],
83
+ )
@@ -0,0 +1,157 @@
1
+ import os
2
+ import pickle
3
+ import logging
4
+
5
+ from google.auth.transport.requests import Request
6
+
7
+ # from google_auth_oauthlib.flow import InstalledAppFlow
8
+ from googleapiclient.discovery import build
9
+ from googleapiclient.http import MediaIoBaseUpload
10
+
11
+ logger = logging.getLogger("google_drive")
12
+ logprint = logger
13
+ # If modifying these scopes, delete the file token.pickle.
14
+ SCOPES = ["https://www.googleapis.com/auth/drive"]
15
+
16
+
17
+ def get_credentials():
18
+ """Gets valid user credentials from storage.
19
+
20
+ If nothing has been stored, or if the stored credentials are invalid,
21
+ the OAuth2 flow is completed to obtain the new credentials.
22
+
23
+ Returns:
24
+ Credentials, the obtained credential.
25
+ """
26
+ creds = None
27
+ # The file token.pickle stores the user's access and refresh tokens, and is
28
+ # created automatically when the authorization flow completes for the first
29
+ # time.
30
+
31
+ path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "token.pickle")
32
+
33
+ if os.path.exists(path):
34
+ with open(path, "rb") as token:
35
+ creds = pickle.load(token)
36
+ # If there are no (valid) credentials available, let the user log in.
37
+ if not creds or not creds.valid:
38
+ if creds and creds.expired and creds.refresh_token:
39
+ creds.refresh(Request())
40
+ else:
41
+ raise ValueError("No valid credentials found")
42
+ # flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
43
+ # creds = flow.run_local_server(port=0)
44
+ # Save the credentials for the next run
45
+ with open(path, "wb") as token:
46
+ pickle.dump(creds, token)
47
+ return creds
48
+
49
+
50
+ def authenticate_gdrive():
51
+ # Load credentials from a file or any other secure source
52
+ # Replace 'path_to_credentials.json' with the actual path to your credentials file
53
+ credentials = get_credentials()
54
+ return build("drive", "v3", credentials=credentials)
55
+
56
+
57
+ def create_folders(service, folder_id, path):
58
+ if service is None:
59
+ logger.exception("Service not initialized. Failed to create folder.")
60
+ return
61
+ current_folder_id = folder_id
62
+ folders = path.split("/")
63
+ for folder in folders:
64
+ if folder:
65
+ # Check if folder already exists
66
+ query = f"'{current_folder_id}' in parents and mimeType='application/vnd.google-apps.folder' and name='{folder}' and trashed=false"
67
+ response = (
68
+ service.files()
69
+ .list(
70
+ q=query,
71
+ fields="files(id)",
72
+ includeItemsFromAllDrives=True,
73
+ supportsAllDrives=True,
74
+ )
75
+ .execute()
76
+ )
77
+ existing_folders = response.get("files", [])
78
+ if existing_folders:
79
+ logprint.error(f"Folder '{folder}' already exists")
80
+ logprint.error(response)
81
+ current_folder_id = existing_folders[0]["id"]
82
+ else:
83
+ logprint.error(f"Creating folder '{folder}'")
84
+ file_metadata = {
85
+ "name": folder,
86
+ "mimeType": "application/vnd.google-apps.folder",
87
+ "parents": [current_folder_id],
88
+ }
89
+ folder_result = (
90
+ service.files()
91
+ .create(body=file_metadata, fields="id", supportsAllDrives=True)
92
+ .execute()
93
+ )
94
+ logprint.error(
95
+ f"Folder '{folder}' created with ID: {folder_result['id']}"
96
+ )
97
+ current_folder_id = folder_result["id"]
98
+
99
+ return current_folder_id
100
+
101
+
102
+ def upload_file_to_gdrive(service, folder_id, path, file_handle, mimetype="text/plain"):
103
+ if service is None:
104
+ logger.exception("Service not initialized. Failed to upload file.")
105
+ return
106
+ file_path, file_name = os.path.split(path)
107
+ folder_id = create_folders(service, folder_id, file_path)
108
+ logprint.error(f"Uploading file '{file_name}' to folder '{folder_id}'")
109
+
110
+ # Check if file already exists in the folder
111
+ query = f"'{folder_id}' in parents and name='{file_name}' and trashed=false"
112
+ response = (
113
+ service.files()
114
+ .list(
115
+ q=query,
116
+ fields="files(id)",
117
+ includeItemsFromAllDrives=True,
118
+ supportsAllDrives=True,
119
+ )
120
+ .execute()
121
+ )
122
+ existing_files = response.get("files", [])
123
+ media = MediaIoBaseUpload(file_handle, mimetype=mimetype)
124
+ if existing_files:
125
+ file_id = existing_files[0]["id"]
126
+ file_result = (
127
+ service.files()
128
+ .update(fileId=file_id, media_body=media, supportsAllDrives=True)
129
+ .execute()
130
+ )
131
+ logprint.error(f'File {file_name} updated with ID: {file_result["id"]}')
132
+ else:
133
+ file_metadata = {"name": file_name, "parents": [folder_id]}
134
+ file_result = (
135
+ service.files()
136
+ .create(
137
+ body=file_metadata,
138
+ media_body=media,
139
+ fields="id",
140
+ supportsAllDrives=True,
141
+ )
142
+ .execute()
143
+ )
144
+ logprint.error(
145
+ f'File {file_metadata["name"]} uploaded with ID: {file_result["id"]}'
146
+ )
147
+ return file_result["id"]
148
+
149
+
150
+ # Example usage
151
+ if __name__ == "__main__":
152
+ # folder_id = "1WgY3HJ0bxzQc-IA_dBdiwb9Hvq6ho3HS" # Replace with the actual folder ID
153
+ folder_id = "13u4Citrr09FETrPDn2gJmBHWeaO0ylji"
154
+ path = "subfolder/subfolder2/subfolder4/file.txt"
155
+ file_handle = open("file.txt", "rb") # Replace 'file.txt' with the actual file path
156
+ service = authenticate_gdrive()
157
+ file_id = upload_file_to_gdrive(service, folder_id, path, file_handle)
@@ -0,0 +1,134 @@
1
+ from typing import Any
2
+
3
+ # find all occurences of a key in a nested json
4
+ def find_key_paths(json_data: dict, key: str, path: str = "") -> list[str]:
5
+ results = []
6
+ # Check if the input is a dictionary
7
+ if isinstance(json_data, dict):
8
+ # If the key is found, return the path to it
9
+ if key in json_data:
10
+ results.append(path + key)
11
+ # Recursively search through each value in the dictionary
12
+ for k, v in json_data.items():
13
+ results.extend(find_key_paths(v, key, path + k + "."))
14
+ # Check if the input is a list
15
+ elif isinstance(json_data, list):
16
+ # Recursively search through each item in the list
17
+ for i, item in enumerate(json_data):
18
+ if isinstance(item, dict):
19
+ results.extend(find_key_paths(item, key, path + f"[{i}]."))
20
+ elif isinstance(item, list):
21
+ for j, sub_item in enumerate(item):
22
+ results.extend(find_key_paths(sub_item, key, path + f"[{i}][{j}]."))
23
+ # Return None if the key is not found
24
+ return results
25
+
26
+
27
+ # method to change the value of the key using key notation .[]
28
+ def get_value(json_data: dict, key: str) -> dict:
29
+ for path in key.split("."):
30
+ if path.startswith("["):
31
+ index = int(path[1:-1])
32
+ json_data = json_data[index]
33
+ else:
34
+ json_data = json_data[path]
35
+ return json_data
36
+
37
+
38
+ def set_value(json_data: dict, key: str, value: Any) -> None:
39
+ for path in key.split(".")[:-1]:
40
+ if path.startswith("["):
41
+ index = int(path[1:-1])
42
+ json_data = json_data[index]
43
+ else:
44
+ json_data = json_data[path]
45
+ path = key.split(".")[-1]
46
+ json_data[path] = value
47
+
48
+
49
+ def convert_to_jq_notation(data: dict, parent_key: str = "", sep: str = ".") -> dict:
50
+ """
51
+ Convert a nested dictionary or list into a flat dictionary with jq-style keys.
52
+
53
+ Args:
54
+ data (dict or list): The nested dictionary or list to flatten.
55
+ parent_key (str): The base key for recursion (used internally).
56
+ sep (str): The separator to use for keys (default is ".").
57
+
58
+ Returns:
59
+ dict: A flattened dictionary with jq-like notation keys.
60
+ """
61
+ items = []
62
+
63
+ if isinstance(data, dict):
64
+ for key, value in data.items():
65
+ new_key = f"{parent_key}{sep}{key}" if parent_key else key
66
+ items.extend(convert_to_jq_notation(value, new_key, sep).items())
67
+ elif isinstance(data, list):
68
+ for index, value in enumerate(data):
69
+ new_key = f"{parent_key}{sep}[{index}]"
70
+ items.extend(convert_to_jq_notation(value, new_key, sep).items())
71
+ else:
72
+ items.append((parent_key, data))
73
+
74
+ return dict(items)
75
+
76
+
77
+ def parse_jq_notation(input_dict: dict) -> dict:
78
+ """
79
+ Convert a dictionary with jq-style keys into a nested dictionary.
80
+
81
+ Args:
82
+ input_dict (dict): The dictionary with jq-style keys.
83
+
84
+ Returns:
85
+ dict: A nested dictionary.
86
+ """
87
+
88
+ def is_array_index(key):
89
+ return key.startswith("[") and key.endswith("]") and key[1:-1].isdigit()
90
+
91
+ output = {}
92
+
93
+ for key, value in input_dict.items():
94
+ keys = key.split(".")
95
+ current = output
96
+
97
+ for i, part in enumerate(keys[:-1]):
98
+ if is_array_index(part):
99
+ # Convert string digits to integers for lists
100
+ part = int(part.strip("[]"))
101
+
102
+ if "nestedlist" in key:
103
+ print(current, key, part)
104
+ if isinstance(part, int):
105
+ # If current is not a list, convert it into one
106
+ if not isinstance(current, list):
107
+ current[keys[i - 1]] = []
108
+ current = current[keys[i - 1]]
109
+ # Extend the list to ensure the index exists
110
+
111
+ if i + 1 <= len(keys[:-1]) and is_array_index(keys[i+1]):
112
+ append_object = []
113
+ else:
114
+ append_object = {}
115
+
116
+ while len(current) <= part:
117
+ # It has to be a placeholder for dictionary or list
118
+ current.append(append_object)
119
+ current = current[part]
120
+ else:
121
+ if part not in current:
122
+ # Create a list if the next key is a digit
123
+ next_part = keys[i + 1]
124
+ current[part] = [] if is_array_index(next_part) else {}
125
+ current = current[part]
126
+
127
+ # Handle the last key
128
+ last_key = keys[-1]
129
+ if is_array_index(last_key):
130
+ current.append(value)
131
+ else:
132
+ current[last_key] = value
133
+
134
+ return output