dcicutils 8.9.0.0b0__py3-none-any.whl → 8.9.0.1b1__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,5 @@
1
1
  from copy import deepcopy
2
2
  from functools import lru_cache
3
- import re
4
3
  from typing import Any, Callable, List, Optional, Tuple, Type, Union
5
4
  from dcicutils.data_readers import RowReader
6
5
  from dcicutils.misc_utils import create_readonly_object
@@ -14,11 +13,9 @@ class PortalObject:
14
13
 
15
14
  _PROPERTY_DELETION_SENTINEL = RowReader.CELL_DELETION_SENTINEL
16
15
 
17
- def __init__(self, data: dict, portal: Portal = None,
18
- schema: Optional[Union[dict, Schema]] = None, type: Optional[str] = None) -> None:
16
+ def __init__(self, data: dict, portal: Optional[Portal] = None, type: Optional[str] = None) -> None:
19
17
  self._data = data if isinstance(data, dict) else {}
20
18
  self._portal = portal if isinstance(portal, Portal) else None
21
- self._schema = schema if isinstance(schema, dict) else (schema.data if isinstance(schema, Schema) else None)
22
19
  self._type = type if isinstance(type, str) else ""
23
20
 
24
21
  @property
@@ -32,7 +29,7 @@ class PortalObject:
32
29
  @property
33
30
  @lru_cache(maxsize=1)
34
31
  def type(self) -> str:
35
- return self._type or Portal.get_schema_type(self._data) or (Schema(self._schema).type if self._schema else "")
32
+ return self._type or Portal.get_schema_type(self._data) or ""
36
33
 
37
34
  @property
38
35
  @lru_cache(maxsize=1)
@@ -47,7 +44,7 @@ class PortalObject:
47
44
  @property
48
45
  @lru_cache(maxsize=1)
49
46
  def schema(self) -> Optional[dict]:
50
- return self._schema if self._schema else (self._portal.get_schema(self.type) if self._portal else None)
47
+ return self._portal.get_schema(self.type) if self._portal else None
51
48
 
52
49
  def copy(self) -> PortalObject:
53
50
  return PortalObject(deepcopy(self.data), portal=self.portal, type=self.type)
@@ -59,39 +56,29 @@ class PortalObject:
59
56
  Returns the list of all identifying property names of this Portal object which actually have values.
60
57
  Implicitly include "uuid" and "identifier" properties as identifying properties if they are actually
61
58
  properties in the object schema, and favor these (first); defavor "aliases"; no other ordering defined.
59
+ Changed (2024-05-26) to use portal_utils.get_identifying_property_names; migrating some intricate stuff there.
62
60
  """
63
- if not (schema := self.schema) or not (schema_identifying_properties := schema.get("identifyingProperties")):
64
- return None
65
- identifying_properties = []
66
- for identifying_property in schema_identifying_properties:
67
- if identifying_property not in ["uuid", "identifier", "aliases"]:
68
- if self._data.get(identifying_property):
69
- identifying_properties.append(identifying_property)
70
- if self._data.get("identifier"):
71
- identifying_properties.insert(0, "identifier")
72
- if self._data.get("uuid"):
73
- identifying_properties.insert(0, "uuid")
74
- if "aliases" in schema_identifying_properties and self._data.get("aliases"):
75
- identifying_properties.append("aliases")
76
- return identifying_properties or None
61
+ # Migrating to and unifying this in portal_utils.Portal.get_identifying_paths (2024-05-26).
62
+ return self._portal.get_identifying_property_names(self.type, portal_object=self._data) if self._portal else []
77
63
 
78
64
  @lru_cache(maxsize=8192)
79
65
  def lookup(self, raw: bool = False,
80
66
  ref_lookup_strategy: Optional[Callable] = None) -> Tuple[Optional[PortalObject], Optional[str], int]:
67
+ if not (identifying_paths := self._get_identifying_paths(ref_lookup_strategy=ref_lookup_strategy)):
68
+ return None, None, 0
81
69
  nlookups = 0
82
70
  first_identifying_path = None
83
71
  try:
84
- if identifying_paths := self._get_identifying_paths(ref_lookup_strategy=ref_lookup_strategy):
85
- for identifying_path in identifying_paths:
86
- if not first_identifying_path:
87
- first_identifying_path = identifying_path
88
- nlookups += 1
89
- if (value := self._portal.get(identifying_path, raw=raw)) and (value.status_code == 200):
90
- return (
91
- PortalObject(value.json(), portal=self._portal, type=self.type if raw else None),
92
- identifying_path,
93
- nlookups
94
- )
72
+ for identifying_path in identifying_paths:
73
+ if not first_identifying_path:
74
+ first_identifying_path = identifying_path
75
+ nlookups += 1
76
+ if self._portal and (item := self._portal.get(identifying_path, raw=raw)) and (item.status_code == 200):
77
+ return (
78
+ PortalObject(item.json(), portal=self._portal, type=self.type if raw else None),
79
+ identifying_path,
80
+ nlookups
81
+ )
95
82
  except Exception:
96
83
  pass
97
84
  return None, first_identifying_path, nlookups
@@ -159,64 +146,12 @@ class PortalObject:
159
146
 
160
147
  @lru_cache(maxsize=1)
161
148
  def _get_identifying_paths(self, ref_lookup_strategy: Optional[Callable] = None) -> Optional[List[str]]:
162
- """
163
- Returns a list of the possible Portal URL paths identifying this Portal object.
164
- """
165
- identifying_paths = []
166
- if not (identifying_properties := self.identifying_properties):
167
- if self.uuid:
168
- if self.type:
169
- identifying_paths.append(f"/{self.type}/{self.uuid}")
170
- identifying_paths.append(f"/{self.uuid}")
171
- return identifying_paths
172
- for identifying_property in identifying_properties:
173
- if identifying_value := self._data.get(identifying_property):
174
- if identifying_property == "uuid":
175
- if self.type:
176
- identifying_paths.append(f"/{self.type}/{identifying_value}")
177
- identifying_paths.append(f"/{identifying_value}")
178
- # For now at least we include the path both with and without the schema type component,
179
- # as for some identifying values, it works (only) with, and some, it works (only) without.
180
- # For example: If we have FileSet with "accession", an identifying property, with value
181
- # SMAFSFXF1RO4 then /SMAFSFXF1RO4 works but /FileSet/SMAFSFXF1RO4 does not; and
182
- # conversely using "submitted_id", also an identifying property, with value
183
- # UW_FILE-SET_COLO-829BL_HI-C_1 then /UW_FILE-SET_COLO-829BL_HI-C_1 does
184
- # not work but /FileSet/UW_FILE-SET_COLO-829BL_HI-C_1 does work.
185
- elif isinstance(identifying_value, list):
186
- for identifying_value_item in identifying_value:
187
- if self.type:
188
- identifying_paths.append(f"/{self.type}/{identifying_value_item}")
189
- identifying_paths.append(f"/{identifying_value_item}")
190
- else:
191
- # TODO: Import from somewhere ...
192
- lookup_options = 0
193
- if schema := self.schema:
194
- # TODO: Hook into the ref_lookup_strategy thing in structured_data to make
195
- # sure we check accession format (since it does not have a pattern).
196
- if callable(ref_lookup_strategy):
197
- lookup_options, ref_validator = ref_lookup_strategy(
198
- self._portal, self.type, schema, identifying_value)
199
- if callable(ref_validator):
200
- if ref_validator(schema, identifying_property, identifying_value) is False:
201
- continue
202
- if pattern := schema.get("properties", {}).get(identifying_property, {}).get("pattern"):
203
- if not re.match(pattern, identifying_value):
204
- # If this identifying value is for a (identifying) property which has a
205
- # pattern, and the value does NOT match the pattern, then do NOT include
206
- # this value as an identifying path, since it cannot possibly be found.
207
- continue
208
- if not lookup_options:
209
- lookup_options = Portal.LOOKUP_DEFAULT
210
- if Portal.is_lookup_root_first(lookup_options):
211
- identifying_paths.append(f"/{identifying_value}")
212
- if Portal.is_lookup_specified_type(lookup_options) and self.type:
213
- identifying_paths.append(f"/{self.type}/{identifying_value}")
214
- if Portal.is_lookup_root(lookup_options) and not Portal.is_lookup_root_first(lookup_options):
215
- identifying_paths.append(f"/{identifying_value}")
216
- if Portal.is_lookup_subtypes(lookup_options):
217
- for subtype_name in self._portal.get_schema_subtype_names(self.type):
218
- identifying_paths.append(f"/{subtype_name}/{identifying_value}")
219
- return identifying_paths or None
149
+ if not self._portal and (uuid := self.uuid):
150
+ return [f"/{uuid}"]
151
+ # Migrating to and unifying this in portal_utils.Portal.get_identifying_paths (2024-05-26).
152
+ return self._portal.get_identifying_paths(self._data,
153
+ portal_type=self.schema,
154
+ lookup_strategy=ref_lookup_strategy) if self._portal else None
220
155
 
221
156
  def _normalized_refs(self, refs: List[dict]) -> Tuple[PortalObject, int]:
222
157
  """
