stepup 3.2.2__tar.gz → 3.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {stepup-3.2.2/stepup.egg-info → stepup-3.2.3}/PKG-INFO +1 -1
  2. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/browse.py +6 -4
  3. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/cascade.py +3 -0
  4. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/clean.py +4 -3
  5. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/director.py +2 -2
  6. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/file.py +7 -3
  7. stepup-3.2.3/stepup/core/sqlite3.py +83 -0
  8. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/startup.py +1 -1
  9. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/step.py +2 -2
  10. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/utils.py +1 -17
  11. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/workflow.py +8 -6
  12. {stepup-3.2.2 → stepup-3.2.3/stepup.egg-info}/PKG-INFO +1 -1
  13. {stepup-3.2.2 → stepup-3.2.3}/stepup.egg-info/SOURCES.txt +1 -0
  14. {stepup-3.2.2 → stepup-3.2.3}/LICENSE +0 -0
  15. {stepup-3.2.2 → stepup-3.2.3}/MANIFEST.in +0 -0
  16. {stepup-3.2.2 → stepup-3.2.3}/README.md +0 -0
  17. {stepup-3.2.2 → stepup-3.2.3}/pyproject.toml +0 -0
  18. {stepup-3.2.2 → stepup-3.2.3}/setup.cfg +0 -0
  19. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/__init__.py +0 -0
  20. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/__main__.py +0 -0
  21. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/actions.py +0 -0
  22. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/api.py +0 -0
  23. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/asyncio.py +0 -0
  24. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/call.py +0 -0
  25. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/deferred_glob.py +0 -0
  26. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/enums.py +0 -0
  27. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/exceptions.py +0 -0
  28. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/hash.py +0 -0
  29. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/interact.py +0 -0
  30. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/job.py +0 -0
  31. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/logo.svg +0 -0
  32. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/nglob.py +0 -0
  33. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/pytest.py +0 -0
  34. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/render_jinja.py +0 -0
  35. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/reporter.py +0 -0
  36. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/rpc.py +0 -0
  37. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/runner.py +0 -0
  38. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/scheduler.py +0 -0
  39. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/script.py +0 -0
  40. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/stepinfo.py +0 -0
  41. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/tui.py +0 -0
  42. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/watcher.py +0 -0
  43. {stepup-3.2.2 → stepup-3.2.3}/stepup/core/worker.py +0 -0
  44. {stepup-3.2.2 → stepup-3.2.3}/stepup.egg-info/dependency_links.txt +0 -0
  45. {stepup-3.2.2 → stepup-3.2.3}/stepup.egg-info/entry_points.txt +0 -0
  46. {stepup-3.2.2 → stepup-3.2.3}/stepup.egg-info/requires.txt +0 -0
  47. {stepup-3.2.2 → stepup-3.2.3}/stepup.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stepup
3
- Version: 3.2.2
3
+ Version: 3.2.3
4
4
  Summary: StepUp Core provides the basic framework for the StepUp build tool
5
5
  Author-email: Toon Verstraelen <toon.verstraelen@ugent.be>
6
6
  License-Expression: GPL-3.0-or-later
@@ -24,7 +24,6 @@ import contextlib
24
24
  import importlib.resources
25
25
  import os
26
26
  import pickle
27
- import sqlite3
28
27
  import stat
29
28
  import traceback
30
29
  from collections.abc import Iterator
@@ -37,6 +36,7 @@ from path import Path
37
36
 
38
37
  from .enums import FileState, Mandatory, StepState
39
38
  from .hash import fmt_digest
39
+ from .sqlite3 import connect
40
40
 
41
41
 
42
42
  def browse_subcommand(subparser: argparse.ArgumentParser) -> callable:
@@ -275,8 +275,8 @@ class GraphServer(BaseHTTPRequestHandler):
275
275
  print("Loading database...")
276
276
  if self.con is not None:
277
277
  self.con.close()
