async def test_expire(): resource = memory_resource(key_type=str, value_type=str, expire=0.01) await resource["1"].put("foo") await resource["1"].get() sleep(0.01) with pytest.raises(NotFoundError): await resource["1"].get()
async def test_clear(): resource = memory_resource(key_type=str, value_type=str) await resource["1"].put("foo") await resource["2"].put("bar") assert len(await resource.get()) == 2 await resource.clear() assert len(await resource.get()) == 0
async def test_gpdl_bytes(): resource = memory_resource(key_type=str, value_type=bytes) data = b"\x00\x0e\0x01\0x01\0x00" id = "binary" await resource[id].put(data) assert await resource.get() == [id] assert await resource[id].get() == data data = bytes((1, 2, 3, 4, 5)) await resource[id].put(data) assert await resource[id].get() == data await resource[id].delete() assert await resource.get() == []
async def test_gpdl_str(): resource = memory_resource(key_type=str, value_type=str) data = "你好,世界!" id = "hello_world" await resource[id].put(data) assert await resource.get() == [id] assert await resource[id].get() == data data = "Goodbye world!" await resource[id].put(data) assert await resource[id].get() == data await resource[id].delete() assert await resource.get() == []
async def test_gpdl_dict(): DC = make_dataclass("DC", [("foo", str), ("bar", int)]) resource = memory_resource(key_type=str, value_type=DC) id = "id1" r1 = DC("hello", 1) await resource[id].put(r1) r2 = await resource[id].get() assert r1 == r2 r1.bar = 2 await resource[id].put(r1) r2 = await resource[id].get() assert r1 == r2 await resource[id].delete() assert await resource.get() == []
def row_resource_class( table: Table, cache_size: int = 0, cache_expire: Union[int, float] = 1, ) -> type: """ Return a base class for a row resource. Parameters: • table: table for which row resource is based • cache_size: number of rows to cache (evict least recently cached) • cache_expire: expire time for cached values in seconds """ pk_type = table.columns[table.pk] cache = (memory_resource( key_type=pk_type, value_type=table.schema, size=cache_size, evict=True, expire=cache_expire, ) if cache_size else None) @resource class RowResource: """Row resource.""" def __init__(self, pk: pk_type): self.table = table self.pk = pk async def _validate(self, value: table.schema): """Validate value; raise ValidationError if invalid.""" validate(value, table.schema) async def _read(self): if cache: with suppress(NotFoundError): return await cache[self.pk].get() row = await table.read(self.pk) if not row: raise NotFoundError if cache: await cache[self.pk].put(row) return row async def _insert(self, value: table.schema): if getattr(value, table.pk) != self.pk: raise ValidationError("primary key mismatch") await table.insert(value) if cache: await cache[self.pk].put(value) async def _update(self, old: table.schema, new: table.schema): if getattr(new, table.pk) != self.pk: raise ValidationError("primary key modified") if old != new: stmt = Statement(f"UPDATE {table.name} SET ") updates = [] for name, python_type in table.columns.items(): ofield = getattr(old, name) nfield = getattr(new, name) if ofield != nfield: updates.append( Expression(f"{name} = ", Param(nfield, python_type))) stmt += Expression.join(updates, ", ") stmt += Expression(f" WHERE {table.pk} = ", Param(self.pk, pk_type), ";") await table.database.execute(stmt) if cache: await cache[self.pk].put(new) @operation async def get(self) -> table.schema: """Get row from table.""" if cache: with suppress(NotFoundError): return await cache[self.pk].get() async with table.database.transaction(): row = await table.read(self.pk) if not row: raise NotFoundError if cache: await cache[self.pk].put(row) return row @operation async def put(self, value: table.schema): """Insert or update (upsert) row.""" await self._validate(value) async with table.database.transaction(): try: old = await self._read() await self._update(old, value) except NotFoundError: await self._insert(value) @operation async def patch(self, body: dict[str, Any]): """Modify row. Patch body is a JSON Merge Patch document.""" async with table.database.transaction(): old = await self._read() try: new = json_merge_patch(value=old, type=table.schema, patch=body) except DecodeError as de: raise BadRequestError from de await self._validate(new) await self._update(old, new) @operation async def delete(self) -> None: """Delete row.""" if cache: with suppress(NotFoundError): await cache[self.pk].delete() async with table.database.transaction(): await table.delete(self.pk) @query async def exists(self) -> bool: """Return if row exists.""" if cache: with suppress(NotFoundError): await cache[self.pk].get() return True where = Expression(f"{table.pk} = ", Param(self.pk, table.columns[table.pk])) async with table.database.transaction(): return await table.count(where) != 0 fondat.types.affix_type_hints(RowResource, localns=locals()) return RowResource
async def test_size_evict(): resource = memory_resource(key_type=str, value_type=str, size=2, evict=True) await resource["1"].put("foo") await resource["2"].put("bar") await resource["3"].put("qux") assert set(await resource.get()) == {"2", "3"}
async def test_size_limit(): resource = memory_resource(key_type=str, value_type=str, size=1) await resource["1"].put("foo") with pytest.raises(InternalServerError): await resource["2"].put("bar")
async def test_delete_notfound(): resource = memory_resource(key_type=str, value_type=str) with pytest.raises(NotFoundError): await resource["1"].delete()