dcicutils/portal_utils.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from collections import deque
2
2
  from functools import lru_cache
3
+ from dcicutils.function_cache_decorator import function_cache
3
4
  import io
4
5
  import json
5
6
  from pyramid.config import Configurator as PyramidConfigurator
@@ -18,6 +19,7 @@ from wsgiref.simple_server import make_server as wsgi_make_server
18
19
  from dcicutils.common import APP_SMAHT, OrchestratedApp, ORCHESTRATED_APPS
19
20
  from dcicutils.ff_utils import get_metadata, get_schema, patch_metadata, post_metadata
20
21
  from dcicutils.misc_utils import to_camel_case, VirtualApp
22
+ from dcicutils.schema_utils import get_identifying_properties
21
23
  from dcicutils.tmpfile_utils import temporary_file
22
24
 
23
25
  Portal = Type["Portal"] # Forward type reference for type hints.
@@ -48,15 +50,16 @@ class Portal:
48
50
  FILE_TYPE_SCHEMA_NAME = "File"
49
51
 
50
52
  # Object lookup strategies; on a per-reference (type/value) basis, used currently ONLY by
51
- # structured_data.py; controlled by an optional ref_lookup_strategy callable; default is
53
+ # structured_data.py; controlled by an optional lookup_strategy callable; default is
52
54
  # lookup at root path but after the specified type path lookup, and then lookup all subtypes;
53
55
  # can choose to lookup root path first, or not lookup root path at all, or not lookup
54
- # subtypes at all; the ref_lookup_strategy callable if specified should take a type_name
56
+ # subtypes at all; the lookup_strategy callable if specified should take a type_name
55
57
  # and value (string) arguements and return an integer of any of the below ORed together.
56
58
  # The main purpose of this is optimization; to minimize portal lookups; since for example,
57
59
  # currently at least, /{type}/{accession} does not work but /{accession} does; so we
58
60
  # currently (smaht-portal/.../ingestion_processors) use LOOKUP_ROOT_FIRST for this.
59
61
  # And current usage NEVER has LOOKUP_SUBTYPES turned OFF; but support just in case.
62
+ LOOKUP_UNDEFINED = 0
60
63
  LOOKUP_SPECIFIED_TYPE = 0x0001
61
64
  LOOKUP_ROOT = 0x0002
62
65
  LOOKUP_ROOT_FIRST = 0x0004 | LOOKUP_ROOT
@@ -205,23 +208,6 @@ class Portal:
205
208
  def vapp(self) -> Optional[TestApp]:
206
209
  return self._vapp
207
210
 
208
- @staticmethod
209
- def is_lookup_specified_type(lookup_options: int) -> bool:
210
- return (lookup_options &
211
- Portal.LOOKUP_SPECIFIED_TYPE) == Portal.LOOKUP_SPECIFIED_TYPE
212
-
213
- @staticmethod
214
- def is_lookup_root(lookup_options: int) -> bool:
215
- return (lookup_options & Portal.LOOKUP_ROOT) == Portal.LOOKUP_ROOT
216
-
217
- @staticmethod
218
- def is_lookup_root_first(lookup_options: int) -> bool:
219
- return (lookup_options & Portal.LOOKUP_ROOT_FIRST) == Portal.LOOKUP_ROOT_FIRST
220
-
221
- @staticmethod
222
- def is_lookup_subtypes(lookup_options: int) -> bool:
223
- return (lookup_options & Portal.LOOKUP_SUBTYPES) == Portal.LOOKUP_SUBTYPES
224
-
225
211
  def get(self, url: str, follow: bool = True,
226
212
  raw: bool = False, database: bool = False, raise_for_status: bool = False, **kwargs) -> OptionalResponse:
227
213
  url = self.url(url, raw, database)
@@ -305,7 +291,10 @@ class Portal:
305
291
 
