• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

uc-cdis / indexd / 13790080312

11 Mar 2025 02:07PM UTC coverage: 87.5%. Remained the same
13790080312

push

github

web-flow
Fix version endpoint, update deps (#391)

2870 of 3280 relevant lines covered (87.5%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.67
indexd/index/blueprint.py
1
import re
1✔
2
import json
1✔
3
import flask
1✔
4
import hashlib
1✔
5
import jsonschema
1✔
6
from ..version_data import VERSION, COMMIT
1✔
7

8
from indexd import auth
1✔
9

10
from indexd.errors import AuthError, AuthzError
1✔
11
from indexd.errors import UserError
1✔
12

13
from .schema import PUT_RECORD_SCHEMA
1✔
14
from .schema import POST_RECORD_SCHEMA
1✔
15
from .schema import RECORD_ALIAS_SCHEMA
1✔
16
from .schema import BUNDLE_SCHEMA
1✔
17
from .schema import UPDATE_ALL_VERSIONS_SCHEMA
1✔
18

19
from .errors import NoRecordFound
1✔
20
from .errors import MultipleRecordsFound
1✔
21
from .errors import RevisionMismatch
1✔
22
from .errors import UnhealthyCheck
1✔
23

24
from cdislogging import get_logger
1✔
25
from indexd.drs.blueprint import bundle_to_drs
1✔
26

27
logger = get_logger("indexd/index blueprint", log_level="info")
1✔
28

29
blueprint = flask.Blueprint("index", __name__)
1✔
30

31
blueprint.config = dict()
1✔
32
blueprint.index_driver = None
1✔
33
blueprint.dist = []
1✔
34

35
ACCEPTABLE_HASHES = {
1✔
36
    "md5": re.compile(r"^[0-9a-f]{32}$").match,
37
    "sha1": re.compile(r"^[0-9a-f]{40}$").match,
38
    "sha256": re.compile(r"^[0-9a-f]{64}$").match,
39
    "sha512": re.compile(r"^[0-9a-f]{128}$").match,
40
    "crc": re.compile(r"^[0-9a-f]{8}$").match,
41
    "etag": re.compile(r"^[0-9a-f]{32}(-\d+)?$").match,
42
}
43

44

45
def validate_hashes(**hashes):
1✔
46
    """
47
    Validate hashes against known and valid hashing algorithms.
48
    """
49
    if not all(h in ACCEPTABLE_HASHES for h in hashes):
1✔
50
        raise UserError("invalid hash types specified")
×
51

52
    if not all(ACCEPTABLE_HASHES[h](v) for h, v in hashes.items()):
1✔
53
        raise UserError("invalid hash values specified")
×
54

55

56
@blueprint.route("/index/", methods=["GET"])
1✔
57
def get_index(form=None):
1✔
58
    """
59
    Returns a list of records.
60
    """
61
    limit = flask.request.args.get("limit")
1✔
62
    start = flask.request.args.get("start")
1✔
63
    page = flask.request.args.get("page")
1✔
64

65
    ids = flask.request.args.get("ids")
1✔
66
    if ids:
1✔
67
        ids = ids.split(",")
1✔
68
        if start is not None or limit is not None or page is not None:
1✔
69
            raise UserError("pagination is not supported when ids is provided")
×
70
    try:
1✔
71
        limit = 100 if limit is None else int(limit)
1✔
72
    except ValueError as err:
1✔
73
        raise UserError("limit must be an integer")
1✔
74

75
    if limit < 0 or limit > 1024:
1✔
76
        raise UserError("limit must be between 0 and 1024")
1✔
77

78
    if page is not None:
1✔
79
        try:
1✔
80
            page = int(page)
1✔
81
        except ValueError as err:
×
82
            raise UserError("page must be an integer")
×
83

84
    size = flask.request.args.get("size")
1✔
85
    try:
1✔
86
        size = size if size is None else int(size)
1✔
87
    except ValueError as err:
1✔
88
        raise UserError("size must be an integer")
1✔
89

90
    if size is not None and size < 0:
1✔
91
        raise UserError("size must be > 0")
1✔
92

93
    uploader = flask.request.args.get("uploader")
1✔
94

95
    # TODO: Based on indexclient, url here should be urls instead. Or change urls to url in indexclient.
96
    urls = flask.request.args.getlist("url")
1✔
97

98
    file_name = flask.request.args.get("file_name")
1✔
99

100
    version = flask.request.args.get("version")
1✔
101

102
    hashes = flask.request.args.getlist("hash")
1✔
103
    hashes = {h: v for h, v in (x.split(":", 1) for x in hashes)}
1✔
104

105
    validate_hashes(**hashes)
1✔
106
    hashes = hashes if hashes else None
1✔
107
    metadata = flask.request.args.getlist("metadata")
1✔
108
    metadata = {k: v for k, v in (x.split(":", 1) for x in metadata)}
1✔
109
    acl = flask.request.args.get("acl")
1✔
110
    if acl is not None:
1✔
111
        acl = [] if acl == "null" else acl.split(",")
1✔
112

113
    authz = flask.request.args.get("authz")
1✔
114
    if authz is not None:
1✔
115
        authz = [] if authz == "null" else authz.split(",")
1✔
116

117
    urls_metadata = flask.request.args.get("urls_metadata")
1✔
118
    if urls_metadata:
1✔
119
        try:
1✔
120
            urls_metadata = json.loads(urls_metadata)
1✔
121
        except ValueError:
×
122
            raise UserError("urls_metadata must be a valid json string")
×
123

124
    negate_params = flask.request.args.get("negate_params")
1✔
125
    if negate_params:
1✔
126
        try:
1✔
127
            negate_params = json.loads(negate_params)
1✔
128
        except ValueError:
×
129
            raise UserError("negate_params must be a valid json string")
×
130

131
    form = flask.request.args.get("form") if not form else form
1✔
132
    if form == "bundle":
1✔
133
        records = blueprint.index_driver.get_bundle_list(
1✔
134
            start=start, limit=limit, page=page
135
        )
136
    elif form == "all":
1✔
137
        records = blueprint.index_driver.get_bundle_and_object_list(
1✔
138
            limit=limit,
139
            page=page,
140
            start=start,
141
            size=size,
142
            urls=urls,
143
            acl=acl,
144
            authz=authz,
145
            hashes=hashes,
146
            file_name=file_name,
147
            version=version,
148
            uploader=uploader,
149
            metadata=metadata,
150
            ids=ids,
151
            urls_metadata=urls_metadata,
152
            negate_params=negate_params,
153
        )
154
    else:
155
        records = blueprint.index_driver.ids(
1✔
156
            start=start,
157
            limit=limit,
158
            page=page,
159
            size=size,
160
            file_name=file_name,
161
            version=version,
162
            urls=urls,
163
            acl=acl,
164
            authz=authz,
165
            hashes=hashes,
166
            uploader=uploader,
167
            ids=ids,
168
            metadata=metadata,
169
            urls_metadata=urls_metadata,
170
            negate_params=negate_params,
171
        )
172

173
    base = {
1✔
174
        "ids": ids,
175
        "records": records,
176
        "limit": limit,
177
        "start": start,
178
        "page": page,
179
        "size": size,
180
        "file_name": file_name,
181
        "version": version,
182
        "urls": urls,
183
        "acl": acl,
184
        "authz": authz,
185
        "hashes": hashes,
186
        "metadata": metadata,
187
        "urls_metadata": urls_metadata,
188
    }
189
    return flask.jsonify(base), 200
1✔
190

191

192
@blueprint.route("/urls/", methods=["GET"])
1✔
193
def get_urls():
1✔
194
    """
195
    Returns a list of urls.
196
    """
197
    ids = flask.request.args.get("ids")
1✔
198
    if ids:
1✔
199
        ids = ids.split(",")
1✔
200
    hashes = flask.request.args.getlist("hash")
1✔
201
    hashes = {h: v for h, v in (x.split(":", 1) for x in hashes)}
1✔
202
    size = flask.request.args.get("size")
1✔
203
    if size:
1✔
204
        try:
1✔
205
            size = int(size)
1✔
206
        except TypeError:
×
207
            raise UserError("size must be an integer")
×
208

209
        if size < 0:
1✔
210
            raise UserError("size must be >= 0")
×
211

212
    try:
1✔
213
        start = int(flask.request.args.get("start", 0))
1✔
214
    except TypeError:
×
215
        raise UserError("start must be an integer")
×
216

217
    try:
1✔
218
        limit = int(flask.request.args.get("limit", 100))
1✔
219
    except TypeError:
×
220
        raise UserError("limit must be an integer")
×
221

222
    if start < 0:
1✔
223
        raise UserError("start must be >= 0")
×
224

225
    if limit < 0:
1✔
226
        raise UserError("limit must be >= 0")
×
227

228
    if limit > 1024:
1✔
229
        raise UserError("limit must be <= 1024")
×
230

231
    validate_hashes(**hashes)
1✔
232

233
    urls = blueprint.index_driver.get_urls(
1✔
234
        size=size, ids=ids, hashes=hashes, start=start, limit=limit
235
    )
236

237
    ret = {"urls": urls, "limit": limit, "start": start, "size": size, "hashes": hashes}
1✔
238

239
    return flask.jsonify(ret), 200
1✔
240

241

242
# NOTE: /index/<record>/deeper-route methods are above /index/<record> so that routing
243
# prefers these first. Without this ordering, newer versions of the web framework
244
# were interpretting index/e383a3aa-316e-4a51-975d-d699eff41bd2/aliases/ as routing
245
# to /index/<record> where <record> was "e383a3aa-316e-4a51-975d-d699eff41bd2/aliases/"
246

247

248
@blueprint.route("/index/<path:record>/aliases", methods=["GET"])
1✔
249
def get_aliases(record):
1✔
250
    """
251
    Get all aliases associated with this DID / GUID
252
    """
253
    # error handling done in driver
254
    aliases = blueprint.index_driver.get_aliases_for_did(record)
1✔
255

256
    aliases_payload = {"aliases": [{"value": alias} for alias in aliases]}
1✔
257
    return flask.jsonify(aliases_payload), 200
1✔
258

259

260
@blueprint.route("/index/<path:record>/aliases/", methods=["POST"])
1✔
261
def append_aliases(record):
1✔
262
    """
263
    Append one or more aliases to aliases already associated with this
264
    DID / GUID, if any.
265
    """
266
    # we set force=True so that if MIME type of request is not application/JSON,
267
    # get_json will still throw a UserError.
268
    aliases_json = flask.request.get_json(force=True)
1✔
269
    try:
1✔
270
        jsonschema.validate(aliases_json, RECORD_ALIAS_SCHEMA)
1✔
271
    except jsonschema.ValidationError as err:
×
272
        # TODO I BELIEVE THIS IS WHERE THE ERROR IS
273
        logger.warning(f"Bad request body:\n{err}")
×
274
        raise UserError(err)
×
275

276
    aliases = [record["value"] for record in aliases_json["aliases"]]
1✔
277

278
    # authorization and error handling done in driver
279
    blueprint.index_driver.append_aliases_for_did(aliases, record)
1✔
280

281
    aliases = blueprint.index_driver.get_aliases_for_did(record)
1✔
282
    aliases_payload = {"aliases": [{"value": alias} for alias in aliases]}
1✔
283
    return flask.jsonify(aliases_payload), 200
1✔
284

285

286
@blueprint.route("/index/<path:record>/aliases", methods=["PUT"])
1✔
287
def replace_aliases(record):
1✔
288
    """
289
    Replace all aliases associated with this DID / GUID
290
    """
291
    # we set force=True so that if MIME type of request is not application/JSON,
292
    # get_json will still throw a UserError.
293
    aliases_json = flask.request.get_json(force=True)
1✔
294
    try:
1✔
295
        jsonschema.validate(aliases_json, RECORD_ALIAS_SCHEMA)
1✔
296
    except jsonschema.ValidationError as err:
×
297
        logger.warning(f"Bad request body:\n{err}")
×
298
        raise UserError(err)
×
299

300
    aliases = [record["value"] for record in aliases_json["aliases"]]
1✔
301

302
    # authorization and error handling done in driver
303
    blueprint.index_driver.replace_aliases_for_did(aliases, record)
1✔
304

305
    aliases_payload = {"aliases": [{"value": alias} for alias in aliases]}
1✔
306
    return flask.jsonify(aliases_payload), 200
1✔
307

308

309
@blueprint.route("/index/<path:record>/aliases", methods=["DELETE"])
1✔
310
def delete_all_aliases(record):
1✔
311
    # authorization and error handling done in driver
312
    blueprint.index_driver.delete_all_aliases_for_did(record)
1✔
313

314
    return flask.jsonify("Aliases deleted successfully"), 200
1✔
315

316

317
@blueprint.route("/index/<path:record>/aliases/<path:alias>", methods=["DELETE"])
1✔
318
def delete_one_alias(record, alias):
1✔
319
    # authorization and error handling done in driver
320
    blueprint.index_driver.delete_one_alias_for_did(alias, record)
1✔
321

322
    return flask.jsonify("Aliases deleted successfully"), 200
1✔
323

324

325
@blueprint.route("/index/<path:record>/versions", methods=["GET"])
1✔
326
def get_all_index_record_versions(record):
1✔
327
    """
328
    Get all record versions
329
    """
330
    ret = blueprint.index_driver.get_all_versions(record)
1✔
331

332
    return flask.jsonify(ret), 200
1✔
333

334

335
@blueprint.route("/index/<path:record>/versions", methods=["PUT"])
1✔
336
def update_all_index_record_versions(record):
1✔
337
    """
338
    Update metadata for all record versions.
339
    NOTE currently the only fields that can be updated for all versions are
340
    (`authz`, `acl`).
341
    """
342
    request_json = flask.request.get_json(force=True)
1✔
343
    try:
1✔
344
        jsonschema.validate(request_json, UPDATE_ALL_VERSIONS_SCHEMA)
1✔
345
    except jsonschema.ValidationError as err:
1✔
346
        logger.warning(f"Bad request body:\n{err}")
1✔
347
        raise UserError(err)
1✔
348

349
    acl = request_json.get("acl")
1✔
350
    authz = request_json.get("authz")
1✔
351
    # authorization and error handling done in driver
352
    ret = blueprint.index_driver.update_all_versions(record, acl=acl, authz=authz)
1✔
353

354
    return flask.jsonify(ret), 200
1✔
355

356

357
@blueprint.route("/index/<path:record>/latest", methods=["GET"])
1✔
358
def get_latest_index_record_versions(record):
1✔
359
    """
360
    Get the latest record version
361
    """
362
    has_version = flask.request.args.get("has_version", "").lower() == "true"
1✔
363
    ret = blueprint.index_driver.get_latest_version(record, has_version=has_version)
1✔
364

365
    return flask.jsonify(ret), 200
1✔
366

367

368
## /index
369

370

371
@blueprint.route("/index/<path:record>", methods=["GET"])
1✔
372
def get_index_record(record):
1✔
373
    """
374
    Returns a record.
375
    """
376
    ret = blueprint.index_driver.get_with_nonstrict_prefix(record)
1✔
377

378
    return flask.jsonify(ret), 200
1✔
379

380

381
@blueprint.route("/index/", methods=["POST"])
1✔
382
def post_index_record():
1✔
383
    """
384
    Create a new record.
385
    """
386
    try:
1✔
387
        jsonschema.validate(flask.request.json, POST_RECORD_SCHEMA)
1✔
388
    except jsonschema.ValidationError as err:
1✔
389
        raise UserError(err)
1✔
390

391
    authz = flask.request.json.get("authz", [])
1✔
392
    auth.authorize("create", authz)
1✔
393

394
    did = flask.request.json.get("did")
1✔
395
    form = flask.request.json["form"]
1✔
396
    size = flask.request.json["size"]
1✔
397
    urls = flask.request.json["urls"]
1✔
398
    acl = flask.request.json.get("acl", [])
1✔
399

400
    hashes = flask.request.json["hashes"]
1✔
401
    file_name = flask.request.json.get("file_name")
1✔
402
    metadata = flask.request.json.get("metadata")
1✔
403
    urls_metadata = flask.request.json.get("urls_metadata")
1✔
404
    version = flask.request.json.get("version")
1✔
405
    baseid = flask.request.json.get("baseid")
1✔
406
    uploader = flask.request.json.get("uploader")
1✔
407
    description = flask.request.json.get("description")
1✔
408
    content_created_date = flask.request.json.get("content_created_date")
1✔
409
    content_updated_date = flask.request.json.get("content_updated_date")
1✔
410

411
    if content_updated_date is None:
1✔
412
        content_updated_date = content_created_date
1✔
413

414
    if content_updated_date is not None and content_created_date is None:
1✔
415
        raise UserError("Cannot set content_updated_date without content_created_date")
1✔
416

417
    if content_updated_date is not None and content_created_date is not None:
1✔
418
        if content_updated_date < content_created_date:
1✔
419
            raise UserError(
1✔
420
                "content_updated_date cannot come before content_created_date"
421
            )
422

423
    did, rev, baseid = blueprint.index_driver.add(
1✔
424
        form,
425
        did,
426
        size=size,
427
        file_name=file_name,
428
        metadata=metadata,
429
        urls_metadata=urls_metadata,
430
        version=version,
431
        urls=urls,
432
        acl=acl,
433
        authz=authz,
434
        hashes=hashes,
435
        baseid=baseid,
436
        uploader=uploader,
437
        description=description,
438
        content_created_date=content_created_date,
439
        content_updated_date=content_updated_date,
440
    )
441

442
    ret = {"did": did, "rev": rev, "baseid": baseid}
1✔
443

444
    return flask.jsonify(ret), 200
1✔
445

446

447
@blueprint.route("/index/blank/", methods=["POST"])
1✔
448
def post_index_blank_record():
1✔
449
    """
450
    Create a blank new record with only uploader and optionally
451
    file_name fields filled
452
    """
453
    body = flask.request.get_json() or {}
1✔
454
    uploader = body.get("uploader")
1✔
455
    file_name = body.get("file_name")
1✔
456
    authz = body.get("authz")
1✔
457

458
    # authorize done in add_blank_record
459
    did, rev, baseid = blueprint.index_driver.add_blank_record(
1✔
460
        uploader=uploader, file_name=file_name, authz=authz
461
    )
462

463
    ret = {"did": did, "rev": rev, "baseid": baseid}
1✔
464

465
    return flask.jsonify(ret), 201
1✔
466

467

468
@blueprint.route("/index/blank/<path:record>", methods=["POST"])
1✔
469
def add_index_blank_record_version(record):
1✔
470
    """
471
    Create a new blank version of the record with this GUID.
472
    Authn/authz fields carry over from the previous version of the record.
473
    Only uploader and optionally file_name fields are filled.
474
    Returns the GUID of the new blank version and the baseid common to all versions
475
    of the record.
476
    """
477
    body = flask.request.get_json() or {}
1✔
478
    new_did = body.get("did")
1✔
479
    uploader = body.get("uploader")
1✔
480
    file_name = body.get("file_name")
1✔
481
    authz = body.get("authz")
1✔
482

483
    # authorize done in add_blank_version for the existing record's authz
484
    did, baseid, rev = blueprint.index_driver.add_blank_version(
1✔
485
        record, new_did=new_did, uploader=uploader, file_name=file_name, authz=authz
486
    )
487
    ret = {"did": did, "baseid": baseid, "rev": rev}
1✔
488

489
    return flask.jsonify(ret), 201
1✔
490

491

492
@blueprint.route("/index/blank/<path:record>", methods=["PUT"])
1✔
493
def put_index_blank_record(record):
1✔
494
    """
495
    Update a blank record with size, hashes and url
496
    """
497
    rev = flask.request.args.get("rev")
1✔
498

499
    body = flask.request.get_json() or {}
1✔
500
    size = body.get("size")
1✔
501
    hashes = body.get("hashes")
1✔
502
    urls = body.get("urls")
1✔
503
    authz = body.get("authz")
1✔
504

505
    # authorize done in update_blank_record
506
    did, rev, baseid = blueprint.index_driver.update_blank_record(
1✔
507
        did=record, rev=rev, size=size, hashes=hashes, urls=urls, authz=authz
508
    )
509
    ret = {"did": did, "rev": rev, "baseid": baseid}
1✔
510

511
    return flask.jsonify(ret), 200
1✔
512

513

514
@blueprint.route("/index/<path:record>", methods=["PUT"])
1✔
515
def put_index_record(record):
1✔
516
    """
517
    Update an existing record.
518
    """
519
    try:
1✔
520
        jsonschema.validate(flask.request.json, PUT_RECORD_SCHEMA)
1✔
521
    except jsonschema.ValidationError as err:
×
522
        raise UserError(err)
×
523

524
    rev = flask.request.args.get("rev")
1✔
525
    json = flask.request.json
1✔
526
    if (
1✔
527
        json.get("content_updated_date") is not None
528
        and json.get("content_created_date") is not None
529
    ):
530
        if json["content_updated_date"] < json["content_created_date"]:
1✔
531
            raise UserError(
1✔
532
                "content_updated_date cannot come before content_created_date"
533
            )
534

535
    # authorize done in update
536
    did, baseid, rev = blueprint.index_driver.update(record, rev, json)
1✔
537
    ret = {"did": did, "baseid": baseid, "rev": rev}
1✔
538

539
    return flask.jsonify(ret), 200
1✔
540

541

542
@blueprint.route("/index/<path:record>", methods=["DELETE"])
1✔
543
def delete_index_record(record):
1✔
544
    """
545
    Delete an existing record.
546
    """
547
    rev = flask.request.args.get("rev")
1✔
548
    if rev is None:
1✔
549
        raise UserError("no revision specified")
×
550

551
    # authorize done in delete
552
    blueprint.index_driver.delete(record, rev)
1✔
553

554
    return "", 200
1✔
555

556

557
@blueprint.route("/index/<path:record>", methods=["POST"])
1✔
558
def add_index_record_version(record):
1✔
559
    """
560
    Add a record version
561
    """
562
    try:
1✔
563
        jsonschema.validate(flask.request.json, POST_RECORD_SCHEMA)
1✔
564
    except jsonschema.ValidationError as err:
×
565
        raise UserError(err)
×
566

567
    new_did = flask.request.json.get("did")
1✔
568
    form = flask.request.json["form"]
1✔
569
    size = flask.request.json["size"]
1✔
570
    urls = flask.request.json["urls"]
1✔
571
    acl = flask.request.json.get("acl", [])
1✔
572
    authz = flask.request.json.get("authz", [])
1✔
573
    hashes = flask.request.json["hashes"]
1✔
574
    file_name = flask.request.json.get("file_name")
1✔
575
    metadata = flask.request.json.get("metadata")
1✔
576
    urls_metadata = flask.request.json.get("urls_metadata")
1✔
577
    version = flask.request.json.get("version")
1✔
578
    description = flask.request.json.get("description")
1✔
579
    content_created_date = flask.request.json.get("content_created_date")
1✔
580
    content_updated_date = flask.request.json.get("content_updated_date")
1✔
581

582
    if content_updated_date is None:
1✔
583
        content_updated_date = content_created_date
1✔
584

585
    if content_updated_date is not None and content_created_date is not None:
1✔
586
        if content_updated_date < content_created_date:
1✔
587
            raise UserError(
×
588
                "content_updated_date cannot come before content_created_date"
589
            )
590

591
    # authorize done in add_version for both the old and new authz
592
    did, baseid, rev = blueprint.index_driver.add_version(
1✔
593
        record,
594
        form,
595
        new_did=new_did,
596
        size=size,
597
        urls=urls,
598
        acl=acl,
599
        authz=authz,
600
        file_name=file_name,
601
        metadata=metadata,
602
        urls_metadata=urls_metadata,
603
        version=version,
604
        hashes=hashes,
605
        description=description,
606
        content_created_date=content_created_date,
607
        content_updated_date=content_updated_date,
608
    )
609

610
    ret = {"did": did, "baseid": baseid, "rev": rev}
1✔
611

612
    return flask.jsonify(ret), 200
1✔
613

614

615
@blueprint.route("/_dist", methods=["GET"])
1✔
616
def get_dist_config():
1✔
617
    """
618
    Returns the dist configuration
619
    """
620

621
    return flask.jsonify(blueprint.dist), 200
1✔
622

623

624
@blueprint.route("/_status", methods=["GET"])
1✔
625
def health_check():
1✔
626
    """
627
    Health Check.
628
    """
629
    blueprint.index_driver.health_check()
1✔
630

631
    return "Healthy", 200
1✔
632

633

634
@blueprint.route("/_stats", methods=["GET"])
1✔
635
def stats():
1✔
636
    """
637
    Return indexed data stats.
638
    """
639

640
    filecount = blueprint.index_driver.len()
1✔
641
    totalfilesize = blueprint.index_driver.totalbytes()
1✔
642

643
    base = {"fileCount": filecount, "totalFileSize": totalfilesize}
1✔
644

645
    return flask.jsonify(base), 200
1✔
646

647

648
@blueprint.route("/_version", methods=["GET"])
1✔
649
def version():
1✔
650
    """
651
    Return the version of this service.
652
    """
653

654
    base = {"version": VERSION, "commit": COMMIT}
1✔
655

656
    return flask.jsonify(base), 200
1✔
657

658

659
def get_checksum(data):
1✔
660
    """
661
    Collect checksums from bundles and objects in the bundle for compute_checksum
662
    """
663
    if "hashes" in data:
1✔
664
        return data["hashes"][list(data["hashes"])[0]]
1✔
665
    elif "checksums" in data:
1✔
666
        return data["checksums"][0]["checksum"]
×
667
    elif "checksum" in data:
1✔
668
        return data["checksum"]
1✔
669

670

671
def compute_checksum(checksums):
1✔
672
    """
673
    Checksum created by sorting alphabetically then concatenating first layer of bundles/objects.
674

675
    Args:
676
        checksums (list): list of checksums from the first layer of bundles and objects
677

678
    Returns:
679
        md5 checksum
680
    """
681
    checksums.sort()
1✔
682
    checksum = "".join(checksums)
1✔
683
    return {
1✔
684
        "checksum": hashlib.md5(
685
            checksum.encode("utf-8"), usedforsecurity=False
686
        ).hexdigest(),
687
        "type": "md5",
688
    }
689

690

691
@blueprint.route("/bundle/", methods=["POST"])
1✔
692
def post_bundle():
1✔
693
    """
694
    Create a new bundle
695
    """
696
    auth.authorize("create", ["/services/indexd/bundles"])
1✔
697
    try:
1✔
698
        jsonschema.validate(flask.request.json, BUNDLE_SCHEMA)
1✔
699
    except jsonschema.ValidationError as err:
1✔
700
        raise UserError(err)
1✔
701

702
    name = flask.request.json.get("name")
1✔
703
    bundles = flask.request.json.get("bundles")
1✔
704
    bundle_id = flask.request.json.get("bundle_id")
1✔
705
    size = flask.request.json.get("size") if flask.request.json.get("size") else 0
1✔
706
    description = (
1✔
707
        flask.request.json.get("description")
708
        if flask.request.json.get("description")
709
        else ""
710
    )
711
    version = (
1✔
712
        flask.request.json.get("version") if flask.request.json.get("version") else ""
713
    )
714
    aliases = (
1✔
715
        flask.request.json.get("aliases") if flask.request.json.get("aliases") else []
716
    )
717

718
    if len(bundles) == 0:
1✔
719
        raise UserError("Bundle data required.")
1✔
720

721
    if len(bundles) != len(set(bundles)):
1✔
722
        raise UserError("Duplicate GUID in bundles.")
1✔
723

724
    if bundle_id in bundles:
1✔
725
        raise UserError("Bundle refers to itself.")
1✔
726

727
    bundle_data = []
1✔
728
    checksums = []
1✔
729

730
    # TODO: Remove this after updating to jsonschema>=3.0.0
731
    if flask.request.json.get("checksums"):
1✔
732
        hashes = {
1✔
733
            checksum["type"]: checksum["checksum"]
734
            for checksum in flask.request.json.get("checksums")
735
        }
736
        validate_hashes(**hashes)
1✔
737
    # get bundles/records that already exists and add it to bundle_data
738
    for bundle in bundles:
1✔
739
        data = get_index_record(bundle)[0]
1✔
740
        data = data.json
1✔
741
        size += data["size"] if not flask.request.json.get("size") else 0
1✔
742
        checksums.append(get_checksum(data))
1✔
743
        data = bundle_to_drs(data, expand=True, is_content=True)
1✔
744
        bundle_data.append(data)
1✔
745
    checksum = (
1✔
746
        flask.request.json.get("checksums")
747
        if flask.request.json.get("checksums")
748
        else [compute_checksum(checksums)]
749
    )
750

751
    ret = blueprint.index_driver.add_bundle(
1✔
752
        bundle_id=bundle_id,
753
        name=name,
754
        size=size,
755
        bundle_data=json.dumps(bundle_data),
756
        checksum=json.dumps(checksum),
757
        description=description,
758
        version=version,
759
        aliases=json.dumps(aliases),
760
    )
761

762
    return flask.jsonify({"bundle_id": ret[0], "name": ret[1], "contents": ret[2]}), 200
1✔
763

764

765
@blueprint.route("/bundle/", methods=["GET"])
1✔
766
def get_bundle_record_list():
1✔
767
    """
768
    Returns a list of bundle records.
769
    """
770

771
    form = (
1✔
772
        flask.request.args.get("form") if flask.request.args.get("form") else "bundle"
773
    )
774

775
    return get_index(form=form)
1✔
776

777

778
@blueprint.route("/bundle/<path:bundle_id>", methods=["GET"])
1✔
779
def get_bundle_record_with_id(bundle_id):
1✔
780
    """
781
    Returns a record given bundle_id
782
    """
783

784
    expand = True if flask.request.args.get("expand") == "true" else False
1✔
785

786
    ret = blueprint.index_driver.get_with_nonstrict_prefix(bundle_id)
1✔
787

788
    ret = bundle_to_drs(ret, expand=expand, is_content=False)
1✔
789

790
    return flask.jsonify(ret), 200
1✔
791

792

793
@blueprint.route("/bundle/<path:bundle_id>", methods=["DELETE"])
1✔
794
def delete_bundle_record(bundle_id):
1✔
795
    """
796
    Delete bundle record given bundle_id
797
    """
798
    auth.authorize("delete", ["/services/indexd/bundles"])
1✔
799
    blueprint.index_driver.delete_bundle(bundle_id)
1✔
800

801
    return "", 200
1✔
802

803

804
@blueprint.errorhandler(NoRecordFound)
1✔
805
def handle_no_record_error(err):
1✔
806
    return flask.jsonify(error=str(err)), 404
1✔
807

808

809
@blueprint.errorhandler(MultipleRecordsFound)
1✔
810
def handle_multiple_records_error(err):
1✔
811
    return flask.jsonify(error=str(err)), 409
1✔
812

813

814
@blueprint.errorhandler(UserError)
1✔
815
def handle_user_error(err):
1✔
816
    return flask.jsonify(error=str(err)), 400
1✔
817

818

819
@blueprint.errorhandler(AuthError)
1✔
820
def handle_auth_error(err):
1✔
821
    return flask.jsonify(error=str(err)), 403
1✔
822

823

824
@blueprint.errorhandler(AuthzError)
1✔
825
def handle_authz_error(err):
1✔
826
    return flask.jsonify(error=str(err)), 401
1✔
827

828

829
@blueprint.errorhandler(RevisionMismatch)
1✔
830
def handle_revision_mismatch(err):
1✔
831
    return flask.jsonify(error=str(err)), 409
×
832

833

834
@blueprint.errorhandler(UnhealthyCheck)
1✔
835
def handle_unhealthy_check(err):
1✔
836
    return "Unhealthy", 500
×
837

838

839
@blueprint.record
1✔
840
def get_config(setup_state):
1✔
841
    config = setup_state.app.config["INDEX"]
1✔
842
    blueprint.index_driver = config["driver"]
1✔
843
    if "DIST" in setup_state.app.config:
1✔
844
        blueprint.dist = setup_state.app.config["DIST"]
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc