Skip to content

Commit 82441cc

Browse files
authored
Replace pony with sqlalchemy>=1.4.36. (#387)
1 parent 1c9cd5d commit 82441cc

20 files changed

Lines changed: 203 additions & 173 deletions

docs/rtd_environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ dependencies:
2525
- click-default-group
2626
- networkx >=2.4
2727
- pluggy
28-
- pony >=0.7.15
2928
- pybaum >=0.1.1
3029
- pexpect
3130
- rich
31+
- sqlalchemy >=1.4.36
3232
- tomli >=1.0.0
3333

3434
- pip:

docs/source/changes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ releases are available on [PyPI](https://pypi.org/project/pytask) and
88
## 0.4.0 - 2023-xx-xx
99

1010
- {pull}`323` remove Python 3.7 support and use a new Github action to provide mamba.
11+
- {pull}`387` replaces pony with sqlalchemy.
1112

1213
## 0.3.2 - 2023-06-07
1314

docs/source/reference_guides/configuration.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ are welcome to also support macOS.
4242
4343
````
4444

45+
````{confval} database_url
46+
47+
pytask uses a database to keep track of tasks, products, and dependencies over runs. By
48+
default, it will create an SQLITE database in the project's root directory called
49+
`.pytask.sqlite3`. If you want to use a different name or a different dialect
50+
[supported by sqlalchemy](https://docs.sqlalchemy.org/en/latest/core/engines.html#backend-specific-urls),
51+
use either {option}`pytask build --database-url` or `database_url` in the config.
52+
53+
```toml
54+
database_url = "sqlite:///.pytask.sqlite3"
55+
```
56+
57+
Relative paths for SQLITE databases are interpreted as either relative to the
58+
configuration file or the root directory.
59+
60+
````
61+
4562
````{confval} editor_url_scheme
4663
4764
Depending on your terminal, pytask is able to turn task ids into clickable links to the

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ dependencies:
1616
- click-default-group
1717
- networkx >=2.4
1818
- pluggy
19-
- pony >=0.7.15
2019
- pybaum >=0.1.1
2120
- rich
21+
- sqlalchemy >=1.4.36
2222
- tomli >=1.0.0
2323

2424
# Misc

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ install_requires =
3636
networkx>=2.4
3737
packaging
3838
pluggy
39-
pony>=0.7.15
4039
pybaum>=0.1.1
4140
rich
41+
sqlalchemy>=1.4.36
4242
tomli>=1.0.0
4343
python_requires = >=3.8
4444
include_package_data = True

src/_pytask/clean.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ def _collect_all_paths_known_to_pytask(session: Session) -> set[Path]:
195195
if session.config["config"]:
196196
known_paths.add(session.config["config"])
197197
known_paths.add(session.config["root"])
198-
known_paths.add(session.config["database_filename"])
198+
199+
database_url = session.config["database_url"]
200+
if database_url.drivername == "sqlite" and database_url.database:
201+
known_paths.add(Path(database_url.database))
199202

200203
# Add files tracked by git.
201204
if is_git_installed():

src/_pytask/click.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,11 @@ def _format_help_text( # noqa: C901, PLR0912, PLR0915
242242

243243
if show_default_is_str or (show_default and (default_value is not None)):
244244
if show_default_is_str:
245-
default_string = f"({param.show_default})" # type: ignore[attr-defined]
245+
default_string = param.show_default # type: ignore[attr-defined]
246246
elif isinstance(default_value, (list, tuple)):
247247
default_string = ", ".join(str(d) for d in default_value)
248248
elif inspect.isfunction(default_value):
249-
default_string = _("(dynamic)")
249+
default_string = _("dynamic")
250250
elif param.is_bool_flag and param.secondary_opts: # type: ignore[attr-defined]
251251
# For boolean flags that have distinct True/False opts,
252252
# use the opt without prefix instead of the value.

src/_pytask/dag.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from _pytask.dag_utils import node_and_neighbors
1717
from _pytask.dag_utils import task_and_descending_tasks
1818
from _pytask.dag_utils import TopologicalSorter
19+
from _pytask.database_utils import DatabaseSession
1920
from _pytask.database_utils import State
2021
from _pytask.exceptions import ResolvingDependenciesError
2122
from _pytask.mark import Mark
@@ -30,7 +31,6 @@
3031
from _pytask.shared import reduce_names_of_multiple_nodes
3132
from _pytask.shared import reduce_node_name
3233
from _pytask.traceback import render_exc_info
33-
from pony import orm
3434
from pybaum import tree_map
3535
from rich.text import Text
3636
from rich.tree import Tree
@@ -126,7 +126,6 @@ def _have_task_or_neighbors_changed(
126126
)
127127

128128

129-
@orm.db_session
130129
@hookimpl(trylast=True)
131130
def pytask_dag_has_node_changed(node: MetaNode, task_name: str) -> bool:
132131
"""Indicate whether a single dependency or product has changed."""
@@ -136,11 +135,11 @@ def pytask_dag_has_node_changed(node: MetaNode, task_name: str) -> bool:
136135
if file_state is None:
137136
return True
138137

138+
with DatabaseSession() as session:
139+
db_state = session.get(State, (task_name, node.name))
140+
139141
# If the node is not in the database.
140-
try:
141-
name = node.name
142-
db_state = State[task_name, name] # type: ignore[type-arg, valid-type]
143-
except orm.ObjectNotFound:
142+
if db_state is None:
144143
return True
145144

146145
# If the modification times match, the node has not been changed.

src/_pytask/database.py

Lines changed: 24 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,43 @@
1-
"""Implement the database managed with pony."""
1+
"""Contains hooks related to the database."""
22
from __future__ import annotations
33

4-
import enum
54
from pathlib import Path
65
from typing import Any
76

8-
import click
9-
from _pytask.click import EnumChoice
107
from _pytask.config import hookimpl
118
from _pytask.database_utils import create_database
12-
from click import Context
13-
14-
15-
class _DatabaseProviders(enum.Enum):
16-
SQLITE = "sqlite"
17-
POSTGRES = "postgres"
18-
MYSQL = "mysql"
19-
ORACLE = "oracle"
20-
COCKROACH = "cockroach"
21-
22-
23-
def _database_filename_callback(
24-
ctx: Context, name: str, value: str | None # noqa: ARG001
25-
) -> str | None:
26-
if value is None:
27-
return ctx.params["root"].joinpath(".pytask.sqlite3")
28-
return value
29-
30-
31-
@hookimpl
32-
def pytask_extend_command_line_interface(cli: click.Group) -> None:
33-
"""Extend command line interface."""
34-
additional_parameters = [
35-
click.Option(
36-
["--database-provider"],
37-
type=EnumChoice(_DatabaseProviders),
38-
help=(
39-
"Database provider. All providers except sqlite are considered "
40-
"experimental."
41-
),
42-
default=_DatabaseProviders.SQLITE,
43-
),
44-
click.Option(
45-
["--database-filename"],
46-
type=click.Path(file_okay=True, dir_okay=False, path_type=Path),
47-
help=("Path to database relative to root."),
48-
default=Path(".pytask.sqlite3"),
49-
callback=_database_filename_callback,
50-
),
51-
click.Option(
52-
["--database-create-db"],
53-
type=bool,
54-
help="Create database if it does not exist.",
55-
default=True,
56-
),
57-
click.Option(
58-
["--database-create-tables"],
59-
type=bool,
60-
help="Create tables if they do not exist.",
61-
default=True,
62-
),
63-
]
64-
cli.commands["build"].params.extend(additional_parameters)
9+
from sqlalchemy.engine import make_url
6510

6611

6712
@hookimpl
6813
def pytask_parse_config(config: dict[str, Any]) -> None:
6914
"""Parse the configuration."""
70-
if not config["database_filename"].is_absolute():
71-
config["database_filename"] = config["root"].joinpath(
72-
config["database_filename"]
15+
# Set default.
16+
if not config["database_url"]:
17+
config["database_url"] = make_url(
18+
f"sqlite:///{config['root'].as_posix()}/.pytask.sqlite3"
7319
)
7420

75-
config["database"] = {
76-
"provider": config["database_provider"].value,
77-
"filename": config["database_filename"].as_posix(),
78-
"create_db": config["database_create_db"],
79-
"create_tables": config["database_create_tables"],
80-
}
21+
if (
22+
config["database_url"].drivername == "sqlite"
23+
and config["database_url"].database
24+
) and not Path(config["database_url"].database).is_absolute():
25+
if config["config"]:
26+
full_path = (
27+
config["config"]
28+
.parent.joinpath(config["database_url"].database)
29+
.resolve()
30+
)
31+
else:
32+
full_path = (
33+
config["root"].joinpath(config["database_url"].database).resolve()
34+
)
35+
config["database_url"] = config["database_url"]._replace(
36+
database=full_path.as_posix()
37+
)
8138

8239

8340
@hookimpl
8441
def pytask_post_parse(config: dict[str, Any]) -> None:
8542
"""Post-parse the configuration."""
86-
create_database(**config["database"])
43+
create_database(config["database_url"])

src/_pytask/database_utils.py

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,64 @@
66
from _pytask.dag_utils import node_and_neighbors
77
from _pytask.nodes import Task
88
from _pytask.session import Session
9-
from pony import orm
9+
from sqlalchemy import Column
10+
from sqlalchemy import create_engine
11+
from sqlalchemy import String
12+
from sqlalchemy.orm import declarative_base
13+
from sqlalchemy.orm import sessionmaker
1014

1115

12-
__all__ = ["create_database", "db", "update_states_in_database"]
16+
__all__ = ["create_database", "update_states_in_database", "DatabaseSession"]
1317

1418

15-
db = orm.Database()
19+
DatabaseSession = sessionmaker()
1620

1721

18-
class State(db.Entity): # type: ignore[name-defined]
22+
Base = declarative_base()
23+
24+
25+
class State(Base): # type: ignore[valid-type, misc]
1926
"""Represent the state of a node in relation to a task."""
2027

21-
task = orm.Required(str)
22-
node = orm.Required(str)
23-
modification_time = orm.Required(str)
24-
file_hash = orm.Optional(str)
28+
__tablename__ = "state"
2529

26-
orm.PrimaryKey(task, node)
30+
task = Column(String, primary_key=True)
31+
node = Column(String, primary_key=True)
32+
modification_time = Column(String)
33+
file_hash = Column(String)
2734

2835

29-
def create_database(
30-
provider: str, filename: str, *, create_db: bool, create_tables: bool
31-
) -> None:
36+
def create_database(url: str) -> None:
3237
"""Create the database."""
3338
try:
34-
db.bind(provider=provider, filename=filename, create_db=create_db)
35-
db.generate_mapping(create_tables=create_tables)
36-
except orm.BindingError:
37-
pass
39+
engine = create_engine(url)
40+
Base.metadata.create_all(bind=engine)
41+
DatabaseSession.configure(bind=engine)
42+
except Exception:
43+
raise
3844

3945

40-
@orm.db_session
4146
def _create_or_update_state(
4247
first_key: str, second_key: str, modification_time: str, file_hash: str
4348
) -> None:
4449
"""Create or update a state."""
45-
try:
46-
state_in_db = State[first_key, second_key] # type: ignore[type-arg, valid-type]
47-
except orm.ObjectNotFound:
48-
State(
49-
task=first_key,
50-
node=second_key,
51-
modification_time=modification_time,
52-
file_hash=file_hash,
53-
)
54-
else:
55-
state_in_db.modification_time = modification_time
56-
state_in_db.file_hash = file_hash
50+
with DatabaseSession() as session:
51+
state_in_db = session.get(State, (first_key, second_key))
52+
53+
if not state_in_db:
54+
session.add(
55+
State(
56+
task=first_key,
57+
node=second_key,
58+
modification_time=modification_time,
59+
file_hash=file_hash,
60+
)
61+
)
62+
else:
63+
state_in_db.modification_time = modification_time
64+
state_in_db.file_hash = file_hash
65+
66+
session.commit()
5767

5868

5969
def update_states_in_database(session: Session, task_name: str) -> None:

0 commit comments

Comments
 (0)