306
292
  @lru_cache(maxsize=100)
307
293
  def get_schema(self, schema_name: str) -> Optional[dict]:
308
- return get_schema(self.schema_name(schema_name), portal_vapp=self.vapp, key=self.key)
294
+ try:
295
+ return get_schema(self.schema_name(schema_name), portal_vapp=self.vapp, key=self.key)
296
+ except Exception:
297
+ return None
309
298
 
310
299
  @lru_cache(maxsize=1)
311
300
  def get_schemas(self) -> dict:
@@ -416,6 +405,215 @@ class Portal:
416
405
  return []
417
406
  return schemas_super_type_map.get(type_name, [])
418
407
 
408
+ @function_cache(maxsize=100, serialize_key=True)
409
+ def get_identifying_paths(self, portal_object: dict, portal_type: Optional[Union[str, dict]] = None,
410
+ first_only: bool = False,
411
+ lookup_strategy: Optional[Union[Callable, bool]] = None) -> List[str]:
412
+ """
413
+ Returns the list of the identifying Portal (URL) paths for the given Portal object. Favors any uuid
414
+ and identifier based paths and defavors aliases based paths (ala self.get_identifying_property_names);
415
+ no other ordering defined. Returns an empty list if no identifying properties or otherwise not found.
416
+ Note that this is a newer version of what was in portal_object_utils and just uses the ref_lookup_stratey
417
+ module directly, as it no longer needs to be exposed (to smaht-portal/ingester and smaht-submitr) and so
418
+ this is a first step toward internalizing it to structured_data/portal_utils/portal_object_utils usages.
419
+ """
420
+ def is_lookup_specified_type(lookup_options: int) -> bool:
421
+ return (lookup_options & Portal.LOOKUP_SPECIFIED_TYPE) == Portal.LOOKUP_SPECIFIED_TYPE
422
+ def is_lookup_root(lookup_options: int) -> bool: # noqa
423
+ return (lookup_options & Portal.LOOKUP_ROOT) == Portal.LOOKUP_ROOT
424
+ def is_lookup_root_first(lookup_options: int) -> bool: # noqa
425
+ return (lookup_options & Portal.LOOKUP_ROOT_FIRST) == Portal.LOOKUP_ROOT_FIRST
426
+ def is_lookup_subtypes(lookup_options: int) -> bool: # noqa
427
+ return (lookup_options & Portal.LOOKUP_SUBTYPES) == Portal.LOOKUP_SUBTYPES
428
+
429
+ results = []
430
+ if not isinstance(portal_object, dict):
431
+ return results
432
+ if not (isinstance(portal_type, str) and portal_type):
433
+ if isinstance(portal_type, dict):
434
+ # It appears that the given portal_type is an actual schema dictionary.
435
+ portal_type = self.schema_name(portal_type.get("title"))
436
+ if not (isinstance(portal_type, str) and portal_type):
437
+ if not (portal_type := self.get_schema_type(portal_object)):
438
+ return results
439
+ if not callable(lookup_strategy):
440
+ lookup_strategy = None if lookup_strategy is False else Portal._lookup_strategy
441
+ for identifying_property in self.get_identifying_property_names(portal_type):
442
+ if not (identifying_value := portal_object.get(identifying_property)):
443
+ continue
444
+ # The get_identifying_property_names call above ensures uuid is first if it is in the object.
445
+ # And also note that ALL schemas do in fact have identifyingProperties which do in fact have
446
+ # uuid, except for a couple "Test" ones, and (for some reason) SubmittedItem; otherwise we
447
+ # might have a special case to check the Portal object explicitly for uuid, but no need.
448
+ if identifying_property == "uuid":
449
+ #
450
+ # Note this idiosyncrasy with Portal paths: the only way we do NOT get a (HTTP 301) redirect
451
+ # is if we use the lower-case-dashed-plural based version of the path, e.g. all of these:
452
+ #
453
+ # - /d13d06c1-218e-4f61-aaf0-91f226248b3c
454
+ # - /d13d06c1-218e-4f61-aaf0-91f226248b3c/
455
+ # - /FileFormat/d13d06c1-218e-4f61-aaf0-91f226248b3c
456
+ # - /FileFormat/d13d06c1-218e-4f61-aaf0-91f226248b3c/
457
+ # - /files-formats/d13d06c1-218e-4f61-aaf0-91f226248b3c
458
+ #
459
+ # Will result in a (HTTP 301) redirect to:
460
+ #
461
+ # - /files-formats/d13d06c1-218e-4f61-aaf0-91f226248b3c/
462
+ #
463
+ # Unfortunately, this code here has no reasonable way of getting that lower-case-dashed-plural
464
+ # based name (e.g. file-formats) from the schema/portal type name (e.g. FileFormat); as the
465
+ # information is contained, for this example, in the snovault.collection decorator for the
466
+ # endpoint definition in smaht-portal/.../types/file_format.py. Unfortunately merely because
467
+ # behind-the-scenes an extra round-trip HTTP request will occur, but happens automatically.
468
+ # And note the disction of just using /{uuid} here rather than /{type}/{uuid} as in the else
469
+ # statement below is not really necessary; just here for emphasis that this is all that's needed.
470
+ #
471
+ if first_only is True:
472
+ results.append(f"/{portal_type}/{identifying_value}")
473
+ else:
474
+ results.append(f"/{identifying_value}")
475
+ elif isinstance(identifying_value, list):
476
+ for identifying_value_item in identifying_value:
477
+ if identifying_value_item:
478
+ results.append(f"/{portal_type}/{identifying_value_item}")
479
+ else:
480
+ lookup_options = Portal.LOOKUP_UNDEFINED
481
+ if schema := self.get_schema(portal_type):
482
+ if callable(lookup_strategy):
483
+ lookup_options, validator = lookup_strategy(self, portal_type, schema, identifying_value)
484
+ if callable(validator):
485
+ if validator(schema, identifying_property, identifying_value) is False:
486
+ continue
487
+ if pattern := schema.get("properties", {}).get(identifying_property, {}).get("pattern"):
488
+ if not re.match(pattern, identifying_value):
489
+ # If this identifying value is for a (identifying) property which has a
490
+ # pattern, and the value does NOT match the pattern, then do NOT include
491
+ # this value as an identifying path, since it cannot possibly be found.
492
+ continue
493
+ if lookup_options == Portal.LOOKUP_UNDEFINED:
494
+ lookup_options = Portal.LOOKUP_DEFAULT
495
+ if is_lookup_root_first(lookup_options):
496
+ results.append(f"/{identifying_value}")
497
+ if is_lookup_specified_type(lookup_options) and portal_type:
498
+ results.append(f"/{portal_type}/{identifying_value}")
499
+ if is_lookup_root(lookup_options) and not is_lookup_root_first(lookup_options):
500
+ results.append(f"/{identifying_value}")
501
+ if is_lookup_subtypes(lookup_options):
502
+ for subtype_name in self.get_schema_subtype_names(portal_type):
503
+ results.append(f"/{subtype_name}/{identifying_value}")
504
+ if (first_only is True) and results:
505
+ return results
506
+ return results
507
+
508
+ @function_cache(maxsize=100, serialize_key=True)
509
+ def get_identifying_path(self, portal_object: dict, portal_type: Optional[Union[str, dict]] = None,
510
+ lookup_strategy: Optional[Union[Callable, bool]] = None) -> Optional[str]:
511
+ if identifying_paths := self.get_identifying_paths(portal_object, portal_type, first_only=True,
512
+ lookup_strategy=lookup_strategy):
513
+ return identifying_paths[0]
514
+ return None
515
+
516
+ @function_cache(maxsize=100, serialize_key=True)
517
+ def get_identifying_property_names(self, schema: Union[str, dict],
518
+ portal_object: Optional[dict] = None) -> List[str]:
519
+ """
520
+ Returns the list of identifying property names for the given Portal schema, which may be
521
+ either a schema name or a schema object. If a Portal object is also given then restricts this
522
+ set of identifying properties to those which actually have values within this Portal object.
523
+ Favors the uuid and identifier property names and defavors the aliases property name; no other
524
+ ordering imposed. Returns empty list if no identifying properties or otherwise not found.
525
+ """
526
+ results = []
527
+ if isinstance(schema, str):
528
+ if not (schema := self.get_schema(schema)):
529
+ return results
530
+ elif not isinstance(schema, dict):
531
+ return results
532
+ if not (identifying_properties := get_identifying_properties(schema)):
533
+ return results
534
+ identifying_properties = list(set(identifying_properties)) # paranoid dedup
535
+ identifying_properties = [*identifying_properties] # copy so as not to change schema if given
536
+ favored_identifying_properties = ["uuid", "identifier"]
537
+ defavored_identifying_properties = ["aliases"]
538
+ for favored_identifying_property in reversed(favored_identifying_properties):
539
+ if favored_identifying_property in identifying_properties:
540
+ identifying_properties.remove(favored_identifying_property)
541
+ identifying_properties.insert(0, favored_identifying_property)
542
+ for defavored_identifying_property in defavored_identifying_properties:
543
+ if defavored_identifying_property in identifying_properties:
544
+ identifying_properties.remove(defavored_identifying_property)
545
+ identifying_properties.append(defavored_identifying_property)
546
+ if isinstance(portal_object, dict):
547
+ for identifying_property in [*identifying_properties]:
548
+ if portal_object.get(identifying_property) is None:
549
+ identifying_properties.remove(identifying_property)
550
+ return identifying_properties
551
+
552
+ @staticmethod
553
+ def _lookup_strategy(portal: Portal, type_name: str, schema: dict, value: str) -> (int, Optional[str]):
554
+ #
555
+ # Note this slightly odd situation WRT object lookups by submitted_id and accession:
556
+ # -----------------------------+-----------------------------------------------+---------------+
557
+ # PATH | EXAMPLE | LOOKUP RESULT |
558
+ # -----------------------------+-----------------------------------------------+---------------+
559
+ # /submitted_id | //UW_FILE-SET_COLO-829BL_HI-C_1 | NOT FOUND |
560
+ # /UnalignedReads/submitted_id | /UnalignedReads/UW_FILE-SET_COLO-829BL_HI-C_1 | FOUND |
561
+ # /SubmittedFile/submitted_id | /SubmittedFile/UW_FILE-SET_COLO-829BL_HI-C_1 | FOUND |
562
+ # /File/submitted_id | /File/UW_FILE-SET_COLO-829BL_HI-C_1 | NOT FOUND |
563
+ # -----------------------------+-----------------------------------------------+---------------+
564
+ # /accession | /SMAFSFXF1RO4 | FOUND |
565
+ # /UnalignedReads/accession | /UnalignedReads/SMAFSFXF1RO4 | NOT FOUND |
566
+ # /SubmittedFile/accession | /SubmittedFile/SMAFSFXF1RO4 | NOT FOUND |
567
+ # /File/accession | /File/SMAFSFXF1RO4 | FOUND |
568
+ # -----------------------------+-----------------------------------------------+---------------+
569
+ #
570
+ def ref_validator(schema: Optional[dict],
571
+ property_name: Optional[str], property_value: Optional[str]) -> Optional[bool]:
572
+ """
573
+ Returns False iff objects of type represented by the given schema, CANNOT be referenced with
574
+ a Portal path using the given property name and its given property value, otherwise returns None.
575
+
576
+ For example, if the schema is for UnalignedReads and the property name is accession, then we will
577
+ return False iff the given property value is NOT a properly formatted accession ID; otherwise, we
578
+ will return None, which indicates that the caller (e.g. dcicutils.structured_data.Portal.ref_exists)
579
+ will continue executing its default behavior, which is to check other ways in which the given type
580
+ CANNOT be referenced by the given value, i.e. it checks other identifying properties for the type
581
+ and makes sure any patterns (e.g. for submitted_id or uuid) are ahered to.
582
+
583
+ The goal (in structured_data) being to detect if a type is being referenced in such a way that
584
+ CANNOT possibly be allowed, i.e. because none of its identifying types are in the required form,
585
+ if indeed there any requirements. It is assumed/guaranteed the given property name is indeed an
586
+ identifying property for the given type.
587
+ """
588
+ if property_format := schema.get("properties", {}).get(property_name, {}).get("format"):
589
+ if (property_format == "accession") and (property_name == "accession"):
590
+ if not Portal._is_accession_id(property_value):
591
+ return False
592
+ return None
593
+
594
+ DEFAULT_RESULT = (Portal.LOOKUP_DEFAULT, ref_validator)
595
+ if not value:
596
+ return DEFAULT_RESULT
597
+ if not schema:
598
+ if not isinstance(portal, Portal) or not (schema := portal.get_schema(type_name)):
599
+ return DEFAULT_RESULT
600
+ if schema_properties := schema.get("properties"):
601
+ if schema_properties.get("accession") and Portal._is_accession_id(value):
602
+ # Case: lookup by accession (only by root).
603
+ return (Portal.LOOKUP_ROOT, ref_validator)
604
+ elif schema_property_info_submitted_id := schema_properties.get("submitted_id"):
605
+ if schema_property_pattern_submitted_id := schema_property_info_submitted_id.get("pattern"):
606
+ if re.match(schema_property_pattern_submitted_id, value):
607
+ # Case: lookup by submitted_id (only by specified type).
608
+ return (Portal.LOOKUP_SPECIFIED_TYPE, ref_validator)
609
+ return DEFAULT_RESULT
610
+
611
+ @staticmethod
612
+ def _is_accession_id(value: str) -> bool:
613
+ # This is here for now because of problems with circular dependencies.
614
+ # See: smaht-portal/.../schema_formats.py/is_accession(instance) ...
615
+ return isinstance(value, str) and re.match(r"^SMA[1-9A-Z]{9}$", value) is not None
616
+
419
617
  def url(self, url: str, raw: bool = False, database: bool = False) -> str:
420
618
  if not isinstance(url, str) or not url:
421
619
  return "/"
@@ -516,6 +714,22 @@ class Portal:
516
714
  response = TestResponseWrapper(response)
517
715
  return response
518
716
 
717
+ @staticmethod
718
+ def _create_vapp(arg: Union[TestApp, VirtualApp, PyramidRouter, str] = None) -> TestApp:
719
+ if isinstance(arg, TestApp):
720
+ return arg
721
+ elif isinstance(arg, VirtualApp):
722
+ if not isinstance(arg.wrapped_app, TestApp):
723
+ raise Exception("Portal._create_vapp VirtualApp argument error.")
724
+ return arg.wrapped_app
725
+ if isinstance(arg, PyramidRouter):
726
+ router = arg
727
+ elif isinstance(arg, str) or not arg:
728
+ router = pyramid_get_app(arg or "development.ini", "app")
729
+ else:
730
+ raise Exception("Portal._create_vapp argument error.")
731
+ return TestApp(router, {"HTTP_ACCEPT": Portal.MIME_TYPE_JSON, "REMOTE_USER": "TEST"})
732
+
519
733
  @staticmethod
520
734
  def create_for_testing(arg: Optional[Union[str, bool, List[dict], dict, Callable]] = None) -> Portal:
521
735
  if isinstance(arg, list) or isinstance(arg, dict) or isinstance(arg, Callable):
