Skip to content

Commit 70fb40e

Browse files
committed
doc: add the missing docstrings
1 parent 03a869c commit 70fb40e

5 files changed

Lines changed: 59 additions & 2 deletions

File tree

‎src/scim2_sqlalchemy/comparison.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class _Normalizer(_Comparator):
8181
"""
8282

8383
def _form(self, column: _Column, expression: Any) -> ColumnElement[Any]:
84+
"""Normalize a string expression to NFC, lowered if the column ignores the case, under the C collation."""
8485
normalized = func.normalize(expression, literal_column("NFC"), type_=String)
8586
if column.casefolded:
8687
normalized = func.normalize(
@@ -89,9 +90,11 @@ def _form(self, column: _Column, expression: Any) -> ColumnElement[Any]:
8990
return normalized.collate("C")
9091

9192
def column(self, column: _Column) -> ColumnElement[Any]:
93+
"""Return a string column normalized to NFC."""
9294
return self._form(column, column.compared)
9395

9496
def operand(self, column: _Column, value: str) -> Any:
97+
"""Return a string normalized to NFC, in Python then in the database."""
9598
return self._form(column, literal(unicodedata.normalize("NFC", value)))
9699

97100

@@ -112,12 +115,15 @@ def __init__(self, policy: ScimPolicy, mappings: Iterable[ResourceMapping]):
112115
}
113116

114117
def prepare(self, connection: Any) -> None:
118+
"""Register the comparison key as a SQL function of a DBAPI connection."""
115119
connection.create_function(_KEY_FUNCTION, 2, self.key, deterministic=True)
116120

117121
async def prepare_async(self, connection: Any) -> None:
122+
"""Register the comparison key as a SQL function of an asyncio DBAPI connection."""
118123
await connection.create_function(_KEY_FUNCTION, 2, self.key, deterministic=True)
119124

120125
def key(self, urn: str, value: Any) -> Any:
126+
"""Return the comparison key of a stored value, or None if the key refuses it."""
121127
if value is None:
122128
return None
123129
try:
@@ -126,17 +132,20 @@ def key(self, urn: str, value: Any) -> Any:
126132
return None
127133

128134
def column(self, column: _Column) -> ColumnElement[Any]:
135+
"""Return the comparison key of a string column, computed by the SQL function."""
129136
key: ColumnElement[Any] = getattr(func, _KEY_FUNCTION)(
130137
literal(column.binding.urn), column.compared, type_=String
131138
)
132139
return key
133140

134141
def operand(self, column: _Column, value: str) -> Any:
142+
"""Return the comparison key of a string, computed in Python."""
135143
return column.binding.comparable(value, self.policy)
136144

137145
def substring(
138146
self, op: CompareOperator, column: _Column, value: str
139147
) -> ColumnElement[bool]:
148+
"""Return whether a string column contains, starts or ends with a value, compared by their keys."""
140149
left = self.column(column)
141150
right = literal(self.operand(column, value))
142151
if op == CompareOperator.co:

‎src/scim2_sqlalchemy/conversion.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def _as_stored(value: Any) -> Any:
4444

4545

4646
def _read(column: _Column, record: Any) -> Any:
47+
"""Return the SCIM value of a column of a record, or None if the column is not read."""
4748
value = getattr(record, column.key)
4849
if value is None or not column.readable:
4950
return None
@@ -53,6 +54,7 @@ def _read(column: _Column, record: Any) -> Any:
5354

5455

5556
def _version(mapping: ResourceMapping, record: Any) -> str:
57+
"""Return the weak ETag of the version of a record."""
5658
return f'W/"{getattr(record, mapping._version.key)}"'
5759

5860

@@ -180,6 +182,7 @@ def _from_scim(
180182

181183

182184
def _write_entry(collection: _Collection, entry: Any) -> Any:
185+
"""Build the record storing an entry of a multi-valued attribute."""
183186
record = collection.entry_class()
184187
if collection.simple:
185188
setattr(record, collection.columns[None].key, _as_stored(entry))

‎src/scim2_sqlalchemy/mapping.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class _Storage(Enum):
5050

5151

5252
def _holds_text(expression: Any) -> bool:
53+
"""Whether an expression holds strings. A type that tells no Python type is taken as text."""
5354
try:
5455
return expression.type.python_type is str
5556
except NotImplementedError:
@@ -183,6 +184,7 @@ class _Column:
183184

184185
@property
185186
def is_string(self) -> bool:
187+
"""Whether the attribute is a SCIM string."""
186188
target = self.binding.target_type
187189
return (
188190
target is not None
@@ -251,22 +253,27 @@ class _Collection:
251253

252254
@property
253255
def owned(self) -> bool:
256+
"""Whether the entries belong to the resource, rather than link to other resources."""
254257
return self.resource_type is None
255258

256259
@property
257260
def entry_class(self) -> type[Any]:
261+
"""The SQLAlchemy model of the entries."""
258262
return self.relationship_property.mapper.class_
259263

260264
@property
261265
def relationship_property(self) -> RelationshipProperty[Any]:
266+
"""The relationship holding the entries."""
262267
return self.relationship.property # type: ignore[return-value]
263268

264269
@property
265270
def single(self) -> bool:
271+
"""Whether the collection holds one entry at most."""
266272
return not self.binding.is_multivalued
267273

268274
@property
269275
def simple(self) -> bool:
276+
"""Whether the collection holds the values of a multi-valued simple attribute."""
270277
return None in self.columns
271278

272279
def column(self, sub_field_name: str | None) -> _Column | None:
@@ -363,6 +370,7 @@ def schemas(self) -> list[Schema]:
363370
return [self._restrict(model.to_schema()) for model in models]
364371

365372
def _restrict(self, schema: Schema) -> Schema:
373+
"""Return a schema with the attributes the mapping stores only."""
366374
attributes = []
367375
for attribute in schema.attributes or []:
368376
restricted = self._restrict_attribute(
@@ -458,6 +466,7 @@ def _all_collections(self) -> Iterator[_Collection]:
458466
yield from collections
459467

460468
def _column(self, path: str) -> _Column:
469+
"""Return the column of the attribute at a path."""
461470
binding = self._resolve(path)
462471
return self._columns[
463472
(binding.model, binding.field_name, binding.sub_field_name)
@@ -596,15 +605,18 @@ def _display(self) -> _Column | None:
596605
return None
597606

598607
def _find(self, path: str) -> AttributeBinding | None:
608+
"""Return the attribute at a path, or None if the model has none there."""
599609
return Path[self.model](path).resolve() # type: ignore[name-defined]
600610

601611
def _resolve(self, path: str) -> AttributeBinding:
612+
"""Return the attribute at a path, refusing a path the model does not know."""
602613
binding = self._find(path)
603614
if binding is None:
604615
raise ValueError(f"{path!r} is not an attribute of {self.model.__name__}")
605616
return binding
606617

607618
def _add(self, path: str, value: Any) -> None:
619+
"""Check the mapping of the attribute at a path, and keep its column or its collections."""
608620
binding = self._resolve(path)
609621
target = binding.target_type
610622
is_complex = isclass(target) and issubclass(target, BaseModel)

‎src/scim2_sqlalchemy/query.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def _exact_key(column: _Column, value: str) -> Any:
8181

8282

8383
def _unmapped(binding: AttributeBinding) -> InvalidFilterException:
84+
"""Return the error of a filter on an attribute the mapping does not let filters read."""
8485
return InvalidFilterException(detail=f"Cannot filter on '{binding.urn}'")
8586

8687

@@ -156,11 +157,16 @@ def _targets(
156157
def _in_collection(
157158
self, collection: _Collection | None, condition: ColumnElement[bool]
158159
) -> ColumnElement[bool]:
160+
"""Apply a condition to the entries of a collection, or to the record itself outside of any collection.
161+
162+
Within a value selection, the condition already applies to the entries.
163+
"""
159164
if collection is None or self.scope is not None:
160165
return condition
161166
return collection.exists(condition)
162167

163168
def visit_comparison(self, node: Comparison) -> ColumnElement[bool]:
169+
"""Keep the records with a value matching the comparison, in any collection holding the attribute."""
164170
binding = self._resolve(node.attr_path, self.filter.resolve_comparison)
165171
if binding is None:
166172
return false()
@@ -194,6 +200,10 @@ def visit_comparison(self, node: Comparison) -> ColumnElement[bool]:
194200
)
195201

196202
def visit_present(self, node: Present) -> ColumnElement[bool]:
203+
"""Keep the records with a value for the attribute.
204+
205+
A complex attribute is present when one of its sub-attributes is.
206+
"""
197207
binding = self._resolve(node.attr_path, self.filter.resolve)
198208
if binding is None:
199209
return false()
@@ -217,13 +227,16 @@ def visit_present(self, node: Present) -> ColumnElement[bool]:
217227
return or_(*(_present(column) for column in sub_columns))
218228

219229
def visit_not(self, node: Not) -> ColumnElement[bool]:
230+
"""Keep the records the inner filter does not keep."""
220231
return not_(self.visit(node.expr))
221232

222233
def visit_logical_expr(self, node: LogicalExpr) -> ColumnElement[bool]:
234+
"""Combine the conditions of the terms with AND or OR."""
223235
combine = and_ if node.op == LogicalOperator.and_ else or_
224236
return combine(*(self.visit(term) for term in node.terms))
225237

226238
def visit_value_path(self, node: ValuePath) -> ColumnElement[bool]:
239+
"""Keep the records with an entry of the attribute matching the inner filter."""
227240
binding = self.filter.resolve(node.attr_path, strict=False)
228241
if binding is None:
229242
return false()
@@ -361,6 +374,7 @@ def _where(
361374
search_request: SearchRequest[Any],
362375
comparator: _Comparator,
363376
) -> ColumnElement[bool]:
377+
"""Return the condition keeping the records matching the filter of a search, or every record without filter."""
364378
if search_request.filter is None:
365379
return true()
366380
scim_filter = ScimFilter[mapping.model](str(search_request.filter)) # type: ignore[name-defined]
@@ -375,6 +389,7 @@ def _cursor_page(page: Select[Any], search_request: SearchRequest[Any]) -> Selec
375389

376390

377391
def _index_page(page: Select[Any], search_request: SearchRequest[Any]) -> Select[Any]:
392+
"""Limit a page to the records from startIndex, at most count of them."""
378393
page = page.offset(search_request.start_index_0 or 0)
379394
if search_request.count is None:
380395
return page

‎src/scim2_sqlalchemy/storage.py‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252

5353

5454
def _utcnow() -> datetime.datetime:
55+
"""Return the current time, in UTC."""
5556
return datetime.datetime.now(datetime.UTC)
5657

5758

@@ -129,11 +130,11 @@ def _cursor_page(
129130

130131

131132
def _nothing() -> None:
132-
pass
133+
"""Check nothing, as the default uniqueness check of a flush."""
133134

134135

135136
async def _nothing_async() -> None:
136-
pass
137+
"""Check nothing, as the default uniqueness check of a flush."""
137138

138139

139140
class _StorageBase:
@@ -156,6 +157,7 @@ def __init__(
156157
mapping._bind(self.mappings)
157158

158159
def _mapping(self, resource_type: ResourceType) -> ResourceMapping:
160+
"""Return the mapping of a resource type."""
159161
return self.mappings[resource_type.name] # type: ignore[index]
160162

161163
def _root_page(
@@ -253,6 +255,7 @@ def _check_storable(
253255
def _not_found(
254256
resource_type: ResourceType, resource_id: str | None
255257
) -> NotFoundException:
258+
"""Return the error of a resource that does not exist."""
256259
return NotFoundException(
257260
detail=f"{resource_type.name} {resource_id!r} not found"
258261
)
@@ -261,6 +264,7 @@ def _not_found(
261264
def _check_version(
262265
mapping: ResourceMapping, record: Any, expected_version: str | None
263266
) -> None:
267+
"""Refuse to change a record whose version is not the expected one."""
264268
if (
265269
expected_version is not None
266270
and _version(mapping, record) != expected_version
@@ -421,6 +425,7 @@ def get(
421425
*,
422426
response_parameters: ResponseParameters[Any] | None = None,
423427
) -> Resource[Any]:
428+
"""Return a resource, with the attributes the response keeps."""
424429
record = self._load(resource_type, resource_id, response_parameters)
425430
return self._to_scim(resource_type, record, response_parameters)
426431

@@ -431,6 +436,7 @@ def search(
431436
*,
432437
position: Any = None,
433438
) -> SearchPage:
439+
"""Return a page of the resources matching a search, on one resource type or at the root."""
434440
if not resource_types:
435441
return SearchPage(0, [])
436442
session = self.session()
@@ -464,6 +470,7 @@ def search(
464470
def create(
465471
self, resource_type: ResourceType, resource: Resource[Any]
466472
) -> Resource[Any]:
473+
"""Store a new resource, and return it as stored."""
467474
mapping = self._mapping(resource_type)
468475
self._check_storable(resource_type, resource, None)
469476
self._check_uniqueness(mapping, resource)
@@ -484,6 +491,7 @@ def update(
484491
*,
485492
expected_version: str | None = None,
486493
) -> Resource[Any]:
494+
"""Replace a resource, and return it as stored."""
487495
mapping = self._mapping(resource_type)
488496
record = self._load(resource_type, resource.id)
489497
self._check_version(mapping, record, expected_version)
@@ -503,6 +511,7 @@ def delete(
503511
*,
504512
expected_version: str | None = None,
505513
) -> None:
514+
"""Delete a resource."""
506515
mapping = self._mapping(resource_type)
507516
record = self._load(resource_type, resource_id)
508517
self._check_version(mapping, record, expected_version)
@@ -526,6 +535,7 @@ def _load(
526535
def _check_uniqueness(
527536
self, mapping: ResourceMapping, resource: Resource[Any]
528537
) -> None:
538+
"""Refuse a resource with a unique value another resource already has."""
529539
session = self.session()
530540
comparator = self._ready_comparator(session)
531541
for statement in self._unique_statements(mapping, resource, comparator):
@@ -542,6 +552,7 @@ def _ready_comparator(self, session: Session) -> _Comparator:
542552
def _links(
543553
self, mapping: ResourceMapping, resource: Resource[Any]
544554
) -> dict[_Collection, list[Any]]:
555+
"""Return the records each Link of a resource links to, refusing a value that links to nothing."""
545556
links: dict[_Collection, list[Any]] = {}
546557
for collections, entries in _link_entries(mapping, resource):
547558
ids = [entry.value for entry in entries]
@@ -620,6 +631,7 @@ async def get(
620631
*,
621632
response_parameters: ResponseParameters[Any] | None = None,
622633
) -> Resource[Any]:
634+
"""Return a resource, with the attributes the response keeps."""
623635
record = await self._load(resource_type, resource_id, response_parameters)
624636
return self._to_scim(resource_type, record, response_parameters)
625637

@@ -630,6 +642,7 @@ async def search(
630642
*,
631643
position: Any = None,
632644
) -> SearchPage:
645+
"""Return a page of the resources matching a search, on one resource type or at the root."""
633646
if not resource_types:
634647
return SearchPage(0, [])
635648
session = self.session()
@@ -663,6 +676,7 @@ async def search(
663676
async def create(
664677
self, resource_type: ResourceType, resource: Resource[Any]
665678
) -> Resource[Any]:
679+
"""Store a new resource, and return it as stored."""
666680
mapping = self._mapping(resource_type)
667681
self._check_storable(resource_type, resource, None)
668682
await self._check_uniqueness(mapping, resource)
@@ -683,6 +697,7 @@ async def update(
683697
*,
684698
expected_version: str | None = None,
685699
) -> Resource[Any]:
700+
"""Replace a resource, and return it as stored."""
686701
mapping = self._mapping(resource_type)
687702
record = await self._load(resource_type, resource.id)
688703
self._check_version(mapping, record, expected_version)
@@ -703,6 +718,7 @@ async def delete(
703718
*,
704719
expected_version: str | None = None,
705720
) -> None:
721+
"""Delete a resource."""
706722
mapping = self._mapping(resource_type)
707723
record = await self._load(resource_type, resource_id)
708724
self._check_version(mapping, record, expected_version)
@@ -730,6 +746,7 @@ async def _load(
730746
async def _check_uniqueness(
731747
self, mapping: ResourceMapping, resource: Resource[Any]
732748
) -> None:
749+
"""Refuse a resource with a unique value another resource already has."""
733750
session = self.session()
734751
comparator = await self._ready_comparator(session)
735752
for statement in self._unique_statements(mapping, resource, comparator):
@@ -747,6 +764,7 @@ async def _ready_comparator(self, session: AsyncSession) -> _Comparator:
747764
async def _links(
748765
self, mapping: ResourceMapping, resource: Resource[Any]
749766
) -> dict[_Collection, list[Any]]:
767+
"""Return the records each Link of a resource links to, refusing a value that links to nothing."""
750768
links: dict[_Collection, list[Any]] = {}
751769
for collections, entries in _link_entries(mapping, resource):
752770
ids = [entry.value for entry in entries]

0 commit comments

Comments
 (0)