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.
- girder_jsonforms-2.0.0/.github/workflows/release.yml +102 -0
- girder_jsonforms-2.0.0/.gitignore +133 -0
- girder_jsonforms-2.0.0/LICENSE +27 -0
- girder_jsonforms-2.0.0/PKG-INFO +33 -0
- girder_jsonforms-2.0.0/README.md +7 -0
- girder_jsonforms-2.0.0/girder_jsonforms/__init__.py +83 -0
- girder_jsonforms-2.0.0/girder_jsonforms/lib/__init__.py +0 -0
- girder_jsonforms-2.0.0/girder_jsonforms/lib/google_drive.py +157 -0
- girder_jsonforms-2.0.0/girder_jsonforms/lib/jq.py +134 -0
- girder_jsonforms-2.0.0/girder_jsonforms/models/__init__.py +0 -0
- girder_jsonforms-2.0.0/girder_jsonforms/models/entry.py +245 -0
- girder_jsonforms-2.0.0/girder_jsonforms/models/form.py +287 -0
- girder_jsonforms-2.0.0/girder_jsonforms/rest/__init__.py +0 -0
- girder_jsonforms-2.0.0/girder_jsonforms/rest/entry.py +126 -0
- girder_jsonforms-2.0.0/girder_jsonforms/rest/form.py +362 -0
- girder_jsonforms-2.0.0/girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs +88 -0
- girder_jsonforms-2.0.0/girder_jsonforms/web_client/dist/style.css +1 -0
- girder_jsonforms-2.0.0/girder_jsonforms.egg-info/PKG-INFO +33 -0
- girder_jsonforms-2.0.0/girder_jsonforms.egg-info/SOURCES.txt +24 -0
- girder_jsonforms-2.0.0/girder_jsonforms.egg-info/dependency_links.txt +1 -0
- girder_jsonforms-2.0.0/girder_jsonforms.egg-info/entry_points.txt +2 -0
- girder_jsonforms-2.0.0/girder_jsonforms.egg-info/not-zip-safe +1 -0
- girder_jsonforms-2.0.0/girder_jsonforms.egg-info/requires.txt +4 -0
- girder_jsonforms-2.0.0/girder_jsonforms.egg-info/top_level.txt +1 -0
- girder_jsonforms-2.0.0/setup.cfg +4 -0
- 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
|
+
)
|
|
File without changes
|
|
@@ -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
|
|
File without changes
|