@@ -547,22 +761,6 @@ class Portal:
547
761
  with temporary_file(content=minimal_ini_for_testing, suffix=".ini") as ini_file:
548
762
  return Portal(ini_file)
549
763
 
550
- @staticmethod
551
- def _create_vapp(arg: Union[TestApp, VirtualApp, PyramidRouter, str] = None) -> TestApp:
552
- if isinstance(arg, TestApp):
553
- return arg
554
- elif isinstance(arg, VirtualApp):
555
- if not isinstance(arg.wrapped_app, TestApp):
556
- raise Exception("Portal._create_vapp VirtualApp argument error.")
557
- return arg.wrapped_app
558
- if isinstance(arg, PyramidRouter):
559
- router = arg
560
- elif isinstance(arg, str) or not arg:
561
- router = pyramid_get_app(arg or "development.ini", "app")
562
- else:
563
- raise Exception("Portal._create_vapp argument error.")
564
- return TestApp(router, {"HTTP_ACCEPT": Portal.MIME_TYPE_JSON, "REMOTE_USER": "TEST"})
565
-
566
764
  @staticmethod
567
765
  def _create_router_for_testing(endpoints: Optional[List[Dict[str, Union[str, Callable]]]] = None) -> PyramidRouter:
568
766
  if isinstance(endpoints, dict):
dcicutils/schema_utils.py CHANGED
@@ -190,7 +190,7 @@ def get_one_of_formats(schema: Dict[str, Any]) -> List[str]:
190
190
 
191
191
  def is_link(property_schema: Dict[str, Any]) -> bool:
192
192
  """Is property schema a link?"""
193
- return property_schema.get(SchemaConstants.LINK_TO, False)
193
+ return bool(property_schema.get(SchemaConstants.LINK_TO))
194
194
 
195
195
 
196
196
  def get_enum(property_schema: Dict[str, Any]) -> List[str]: