Skip to content

Commit 06140db

Browse files
committed
Add general improvement to e3.anod.store.*
This commit improves our current store implementation in several ways: - It improves typing to accept any `os.PathLike[str]` object when possible. - Change the time format stored on the database (the space between the date and the hour has been replaced by a `T`). - Replace the `_id` field of `e3.anod.store.file.ResourceDict` by `id`. - The metadata field is set to an empty dict by default instead of `None`. - Transform the local function `e3.anod.store.StoreWriteOnly.submit_component.insert_to_component_file` to a default class instance method for future usage. - Fix some typos and string errors. Also, improve logging. - Add the `_add_component_attachment` method. This method does not commit the database changes at the end of the function. This is done for optimization purposes. Related to it/org/software-supply-chain/production-pipeline/issues#1
1 parent 9197922 commit 06140db

File tree

5 files changed

+104
-65
lines changed

5 files changed

+104
-65
lines changed

src/e3/anod/store/__init__.py

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -201,23 +201,24 @@ class TableName(StrEnum):
201201
component_releases = "component_releases"
202202
components = "components"
203203

204-
def __init__(self, db: os.PathLike | None = None) -> None:
204+
def __init__(self, db: os.PathLike[str] | str | None = None) -> None:
205205
"""Initialize the Store class.
206206
207207
This will create the database and its tables (if needed).
208208
209209
:param db: A path to the database. If None, a new database is created with the
210210
name `.store.db`.
211211
"""
212-
self.connection = sqlite3.connect(db or ".store.db")
212+
self.db_path = os.fspath(db or ".store.db")
213+
self.connection = sqlite3.connect(self.db_path)
213214
self.cursor = self.connection.cursor()
214215
self.cursor.execute(
215216
f"CREATE TABLE IF NOT EXISTS {_Store.TableName.buildinfos}("
216217
" id TEXT NOT NULL PRIMARY KEY,"
217218
" build_date TEXT NOT NULL,"
218219
" setup TEXT NOT NULL,"
219220
" creation_date TEXT NOT NULL DEFAULT("
220-
" STRFTIME('%Y-%m-%d %H:%M:%f+00:00', 'now')"
221+
" STRFTIME('%Y-%m-%dT%H:%M:%f+00:00', 'now')"
221222
" ),"
222223
" build_version TEXT NOT NULL,"
223224
" isready INTEGER NOT NULL DEFAULT 0 CHECK(isready in (0, 1))"
@@ -230,7 +231,7 @@ def __init__(self, db: os.PathLike | None = None) -> None:
230231
" path TEXT NOT NULL,"
231232
" size INTEGER NOT NULL,"
232233
" creation_date TEXT NOT NULL DEFAULT("
233-
" STRFTIME('%Y-%m-%d %H:%M:%f+00:00', 'now')"
234+
" STRFTIME('%Y-%m-%dT%H:%M:%f+00:00', 'now')"
234235
" )"
235236
")"
236237
)
@@ -246,7 +247,7 @@ def __init__(self, db: os.PathLike | None = None) -> None:
246247
" revision TEXT NOT NULL,"
247248
" metadata TEXT NOT NULL,"
248249
" creation_date TEXT NOT NULL DEFAULT("
249-
" STRFTIME('%Y-%m-%d %H:%M:%f+00:00', 'now')"
250+
" STRFTIME('%Y-%m-%dT%H:%M:%f+00:00', 'now')"
250251
" )"
251252
")"
252253
)
@@ -259,7 +260,7 @@ def __init__(self, db: os.PathLike | None = None) -> None:
259260
" internal INTEGER NOT NULL DEFAULT 1 CHECK(internal IN (0, 1)),"
260261
" attachment_name TEXT,"
261262
" CHECK("
262-
" (attachment_name IS NOT NULL AND kind='attachment')"
263+
" (attachment_name IS NOT NULL AND kind='attachment') "
263264
" OR kind IN ('file', 'source')"
264265
" )"
265266
")"
@@ -280,7 +281,7 @@ def __init__(self, db: os.PathLike | None = None) -> None:
280281
" specname TEXT," # Can be Null
281282
" build_id TEXT NOT NULL,"
282283
" creation_date TEXT NOT NULL DEFAULT("
283-
" STRFTIME('%Y-%m-%d %H:%M:%f+00:00', 'now')"
284+
" STRFTIME('%Y-%m-%dT%H:%M:%f+00:00', 'now')"
284285
" ),"
285286
" is_valid INTEGER NOT NULL DEFAULT 1 CHECK(is_valid in (0, 1)),"
286287
" is_published INTEGER NOT NULL DEFAULT 0 CHECK(is_published in (0, 1)),"
@@ -341,6 +342,7 @@ def _select(
341342
if where_clause:
342343
sql = f"{sql}WHERE {where_clause} "
343344
sql = f"{sql}ORDER BY {order_by}"
345+
344346
return self.cursor.execute(sql, dynamic_where_values).fetchall()
345347

346348
def _select_one(
@@ -397,7 +399,7 @@ def _tuple_to_resource(cls, req_tuple: _Store.ResourceTuple) -> ResourceDict:
397399
"""
398400
_, resource_id, path, size, creation_date = req_tuple
399401
return {
400-
"_id": resource_id,
402+
"id": resource_id,
401403
"path": path,
402404
"size": size,
403405
"creation_date": creation_date,
@@ -447,7 +449,7 @@ def _tuple_to_file(
447449
self._select_one(_Store.TableName.buildinfos, build_id) # type: ignore[arg-type]
448450
)
449451

450-
if not resource or resource["_id"] != resource_id:
452+
if not resource or resource["id"] != resource_id:
451453
resource = self._tuple_to_resource(
452454
self._select_one(
453455
_Store.TableName.resources, resource_id, field_name="resource_id" # type: ignore[arg-type]
@@ -461,7 +463,7 @@ def _tuple_to_file(
461463
"alias": alias,
462464
"filename": filename,
463465
"revision": revision,
464-
"metadata": json.loads(metadata) if metadata else None,
466+
"metadata": json.loads(metadata) if metadata else {},
465467
"build_id": str(build_id),
466468
"resource_id": resource_id,
467469
"build": buildinfo,
@@ -729,7 +731,8 @@ def _tuple_to_comp(
729731
else self._list_component_files(
730732
"attachment", comp_id, component_buildinfo=buildinfo
731733
)
732-
),
734+
)
735+
or None,
733736
"build": buildinfo,
734737
}
735738

@@ -822,29 +825,29 @@ def create_thirdparty(self, file_info: FileDict) -> FileDict:
822825
file_info["revision"] = ""
823826
return self.submit_file(file_info)
824827

825-
def submit_component(self, component_info: ComponentDict) -> ComponentDict:
826-
"""See e3.anod.store.interface.StoreWriteInterface."""
828+
def _insert_to_component_files(
829+
self,
830+
kind: Literal["file"] | Literal["source"] | Literal["attachment"],
831+
file_list: Sequence[tuple[str | None, FileDict]],
832+
component_id: str | int,
833+
) -> None:
834+
"""Insert a list of files to the component_files database table.
827835
828-
def insert_to_component_files(
829-
kind: Literal["file"] | Literal["source"] | Literal["attachment"],
830-
file_list: Sequence[tuple[str | None, FileDict]],
831-
component_id: str | int,
832-
) -> None:
833-
"""Insert a list of files to the component_files database table.
834-
835-
:param kind: The kind of file to insert.
836-
:param file_list: A sequence of tuples. The first element of this tuple must
837-
be None if the kind != attachment. Otherwise, this represents the
838-
attachment name. The second element should always be a FileDict.
839-
:param component_id: The component id linked to these files.
840-
"""
841-
for att_name, f in file_list:
842-
self._insert(
843-
_Store.TableName.component_files,
844-
["kind", "file_id", "component_id", "attachment_name"], # type: ignore[arg-type]
845-
[kind, f["_id"], component_id, att_name],
846-
)
836+
:param kind: The kind of file to insert.
837+
:param file_list: A sequence of tuples. The first element of this tuple must
838+
be None if the kind != attachment. Otherwise, this represents the
839+
attachment name. The second element should always be a FileDict.
840+
:param component_id: The component id linked to these files.
841+
"""
842+
for att_name, f in file_list:
843+
self._insert(
844+
_Store.TableName.component_files,
845+
["kind", "file_id", "component_id", "attachment_name"], # type: ignore[arg-type]
846+
[kind, f["_id"], component_id, att_name],
847+
)
847848

849+
def submit_component(self, component_info: ComponentDict) -> ComponentDict:
850+
"""See e3.anod.store.interface.StoreWriteInterface."""
848851
# Upload only readmes, binaries and attachments, as sources are supposed to be
849852
# already there.
850853

@@ -859,7 +862,7 @@ def insert_to_component_files(
859862
# Retrieve binaries an upload them.
860863
files = [self._submit_file(file_info) for file_info in component_info["files"]]
861864

862-
# Retrieve attachment an upload them.
865+
# Retrieve attachments and upload them.
863866
attachments_with_name: dict[str, FileDict] = {}
864867
attachments = component_info.get("attachments")
865868
if attachments:
@@ -914,10 +917,12 @@ def insert_to_component_files(
914917
)
915918
# Create relation between files/sources/attachment and the new component.
916919
comp_id = req_tuple[0]
917-
insert_to_component_files("file", [(None, f) for f in files], comp_id)
920+
self._insert_to_component_files("file", [(None, f) for f in files], comp_id)
918921
sources = component_info["sources"]
919-
insert_to_component_files("source", [(None, src) for src in sources], comp_id)
920-
insert_to_component_files(
922+
self._insert_to_component_files(
923+
"source", [(None, src) for src in sources], comp_id
924+
)
925+
self._insert_to_component_files(
921926
"attachment", list(attachments_with_name.items()), comp_id
922927
)
923928
# Add the list of releases linked to this component.
@@ -1002,12 +1007,12 @@ def update_file_metadata(self, file_info: FileDict) -> FileDict:
10021007
_Store.TableName.files,
10031008
fid,
10041009
["metadata"], # type: ignore[arg-type]
1005-
[json.dumps(file_info["metadata"]) if file_info["metadata"] else ""],
1010+
[json.dumps(file_info["metadata"]) if file_info["metadata"] else "{}"],
10061011
)
10071012
self.connection.commit()
10081013
return self._tuple_to_file(req_tuple, buildinfo=buildinfo) # type: ignore[arg-type]
10091014

1010-
def add_component_attachment(
1015+
def _add_component_attachment(
10111016
self, component_id: str, file_id: str, name: str
10121017
) -> None:
10131018
"""See e3.anod.store.interface.StoreWriteInterface."""
@@ -1016,6 +1021,12 @@ def add_component_attachment(
10161021
["kind", "file_id", "component_id", "attachment_name"], # type: ignore[arg-type]
10171022
["attachment", file_id, component_id, name],
10181023
)
1024+
1025+
def add_component_attachment(
1026+
self, component_id: str, file_id: str, name: str
1027+
) -> None:
1028+
"""See e3.anod.store.interface.StoreWriteInterface."""
1029+
self._add_component_attachment(component_id, file_id, name)
10191030
self.connection.commit()
10201031

10211032
### PRIVATE ###
@@ -1087,7 +1098,7 @@ def _submit_file(self, file_info: FileDict) -> FileDict:
10871098
file_info["kind"],
10881099
resource_id,
10891100
file_info["revision"],
1090-
json.dumps(file_info["metadata"]) if file_info["metadata"] else "",
1101+
json.dumps(file_info["metadata"]) if file_info["metadata"] else "{}",
10911102
],
10921103
)
10931104
res = self._tuple_to_file(req_tuple, resource=resource) # type: ignore[arg-type]
@@ -1258,7 +1269,7 @@ def get_build_info_list(
12581269
date = date or "all"
12591270

12601271
static_where_rules: tuple[str, ...]
1261-
if date == "all":
1272+
if not date or date == "all":
12621273
static_where_rules = ()
12631274
else:
12641275
assert len(date) == 8

src/e3/anod/store/buildinfo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def __init__(
7979
build_version: str,
8080
isready: bool,
8181
store: StoreReadInterface | StoreRWInterface | None = None,
82-
):
82+
) -> None:
8383
"""Initialize the BuildInfo object.
8484
8585
:param build_date: Same as the attribute.

src/e3/anod/store/component.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def __init__(
115115
else:
116116
self.attachments = attachments or {}
117117

118-
self.releases = releases
118+
self.releases = releases or []
119119
self.is_valid = is_valid
120120
self.is_published = is_published
121121
self.build_info = build_info
@@ -583,7 +583,8 @@ def __eq__(self, other: object) -> bool:
583583
val = getattr(other, attr_name)
584584
if val != attr_val:
585585
logger.debug(
586-
f"Component attribute {attr_name} differ: {val!r} != {attr_val!r}"
586+
f"Component attribute {attr_name!r} differ: "
587+
f"other.{attr_name} = {val!r}, self.{attr_name} = {attr_val!r}"
587588
)
588589
return False
589590
return True

src/e3/anod/store/file.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
from e3.anod.store.buildinfo import BuildInfo, BuildInfoDict
2424

2525
class ResourceDict(TypedDict):
26-
_id: str
27-
path: os.PathLike | str
26+
id: str
27+
path: os.PathLike[str] | str
2828
size: int
2929
creation_date: str
3030

@@ -103,24 +103,29 @@ def __init__(
103103
build_info: BuildInfo | None = None,
104104
metadata: dict[str, Any] | None = None,
105105
store: StoreReadInterface | StoreRWInterface | None = None,
106-
resource_path: str | None = None,
106+
resource_path: os.PathLike[str] | str | None = None,
107107
unpack_dir: str | None = None,
108108
) -> None:
109109
"""Initialize a File.
110110
111-
:param file_id: see corresponding attribute
112111
:param build_id: see corresponding attribute.
113-
:param kind: see corresponding attribute
114-
:param name: see corresponding attribute
115-
:param resource_id: see corresponding attribute
116-
:param filename: see corresponding attribute
117-
:param internal: see corresponding attribute (default to True)
118-
:param alias: see corresponding attribute (if None, filename is used)
119-
:param revision: see corresponding attribute (default is '')
120-
:param build_info: see corresponding attribute (default is None)
121-
:param metadata: see corresponding attribute (default is None)
122-
:param store: a store instance
112+
:param kind: see corresponding attribute.
113+
:param name: see corresponding attribute.
114+
:param filename: see corresponding attribute.
115+
:param resource_id: see corresponding attribute.
116+
:param file_id: see corresponding attribute.
117+
:param internal: see corresponding attribute (default to True).
118+
:param alias: see corresponding attribute (if None, filename is used).
119+
:param revision: see corresponding attribute (default is '').
120+
:param build_info: see corresponding attribute (default is None).
121+
:param metadata: see corresponding attribute (default is None).
122+
:param store: a store instance.
123+
:param resource_path: the path of the file in the current computer. Generaly
124+
used to create a file to push.
125+
:param unpack_dir: the directory to unpack the file (if the file is an archive).
123126
"""
127+
if resource_path:
128+
resource_path = os.fspath(resource_path)
124129
assert file_id is None or isinstance(
125130
file_id, str
126131
), f"invalid file_id: {file_id}"
@@ -188,12 +193,14 @@ def __update(self, file: File) -> None:
188193
self.unpack_dir = file.unpack_dir
189194
self.build_info = file.build_info
190195

191-
def bind_to_resource(self, path: str) -> None:
196+
def bind_to_resource(self, path: os.PathLike[str] | str) -> None:
192197
"""Bind this File to the file at the given path.
193198
194199
Unless self.resource_id is already set, this also sets self.resource_id
195200
using this file's contents (see e3.anod.store.interface.resource_id for
196201
more info on that).
202+
203+
:param path: the file path on the current computer.
197204
"""
198205
if self.resource_id is None:
199206
self.resource_id = store_resource_id(path)
@@ -474,15 +481,31 @@ def load(
474481
if "build" in data:
475482
build_info = BuildInfo.load(data["build"])
476483

484+
# The `internal` default field value is related to the file kind.
485+
#
486+
# `internal` is used to said if some files must be distributed with a component
487+
# or not.
488+
#
489+
# For a binary file, the `internal` default value is `False` because this is a
490+
# file generated by a component build.
491+
#
492+
# For any other file type, if internal is not specified, then the default value
493+
# is `True` for security reason: If internal is not specified by mistake,
494+
# the risk of potential leaks is reduced.
495+
kind = data["kind"]
496+
internal = data.get("internal")
497+
if internal is None:
498+
internal = kind != "binary"
499+
477500
try:
478501
result = File(
479502
file_id=data["_id"],
480503
build_id=data["build_id"],
481-
kind=FileKind(data["kind"]),
504+
kind=FileKind(kind),
482505
name=data["name"],
483506
resource_id=data["resource_id"],
484507
filename=data["filename"],
485-
internal=bool(data.get("internal")),
508+
internal=bool(internal),
486509
alias=data.get("alias"),
487510
revision=data.get("revision", ""),
488511
metadata=data.get("metadata"),
@@ -657,8 +680,12 @@ def __eq__(self, other: object) -> bool:
657680
if attr_name in ("downloaded_as", "unpack_dir", "store"):
658681
# This attribute is not meaningful when comparing two files.
659682
continue
660-
if getattr(other, attr_name) != attr_val:
661-
logger.debug(f"File attribute {attr_name} is different")
683+
val = getattr(other, attr_name)
684+
if val != attr_val:
685+
logger.debug(
686+
f"File attribute {attr_name!r} is different: "
687+
f"other.{attr_name} = {val!r}, self.{attr_name} = {attr_val!r}"
688+
)
662689
return False
663690
return True
664691

tests/tests_e3/anod/store/store_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ def test_file(store):
8585
assert f["alias"] == "test.txt"
8686
assert f["filename"] == "test.txt"
8787
assert f["revision"] == ""
88-
assert f["metadata"] is None
88+
assert f["metadata"] == {}
8989
assert f["build_id"] == buildinfo["_id"]
9090
assert "resource_id" in f
91-
assert "_id" in f["resource"]
92-
assert f["resource"]["_id"] == f["resource_id"]
91+
assert "id" in f["resource"]
92+
assert f["resource"]["id"] == f["resource_id"]
9393
assert f["resource"]["path"] == path_abs
9494
assert f["resource"]["size"] == 1
9595
date = datetime.fromisoformat(f["resource"]["creation_date"]).replace(

0 commit comments

Comments
 (0)