278
- self.con = sqlite3.Connection(":memory:")
279
- src = sqlite3.Connection(self.path_db)
278
+ self.con = connect(":memory:")
279
+ src = connect(self.path_db)
280
280
  try:
281
281
  src.backup(self.con)
282
282
  finally:
@@ -415,7 +415,9 @@ class GraphServer(BaseHTTPRequestHandler):
415
415
 
416
416
  elif kind == "file":
417
417
  (state_i, digest, mode, mtime, size, inode) = self.con.execute(
418
- "SELECT state, digest, mode, mtime, size, inode FROM file WHERE node = ?", (node_i,)
418
+ "SELECT state, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM file "
419
+ "WHERE node = ?",
420
+ (node_i,),
419
421
  ).fetchone()
420
422
  state = FileState(state_i)
421
423
  yield f'<p><b>State:</b> <span class="{state.name.lower()}">{state.name}</span></p>'
@@ -562,6 +562,9 @@ class Cascade:
562
562
  # While making this change, the enums were also made more intuitive.
563
563
  # Schema 2 became outdated due to the worker actions.
564
564
  # Schema 3 became outdated due to a change in step table (dirty field).
565
+
566
+ # Delayed Schema updates, for version 5:
567
+ # - Use UINT64 with PARSE_DECLTYPES instead of PARSE_COLNAMES.
565
568
  return 4
566
569
 
567
570
  @classmethod
@@ -29,7 +29,8 @@ from rich.console import Console
29
29
  from .cascade import DROP_CONSUMERS, INITIAL_CONSUMERS, RECURSE_CONSUMERS
30
30
  from .enums import FileState
31
31
  from .hash import FileHash
32
- from .utils import mynormpath, sqlite3_copy_in_memory, translate, translate_back
32
+ from .sqlite3 import copy_db_in_memory
33
+ from .utils import mynormpath, translate, translate_back
33
34
 
34
35
 
35
36
  def clean_subcommand(subparser: argparse.ArgumentParser) -> callable:
@@ -91,7 +92,7 @@ def clean_tool(args: argparse.Namespace):
91
92
  # Copy the database in memory and work on the copy.
92
93
  root = Path(os.getenv("STEPUP_ROOT", "."))
93
94
  path_db = root / ".stepup/graph.db"
94
- with sqlite3_copy_in_memory(path_db) as con:
95
+ with copy_db_in_memory(path_db) as con:
95
96
  clean(con, tr_paths, args)
96
97
 
97
98
 
@@ -175,7 +176,7 @@ def fmtnum(i: int):
175
176
  CREATE_INITIAL_PATHS = "CREATE TABLE temp.initial_path(path TEXT PRIMARY KEY) WITHOUT ROWID"
176
177
 
