Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
157 commits
Select commit Hold shift + click to select a range
930cf48
fix: correct resource name
Xarthisius Feb 16, 2025
ad2d199
Add wip
Xarthisius Feb 17, 2025
0ec72ed
add basic list view
Xarthisius Feb 17, 2025
3f84503
Add deposition view. Add alternate identifiers to UI
Xarthisius Feb 18, 2025
1629edf
add bootstrap-autocomplete
Xarthisius Feb 19, 2025
135b966
make radio button in add creator actually work
Xarthisius Feb 19, 2025
b5c0899
Add endpoint for ORCID search
Xarthisius Feb 19, 2025
f01ad4d
Magical creators list
Xarthisius Feb 19, 2025
1240feb
Move parts of deposition form to subwidgets to avoid resetting state
Xarthisius Feb 21, 2025
d896e1e
Add fancy searching
Xarthisius Feb 22, 2025
b596602
Display more data in deposition view
Xarthisius Feb 22, 2025
c641eaf
Add DELETE /deposition for admin
Xarthisius Feb 24, 2025
89b3f00
Store creator in entry. Make folderId optional
Xarthisius Feb 24, 2025
e600437
Make girder.formId enumSource even more magical
Xarthisius Feb 24, 2025
f134712
Add relations to deposition whenever an entry referencing it is created
Xarthisius Feb 24, 2025
e15b596
Update UI code
Xarthisius Feb 24, 2025
2c0cd9a
Don't die if source/destination doesn't exist for a form
Xarthisius Feb 24, 2025
3b6f0a0
fix: export uses correct qparam now
Xarthisius Feb 26, 2025
7c115cc
Add dependency for openpyxl for xlsx export/import
Xarthisius Feb 27, 2025
53d04a2
Add missing settings import
Xarthisius Feb 27, 2025
c4a07b6
Only show nav entries for logged users
Xarthisius Feb 27, 2025
eaba216
update methods names
Xarthisius Mar 4, 2025
41a059d
first working iteration of weihs lab
Xarthisius Mar 11, 2025
0388120
Add ACLs and improve UI links
Xarthisius Mar 25, 2025
afef2e6
Add autocreation of sample trackers
Xarthisius Mar 25, 2025
1b76389
Copy form access to derived depositions
Xarthisius Mar 25, 2025
66b90d7
add deposition editing
Xarthisius Mar 31, 2025
3ff9391
Use girder exported jQuery
Xarthisius Mar 31, 2025
6c41005
Add patch for json-editor
Xarthisius Mar 31, 2025
e3b6540
fix autocomplete
Xarthisius Apr 2, 2025
2e128b3
bump version
Xarthisius Apr 2, 2025
766fa1f
test: clean up plugins after gdrive test
Xarthisius Apr 2, 2025
9f4acb1
Add tests for Weihs Lab magic
Xarthisius Apr 2, 2025
c7a4118
Form should be owned by the creator
Xarthisius Apr 2, 2025
0b7351f
remove unused import
Xarthisius Apr 2, 2025
1f82b38
Drop tests from coverage
Xarthisius Apr 3, 2025
03334e3
fix typo
Xarthisius Apr 3, 2025
5ea1170
do not include coveragerc in the package
Xarthisius Apr 3, 2025
913e60e
Navigate back to form instead of forms after entry
Xarthisius Apr 3, 2025
0016d3d
Add necessary bits for IMQCAM use case
Xarthisius Apr 3, 2025
c18116e
Moar tests...
Xarthisius Apr 4, 2025
237c347
Remove async daemon event handler
Xarthisius Apr 7, 2025
26af1a0
Allow to provide js code for eval instead of hardcoding everything in…
Xarthisius Apr 10, 2025
8d36e7d
Use GET /entry/search for autocomplete
Xarthisius Apr 10, 2025
e053bff
Add preloading capability
Xarthisius Apr 10, 2025
9bbebd2
Allow form to provide igsn metadata
Xarthisius Apr 11, 2025
a8f7349
Fix regex for related entries in the deposition view
Xarthisius Apr 11, 2025
fb1a889
Allow to filter GET /entry by field values
Xarthisius Apr 14, 2025
ec393f2
update for htmax powder forms
Xarthisius Apr 21, 2025
3a16931
Handle description as an array
Xarthisius Apr 22, 2025
c0cb5bb
Add a link to sample tracker in deposition view
Xarthisius Apr 22, 2025
5a7f207
Add searching by IGSN. Show related data on deposition view
Xarthisius Apr 22, 2025
cf297e1
IGSN are indexed from 1
Xarthisius Apr 23, 2025
736fceb
Add filter to IGSN list view
Xarthisius Apr 23, 2025
f52edc9
Update weihs batch treatment
Xarthisius Apr 23, 2025
67f83ec
Only display link to sample tracker if it exists
Xarthisius Apr 23, 2025
cb11ee5
Fix sample names in batch mode
Xarthisius Apr 23, 2025
e809099
Search IGSNs by text
Xarthisius Apr 23, 2025
eb7e507
Add IGSN link in ItemView
Xarthisius Apr 23, 2025
4d974b8
Make depositions clickable again...
Xarthisius Apr 23, 2025
0e32a49
Fix searching by altID
Xarthisius Apr 23, 2025
08ce10a
Escape slashes in IGSN link
Xarthisius Apr 23, 2025
600ca33
add support for attributes/relIds defined in igsnMeta
Xarthisius Apr 24, 2025
d7d48ce
Don't set upload field with irrelevant string
Xarthisius Apr 24, 2025
f1ee018
Add UMass as valid IGSN prefix
Xarthisius Apr 24, 2025
6320a60
Use '-' instead of '/' for child IGSNs
Xarthisius Apr 24, 2025
ff1516d
Switch form schema to body
Xarthisius May 1, 2025
192804f
Fetch names for entries/forms in Deposition view
Xarthisius May 6, 2025
d9c5355
StaticDir has to be a Path
Xarthisius May 12, 2025
e1b8d09
Ensure that metadata is deepcopied in batch creation
Xarthisius May 13, 2025
4253ff3
Make sure Deposition is initialized
Xarthisius May 29, 2025
0935697
Remove relatedIdentifier if entry is removed
Xarthisius May 29, 2025
60fd92f
Add import file handling on per form basis
Xarthisius Jun 11, 2025
9ea5c5f
Freeze submit button after request is sent. Fixes #24
Xarthisius Jun 11, 2025
badb2f8
Fix ACL propagation to sample tracker
Xarthisius Jun 12, 2025
85391dc
Fix styling on small screen
Xarthisius Jun 16, 2025
f174eec
Re-enable submit button on failed form validation
Xarthisius Jun 16, 2025
ad49142
Provide 'name' field for search results
Xarthisius Jun 16, 2025
f6b40ef
Handle lack of attributes
Xarthisius Jun 16, 2025
43b4c90
Trace form entries changes
Xarthisius Jun 17, 2025
493e4dc
Add IsDerivedFrom/IsSourceOf handling for HTMAX ceramic powders
Xarthisius Jun 25, 2025
31f1728
Allow to create sample tracker with new deposition
Xarthisius Jul 2, 2025
f2f5d38
Validate creator and remove debug
Xarthisius Jul 2, 2025
6165aba
Rearrange form order
Xarthisius Jul 2, 2025
b4b9eca
Handle adding/removing sample id from deposition
Xarthisius Jul 2, 2025
b6d6cc0
Add Related IDs to deposition edit view
Xarthisius Jul 3, 2025
5420d8b
Make dropdowns required
Xarthisius Jul 3, 2025
c5f848e
Re-add delete button on Entry view.
Xarthisius Jul 3, 2025
47e54e0
Allow to create batch IGSNs
Xarthisius Jul 11, 2025
8083645
Propagate ACLs to sample if recurse is true
Xarthisius Jul 11, 2025
33f10f5
Update Weihs lab test
Xarthisius Jul 16, 2025
13d1da9
Show link to IGSN on entry form
Xarthisius Jul 18, 2025
1ad242e
Prevent creating new entries based on old if IGSN is assigned
Xarthisius Jul 18, 2025
600846a
Don't enforce uppercase for QR payload.
Xarthisius Jul 21, 2025
70b8b5e
Bump version of QRparams to support a-z
Xarthisius Jul 21, 2025
bc52315
Filter relatedIdentifers per ACLs
Xarthisius Jul 22, 2025
a1b7bf0
Fix batch input value in Deposition form
Xarthisius Jul 22, 2025
fc81785
Don't leak sampleId from Deposition if ACLs are not sufficient
Xarthisius Jul 22, 2025
2eaa240
Allow to modify jsHelpers via PUT
Xarthisius Jul 22, 2025
e4827d5
Validate that schema is JSON
Xarthisius Jul 22, 2025
9662bbd
Add method for creating sub-sample
Xarthisius Jul 23, 2025
df9038b
Add UI for adding child IGSNs
Xarthisius Jul 23, 2025
02a1c44
Validate IGSN metadata against datacite v4.5 schema
Xarthisius Jul 24, 2025
9ba1225
Handle affiliations via ROR
Xarthisius Jul 24, 2025
91b6ce5
Only use ORCID if it is actually there
Xarthisius Jul 24, 2025
9c1111e
Drop debug
Xarthisius Jul 24, 2025
bcf8076
Allow to search deposition by sampleId
Xarthisius Jul 24, 2025
561d337
Separate update from creation of entry
Xarthisius Jul 25, 2025
0ce1f02
Improve form tests
Xarthisius Jul 25, 2025
7599640
Add tests for entries
Xarthisius Jul 25, 2025
842c0ab
Add tests for depositions
Xarthisius Jul 25, 2025
d8fa60f
Display org in IGSN view
Xarthisius Jul 31, 2025
b4c3478
Add search 'by Creator'
Xarthisius Aug 6, 2025
940b43c
Add UI for byCreator search
Xarthisius Aug 6, 2025
6cc5469
Bump mongo
Xarthisius Aug 6, 2025
6f984ec
Autodelete _temp folder after 1h
Xarthisius Aug 7, 2025
0c09c33
Don't try to create deposition with duplicate igsn
Xarthisius Aug 12, 2025
9579b3d
Add sorting widget to deposition list
Xarthisius Aug 12, 2025
0718586
Foolproof deposition template
Xarthisius Aug 12, 2025
8d0e441
Allow to filter IGSNs by ACL
Xarthisius Aug 13, 2025
f81d9de
Add sort widget to folder list
Xarthisius Aug 18, 2025
fd0cd7c
Add persistent settings for sorting IGSNs
Xarthisius Aug 19, 2025
ecbca3f
Linter
Xarthisius Aug 19, 2025
d820bdb
Handle relatedIdentifiers for enumSource automatically
Xarthisius Aug 26, 2025
7e39591
Add method for IMQCAM/croom
Xarthisius Aug 27, 2025
49819f8
Simplify field setting for form upload
Xarthisius Aug 28, 2025
95d124a
Style form view/download links
Xarthisius Aug 28, 2025
0551744
Fix sort widget
Xarthisius Aug 29, 2025
a2ce438
Use proper format for publisher
Xarthisius Aug 29, 2025
29a7490
Make IGSN route public
Xarthisius Sep 4, 2025
1a2d0c8
update defaults for provider/publisher/prefix
Xarthisius Sep 4, 2025
06cef32
Add missing 'doi' and 'url' to deposition's meta
Xarthisius Sep 5, 2025
ec9d2e5
SubmitDate has to be %Y-%m-%d
Xarthisius Sep 5, 2025
fc2bcd5
Fix publisher setting in tests
Xarthisius Sep 8, 2025
aae23f0
fix case handling of local identifier type
Xarthisius Sep 8, 2025
445e423
When updating deposition, closing should redirect to its page instead…
Xarthisius Sep 8, 2025
89ed424
Initialize sample tracker field with current sampleId
Xarthisius Sep 8, 2025
eb6ebec
do not reset sample id if it's not provided in PUT
Xarthisius Sep 8, 2025
d507844
allow to skip schema/jsHelpers in GET /forms
Xarthisius Sep 12, 2025
45fd0d7
Add POST /deposition/relation
Xarthisius Sep 12, 2025
df2e3ce
New view for depositions
Xarthisius Sep 12, 2025
3e875d7
Add task for associating entry to igsn
Xarthisius Sep 15, 2025
efa501f
Validate entries against common sense and schema
Xarthisius Sep 15, 2025
b8bd8f8
update default settings
Xarthisius Sep 18, 2025
0402902
Add GET /form/:id/schema
Xarthisius Sep 18, 2025
0d386b6
Fix dep
Xarthisius Sep 19, 2025
7b98301
Display DOI url if state is not draft
Xarthisius Sep 21, 2025
f77d20a
fix weihs_lab test
Xarthisius Sep 21, 2025
e0bd6cf
[igsn] Allow multiple descriptions
Xarthisius Sep 23, 2025
2909d92
Limit number of relations shown. Fixes #29
Xarthisius Sep 24, 2025
f99e1d4
Allow to register IGSNs with AIMD
Xarthisius Sep 29, 2025
8eb6ebd
Add GET /item/query
Xarthisius Oct 16, 2025
135792e
Convert date strings to objects in item query
Xarthisius Oct 16, 2025
dfc8713
Sort items and folders
Xarthisius Oct 21, 2025
a593817
Set HasPart on main deposition. Fixes #26
Xarthisius Oct 23, 2025
0bd8f1b
Handle automatic relation for arrays
Xarthisius Oct 28, 2025
4afeb72
Add generic entry queries
Xarthisius Dec 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[run]
omit = ./venv/*,girder_jsonforms/tests/*

[report]
omit = ./venv/*,girder_jsonforms/tests/*
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
python-version: ["3.12"]
services:
mongodb:
image: mongo:4.0
image: mongo:4.2
ports:
- 27017:27017
steps:
Expand Down
5 changes: 3 additions & 2 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
prune girder_jsonforms/web_client
include girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs
include girder_jsonforms/web_client/dist/style.css
include girder_jsonforms/schemas/*.json
prune girder_jsonforms/tests
exclude girder_jsonformst/tests
global-prune *.yaml
global-exclude *.yaml
prune scripts
exclude scripts
exclude codecov.yml MANIFEST.in requirements-dev.txt tox.ini
prune codecov.yml MANIFEST.in requirements-dev.txt tox.ini
exclude codecov.yml MANIFEST.in requirements-dev.txt tox.ini .coveragerc
prune codecov.yml MANIFEST.in requirements-dev.txt tox.ini .coveragerc
include README.md
235 changes: 233 additions & 2 deletions girder_jsonforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,34 @@
import logging
from pathlib import Path

import cherrypy
from bson import ObjectId
from girder import events
from girder.constants import AccessType
from girder.api import access
from girder.api import rest as girderRest
from girder.api.describe import Description, autoDescribeRoute
from girder.constants import AccessType, TokenScope
from girder.exceptions import GirderException, ValidationException
from girder.models.file import File
from girder.models.folder import Folder
from girder.models.item import Item
from girder.models.setting import Setting
from girder.models.user import User
from girder.plugin import GirderPlugin, registerPluginStaticContent
from girder.utility import search
from girder.utility.model_importer import ModelImporter

from .lib.google_drive import authenticate_gdrive, upload_file_to_gdrive
from .lib.jq import convert_dates
from .models.deposition import Deposition as DepositionModel
from .models.deposition import PrefixCounter as PrefixCounterModel
from .models.entry import FormEntry as FormEntryModel
from .models.form import Form as FormModel
from .rest.deposition import Deposition
from .rest.entry import FormEntry
from .rest.form import Form
from .settings import PluginSettings
from .worker_plugin.delete_folder import run as delete_folder_task

GDRIVE_SERVICE = None
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -64,23 +78,240 @@ def upload_to_gdrive(event):
Item().setMetadata(parent, {"gdriveFileId": gdrive_file_id})


def igsn_search(query, types, user, level, limit, offset):
results = {}
allowed = {
"folder": ["_id", "name", "description", "parentId", "meta.igsn"],
"item": ["_id", "name", "description", "folderId", "meta.igsn"],
}
query = {"meta.igsn": {"$regex": query, "$options": "i"}}
for modelName in types:
if modelName not in allowed:
continue
model = ModelImporter.model(modelName)
if model is None:
continue
if hasattr(model, "filterResultsByPermission"):
cursor = model.find(query, fields=allowed[modelName] + ["public", "access"])
results[modelName] = [
model.filter(obj, user)
for obj in model.filterResultsByPermission(
cursor, user, level, limit=limit, offset=offset
)
]
else:
results[modelName] = list(
model.find(query, fields=allowed[modelName], limit=limit, offset=offset)
)
return results


def search_by_user(query, types, user, level, limit, offset):
allowed = {
"folder": ["_id", "name", "description", "parentId", "meta.user", "created"],
"item": ["_id", "name", "description", "folderId", "meta.user", "created"],
"deposition": [
"_id",
"created",
"igsn",
"metadata.alternateIdentifiers",
"metadata.titles",
"metadata.descriptions",
],
}
results = {_type: [] for _type in types if _type in allowed}
if not query:
return results

try:
ObjectId(query)
creator = User().load(query, level=AccessType.READ, user=user, exc=True)
except Exception:
return results

for modelName in types:
if modelName not in allowed:
continue
if modelName == "deposition":
model = DepositionModel()
else:
model = ModelImporter.model(modelName)
if model is None:
continue
cursor = model.find(
{"creatorId": creator["_id"]},
fields=allowed[modelName] + ["public", "access"],
sort=[("created", -1)],
)
results[modelName] = [
model.filter(obj, user)
for obj in model.filterResultsByPermission(
cursor, user, level, limit=limit, offset=offset
)
]
return results


def igsn_text_search(query, types, user, level, limit, offset):
results = {}
allowed = {
"deposition": [
"_id",
"igsn",
"metadata.alternateIdentifiers",
"metadata.titles",
"metadata.descriptions",
],
}
query = {
"$or": [
{"igsn": {"$regex": query, "$options": "i"}},
{
"metadata.alternateIdentifiers.alternateIdentifier": {
"$regex": query,
"$options": "i",
}
},
{"metadata.titles.title": {"$regex": query, "$options": "i"}},
{"metadata.descriptions.description": {"$regex": query, "$options": "i"}},
]
}
for modelName in types:
if modelName not in allowed:
continue
cursor = DepositionModel().find(
query, fields=allowed[modelName] + ["public", "access"]
)
results[modelName] = list(
DepositionModel().filterResultsByPermission(
cursor, user, level, limit=limit, offset=offset
)
)
for entry in results["deposition"]:
local_id = None
attrs = entry["metadata"].get("alternateIdentifiers", [])
for attr in attrs:
if attr["alternateIdentifierType"].lower() == "local":
local_id = attr["alternateIdentifier"]
break
if local_id:
tag = f"{entry['igsn']} ({local_id})"
else:
tag = f"{entry['igsn']}"
entry["name"] = f"{tag} - {entry['metadata']['titles'][0]['title']}"
return results


@access.user(scope=TokenScope.DATA_OWN)
@girderRest.boundHandler
def _delayed_delete_folder(self, event):
folderId = event.info["id"]
user = self.getCurrentUser()
folder = Folder().load(folderId, user=user, level=AccessType.ADMIN)

if not folder:
return # proceed as normal and let girder handle the error

params = event.info["params"]
try:
countdown = float(params.get("countdown", "0"))
if countdown <= 0:
raise ValueError
except ValueError:
return # proceed as normal and let girder handle the deletion immediately

delete_folder_task.apply_async(
args=(),
kwargs={
"folderId": str(folder["_id"]),
"progress": params.get("progress", False),
"userId": str(user["_id"]),
"girder_job_title": f"Delete temporary folder '{folder['name']}'",
},
countdown=countdown,
)
event.preventDefault().addResponse(
{
"message": f"Marked folder {folder['name']} for deletion in {countdown} seconds"
}
)


@access.user
@autoDescribeRoute(
Description("Search items using mongo query syntax.")
.jsonParam("query", "The MongoDB query to apply.", requireObject=True)
.pagingParams(defaultSort="lowerName")
.errorResponse()
.errorResponse("You are not authorized to search items.", 403)
)
@girderRest.boundHandler
def _item_advanced_search(self, query, limit, offset, sort):
user = self.getCurrentUser()
query = convert_dates(query)
cursor = Item().findWithPermissions(
query, sort=sort, user=user, level=AccessType.READ, limit=limit, offset=offset
)
if callable(getattr(cursor, "count", None)):
cherrypy.response.headers["Girder-Total-Count"] = cursor.count()
return [Item().filter(doc, user) for doc in cursor]


class JSONFormsPlugin(GirderPlugin):
DISPLAY_NAME = "JSON Forms"

def load(self, info):
ModelImporter.registerModel("form", FormModel, plugin="jsonforms")
from girder.api.v1.folder import Folder as FolderResource # noqa: F401

ModelImporter.registerModel("deposition", DepositionModel, plugin="jsonforms")
ModelImporter.registerModel("entry", FormEntryModel, plugin="jsonforms")
ModelImporter.registerModel("form", FormModel, plugin="jsonforms")
ModelImporter.registerModel(
"prefixcounter", PrefixCounterModel, plugin="jsonforms"
)
global GDRIVE_SERVICE
if Setting().get(PluginSettings.GOOGLE_DRIVE_ENABLED):
try:
GDRIVE_SERVICE = authenticate_gdrive()
except ValueError:
logger.exception("Failed to authenticate with Google Drive")
info["apiRoot"].item.route("GET", ("query",), _item_advanced_search)
info["apiRoot"].form = Form()
info["apiRoot"].entry = FormEntry()
info["apiRoot"].deposition = Deposition()
try:
DepositionModel().validate({}) # To initialize the model and bind events
except ValidationException:
pass
events.bind("data.process", "jsonforms", annotate_uploads)
events.bind(
"rest.delete.folder/:id.before", "jsonforms", _delayed_delete_folder
)
if GDRIVE_SERVICE is not None:
events.bind("gdrive.upload", "jsonforms", upload_to_gdrive)
try:
search.addSearchMode("igsn", igsn_search)
except GirderException:
logger.warning("IGSN search mode already registered.")
try:
search.addSearchMode("igsnText", igsn_text_search)
except GirderException:
logger.warning("IGSN text search mode already registered.")
try:
search.addSearchMode("byCreator", search_by_user)
except GirderException:
logger.warning("byCreator search mode already registered.")

FolderResource.deleteFolder.description.param(
"countdown",
(
"Number of seconds into the future that the task should execute. "
"Defaults to immediate execution."
),
required=False,
dataType="float",
)

registerPluginStaticContent(
plugin="jsonforms",
css=["/style.css"],
Expand Down
40 changes: 39 additions & 1 deletion girder_jsonforms/lib/jq.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,42 @@
from typing import Any
from dateutil import parser
from dateutil.parser import ParserError


def convert_dates(data):
"""
Recursively identifies string values that can be parsed as dates
and converts them to datetime objects.

Args:
data: The dictionary, list, or value to process.

Returns:
The processed dictionary, list, or value with date strings
converted to datetime objects.
"""
if isinstance(data, dict):
# Process dictionary items recursively
return {k: convert_dates(v) for k, v in data.items()}

elif isinstance(data, list):
# Process list items recursively
return [convert_dates(item) for item in data]

elif isinstance(data, str):
# Attempt to parse the string as a date
try:
# Use 'parser.parse' with 'fuzzy=False' for stricter parsing
# You can set 'ignoretz=True' if you want to drop timezone info
return parser.parse(data)
except (ParserError, TypeError, ValueError):
# If parsing fails, return the original string
return data

else:
# Return all other types (int, float, bool, etc.) as is
return data


# find all occurences of a key in a nested json
def find_key_paths(json_data: dict, key: str, path: str = "") -> list[str]:
Expand Down Expand Up @@ -108,7 +146,7 @@ def is_array_index(key):
current = current[keys[i - 1]]
# Extend the list to ensure the index exists

if i + 1 <= len(keys[:-1]) and is_array_index(keys[i+1]):
if i + 1 <= len(keys[:-1]) and is_array_index(keys[i + 1]):
append_object = []
else:
append_object = {}
Expand Down
Loading