177
178
  SELECT_OUTPUTS = f"""
178
- SELECT label, file.state, orphan, digest, mode, mtime, size, inode FROM node
179
+ SELECT label, file.state, orphan, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM node
179
180
  JOIN all_consumer ON node.i = all_consumer.current
180
181
  JOIN file ON file.node = all_consumer.current
181
182
  WHERE file.state in
@@ -24,7 +24,6 @@ import asyncio
24
24
  import logging
25
25
  import os
26
26
  import signal
27
- import sqlite3
28
27
  import sys
29
28
  import time
30
29
  import traceback
@@ -48,6 +47,7 @@ from .reporter import ReporterClient
48
47
  from .rpc import allow_rpc, serve_socket_rpc
49
48
  from .runner import Runner
50
49
  from .scheduler import Scheduler
50
+ from .sqlite3 import connect
51
51
  from .startup import startup_from_db
52
52
  from .step import Step
53
53
  from .stepinfo import StepInfo
@@ -258,7 +258,7 @@ async def serve(
258
258
  check_plan("plan.py")
259
259
 
260
260
  # Create basic components
261
- con = sqlite3.connect(".stepup/graph.db", cached_statements=1024)
261
+ con = connect(".stepup/graph.db")
262
262
  dblock = DBLock(con)
263
263
  workflow = Workflow(con)
264
264
  scheduler = Scheduler(workflow.job_queue, workflow.config_queue, workflow.job_queue_changed)
@@ -29,6 +29,7 @@ from path import Path
29
29
  from .cascade import Node
30
30
  from .enums import DirWatch, FileState
31
31
  from .hash import FileHash
32
+ from .sqlite3 import UInt64
32
33
  from .utils import format_digest
33
34
 
34
35
  if TYPE_CHECKING:
@@ -102,7 +103,10 @@ class File(Node):
102
103
  # If the file was previously BUILT or OUTDATED, and created again as AWAITED,
103
104
  # it should copy that state
104
105
  if state == FileState.AWAITED:
105
- sql = "SELECT state, digest, mode, mtime, size, inode FROM file WHERE node = ?"
106
+ sql = (
107
+ "SELECT state, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM file "
108
+ "WHERE node = ?"
109
+ )
106
110
  row = self.con.execute(sql, (self.i,)).fetchone()
107
111
  if row is not None and row[0] in (FileState.BUILT.value, FileState.OUTDATED.value):
108
112
  state = FileState(row[0])
@@ -121,7 +125,7 @@ class File(Node):
121
125
  "mode": mode,
122
126
  "mtime": mtime,
123
127
  "size": size,
124
- "inode": inode,
128
+ "inode": UInt64(inode),
125
129
  },
126
130
  )
127
131
  # If the state is BUILT, mark it as OUTDATED to force a rebuild.
@@ -215,7 +219,7 @@ class File(Node):
215
219
  self.con.execute(sql, (state.value, self.i))
216
220
 
217
221
  def get_hash(self) -> FileHash:
218
- sql = "SELECT digest, mode, mtime, size, inode FROM file WHERE node = ?"
222
+ sql = "SELECT digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM file WHERE node = ?"
219
223
  row = self.con.execute(sql, (self.i,)).fetchone()
220
224
  return FileHash(*row)
221
225
 
@@ -0,0 +1,83 @@
1
+ # StepUp Core provides the basic framework for the StepUp build tool.
2
+ # Copyright 2024-2026 Toon Verstraelen
3
+ #
4
+ # This file is part of StepUp Core.
5
+ #
6
+ # StepUp Core is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU General Public License
8
+ # as published by the Free Software Foundation; either version 3
9
+ # of the License, or (at your option) any later version.
10
+ #
11
+ # StepUp Core is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program; if not, see <http://www.gnu.org/licenses/>
18
+ #
19
+ # --
20
+ """Wrapper for SQLite3 functionality."""
21
+
22
+ import contextlib
23
+ import os
24
+ import sqlite3
25
+ from collections.abc import Iterator
26
+ from typing import Self
27
+
28
+ __all__ = ("UInt64", "connect", "copy_db_in_memory")
29
+
30
+
31
+ class UInt64(int):
32
+ """A wrapper to tell SQLite this int should be treated as an unsigned 64-bit value."""
33
+
34
+ MAX_SIGNED_64 = 2**63 - 1
35
+ MAX_WRAPAROUND_64 = 2**64
36
+ MAX_UNSIGNED_64 = MAX_WRAPAROUND_64 - 1
37
+
38
+ @staticmethod
39
+ def adapt(val: Self) -> int:
40
+ if not (0 <= val <= UInt64.MAX_UNSIGNED_64):
41
+ raise ValueError(f"Value {val} out of UINT64 range")
42
+ return val - UInt64.MAX_WRAPAROUND_64 if val > UInt64.MAX_SIGNED_64 else val
43
+
44
+ @staticmethod
45
+ def convert(val: bytes) -> Self:
46
+ val = int(val)
47
+ if val < 0:
48
+ val += UInt64.MAX_WRAPAROUND_64
49
+ return UInt64(val)
50
+
51
+
52
+ sqlite3.register_adapter(UInt64, UInt64.adapt)
53
+ sqlite3.register_converter("UINT64", UInt64.convert)
54
+
55
+
56
+ def connect(path: str | os.PathLike[str], **kwargs) -> sqlite3.Connection:
57
+ """Connect to a SQLite database, with the appropriate settings for StepUp.
58
+
59
+ The following deviations from the default settings are used:
60
+
61
+ - Types can be detected from column names,
62
+ which allows us to use the custom UINT64 type for file inodes.
63
+ - The `cached_statements` parameter is set to a large value to improve
64
+ performance when executing many similar statements.
65
+ """
66
+ my_kwargs = {"cached_statements": 1024, "detect_types": sqlite3.PARSE_COLNAMES}
67
+ my_kwargs.update(kwargs)
68
+ return sqlite3.connect(path, **my_kwargs)
69
+
70
+
71
+ @contextlib.contextmanager
72
+ def copy_db_in_memory(path_db) -> Iterator[sqlite3.Connection]:
73
+ """Copy an SQLite database into memory and yield the connection."""
74
+ dst = connect(":memory:")
75
+ try:
76
+ src = connect(path_db)
77
+ try:
78
+ src.backup(dst)
79
+ finally:
80
+ src.close()
81
+ yield dst
82
+ finally:
83
+ dst.close()
@@ -124,7 +124,7 @@ async def scan_file_changes(
124
124
  ) -> tuple[set[str], set[str]]:
125
125
  """Check all files in the workflow for changes."""
126
126
  sql = (
127
- "SELECT label, state, digest, mode, mtime, size, inode "
127
+ "SELECT label, state, digest, mode, mtime, size, inode AS 'inode [UINT64]' "
128
128
  "FROM node JOIN file ON node.i = file.node AND state NOT IN (?, ?) AND NOT orphan"
129
129
  )
130
130
  data = (FileState.AWAITED.value, FileState.VOLATILE.value)
@@ -558,7 +558,7 @@ class Step(Node):
558
558
  fields.append("state")
559
559
  join_file = True
560
560
  if yield_hash:
561
- fields.extend(["digest", "mode", "mtime", "size", "inode"])
561
+ fields.extend(["digest", "mode", "mtime", "size", "inode AS 'inode [UINT64]'"])
562
562
  join_file = True
563
563
  if yield_orphan:
564
564
  fields.append("orphan")
@@ -731,7 +731,7 @@ class Step(Node):
731
731
  sql = (
732
732
  "SELECT node.label, node.orphan, file.state, "
733
733
  "EXISTS (SELECT 1 FROM amended_dep WHERE amended_dep.i = dep.i), "
734
- "file.digest, file.mode, file.mtime, file.size, file.inode "
734
+ "file.digest, file.mode, file.mtime, file.size, file.inode AS 'inode [UINT64]' "
735
735
  "FROM node JOIN dependency AS dep ON node.i = dep.supplier "
736
736
  "JOIN file ON file.node = node.i "
737
737
  "WHERE dep.consumer = ?"
@@ -20,7 +20,6 @@
20
20
  """Small utilities used throughout."""
21
21
 
22
22
  import asyncio
23
- import contextlib
24
23
  import logging
25
24
  import os
26
25
  import re
@@ -28,7 +27,7 @@ import shlex
28
27
  import sqlite3
29
28
  import string
30
29
  import sys
31
- from collections.abc import Collection, Iterator
30
+ from collections.abc import Collection
32
31
 
33
32
  import attrs
34
33
  from path import Path
@@ -429,18 +428,3 @@ def string_to_bool(v: str | bool) -> bool:
429
428
  return False
430
429
  raise ValueError(f"Cannot interpret '{v}' as a boolean value.")
431
430
  raise TypeError(f"Expected a boolean value or string. Got {type(v).__name__}")
432
-
433
-
434
- @contextlib.contextmanager
435
- def sqlite3_copy_in_memory(path_db) -> Iterator[sqlite3.Connection]:
436
- """Copy an SQLite database into memory and yield the connection."""
437
- dst = sqlite3.Connection(":memory:")
438
- try:
439
- src = sqlite3.Connection(path_db)
440
- try:
441
- src.backup(dst)
442
- finally:
443
- src.close()
444
- yield dst
445
- finally:
446
- dst.close()
@@ -39,6 +39,7 @@ from .exceptions import GraphError
39
39
  from .file import File
40
40
  from .hash import FileHash, fmt_digest
41
41
  from .nglob import NGlobMulti, convert_nglob_to_regex, iter_wildcard_names
42
+ from .sqlite3 import UInt64
42
43
  from .step import Step
43
44
  from .utils import myparent, string_to_bool
44
45
 
@@ -568,13 +569,14 @@ class Workflow(Cascade):
568
569
  sql = "INSERT INTO temp.missing VALUES (?)"
569
570
  self.con.executemany(sql, ((file.i,) for file in deferred))
570
571
  sql = (
571
- "SELECT label, digest, mtime, mode, size, inode FROM temp.missing "
572
+ "SELECT label, digest, mode, mtime, size, inode AS 'inode [UINT64]' "
573
+ "FROM temp.missing "
572
574
  "JOIN node ON node.i = temp.missing.node "
573
575
  "JOIN file ON file.node = temp.missing.node"
574
576
  )
575
577
  return [
576
- (path, FileHash(digest, mtime, mode, size, inode))
577
- for path, digest, mtime, mode, size, inode in self.con.execute(sql)
578
+ (path, FileHash(digest, mode, mtime, size, inode))
579
+ for path, digest, mode, mtime, size, inode in self.con.execute(sql)
578
580
  ]
579
581
  finally:
580
582
  self.con.execute("DROP TABLE IF EXISTS temp.missing")
@@ -597,7 +599,7 @@ class Workflow(Cascade):
597
599
  self.con.execute("CREATE TABLE temp.paths(path TEXT PRIMARY KEY)")
598
600
  self.con.executemany("INSERT INTO temp.paths VALUES (?)", ((path,) for path in paths))
599
601
  sql = (
600
- "SELECT label, digest, mode, mtime, size, inode FROM node "
602
+ "SELECT label, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM node "
601
603
  "JOIN file ON file.node = node.i JOIN temp.paths ON label = temp.paths.path"
602
604
  )
603
605
  return [
@@ -735,7 +737,7 @@ class Workflow(Cascade):
735
737
  "UPDATE file SET state = ?, digest = ?, mode = ?, mtime = ?, size = ?, inode = ? "
736
738
  "WHERE node = ?",
737
739
  (
738
- (state.value, fh.digest, fh.mode, fh.mtime, fh.size, fh.inode, i)
740
+ (state.value, fh.digest, fh.mode, fh.mtime, fh.size, UInt64(fh.inode), i)
739
741
  for i, state, fh in new_states_hashes
740
742
  ),
741
743
  )
@@ -1141,7 +1143,7 @@ class Workflow(Cascade):
1141
1143
  file.orphan()
1142
1144
  # Delete outputs of steps that are no longer mandatory.
1143
1145
  cur = self.con.execute(
1144
- "SELECT label, digest, mode, mtime, size, inode FROM file "
1146
+ "SELECT label, digest, mode, mtime, size, inode AS 'inode [UINT64]' FROM file "
1145
1147
  "JOIN node ON node.i = file.node "
1146
1148
  "JOIN dependency ON node.i = consumer "
1147
1149
  "JOIN step ON step.node = supplier "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stepup
3
- Version: 3.2.2
3
+ Version: 3.2.3
4
4
  Summary: StepUp Core provides the basic framework for the StepUp build tool
5
5
  Author-email: Toon Verstraelen <toon.verstraelen@ugent.be>
6
6
  License-Expression: GPL-3.0-or-later
@@ -34,6 +34,7 @@ stepup/core/rpc.py
34
34
  stepup/core/runner.py
35
35
  stepup/core/scheduler.py
36
36
  stepup/core/script.py
37
+ stepup/core/sqlite3.py
37
38
  stepup/core/startup.py
38
39
  stepup/core/step.py
39
40
  stepup/core/stepinfo.py
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes