Skip to content

Commit

Permalink
Update handling of Replica creation (#7)
Browse files Browse the repository at this point in the history
This mirrors how we've done this in the C++ interface used by
Taskwarrior: instead of trying to wrap `dyn Storage`, this just uses a
few storage-specific methods to create replicas.
  • Loading branch information
djmitche authored Dec 25, 2024
1 parent 4aae815 commit cb3ea2d
Show file tree
Hide file tree
Showing 8 changed files with 46 additions and 76 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ It follows the TaskChampion API closely, with minimal adaptation for Python.
The `taskchampion-py` package version matches the Rust crate's version.
When an additional package release is required for the same Rust crate, a fourth version component is used; for example `1.2.0.1` for the second release of `taskchampion-py` containing TaskChampion version `1.2.0`.

## Usage

```py
from taskchampion import Replica

# Set up a replica.
r = Replica.new_on_disk("/some/path", true)

# (more TBD)
```

## Development

This project is built using [maturin](https://github.com/PyO3/maturin).
Expand Down Expand Up @@ -45,5 +56,4 @@ poetry run pytest

## TODO

- There is no good way to describe functions that accept interface (e.g. `Replica::new` accepts any of the storage implementations, but Python bindings lack such mechanisms), currently, `Replica::new` just constructs the SqliteStorage from the params passed into the constructor.
- Possible integration with Github Workflows for deployment to PyPI
4 changes: 0 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ pub mod replica;
use replica::*;
pub mod working_set;
use working_set::*;
pub mod storage;
use storage::*;
pub mod dependency_map;
use dependency_map::*;
pub mod operation;
Expand All @@ -20,8 +18,6 @@ fn taskchampion(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Annotation>()?;
m.add_class::<WorkingSet>()?;
m.add_class::<Tag>()?;
m.add_class::<InMemoryStorage>()?;
m.add_class::<SqliteStorage>()?;
m.add_class::<DependencyMap>()?;
m.add_class::<Operation>()?;

Expand Down
29 changes: 16 additions & 13 deletions src/replica.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ use std::rc::Rc;
use crate::task::TaskData;
use crate::{DependencyMap, Operation, Task, WorkingSet};
use pyo3::prelude::*;
use taskchampion::storage::{InMemoryStorage, SqliteStorage};
use taskchampion::{Operations as TCOperations, Replica as TCReplica, Uuid};
use taskchampion::{Operations as TCOperations, Replica as TCReplica, StorageConfig, Uuid};

#[pyclass]
/// A replica represents an instance of a user's task data, providing an easy interface
Expand All @@ -15,29 +14,33 @@ pub struct Replica(TCReplica);
unsafe impl Send for Replica {}
#[pymethods]
impl Replica {
#[new]
/// Instantiates the Replica
#[staticmethod]
/// Create a Replica with on-disk storage.
///
/// Args:
/// path (str): path to the directory with the database
/// create_if_missing (bool): create the database if it does not exist
/// Raises:
/// RuntimeError: if database does not exist, and create_if_missing is false
pub fn new(path: String, create_if_missing: bool) -> anyhow::Result<Replica> {
let storage = SqliteStorage::new(path, create_if_missing)?;

Ok(Replica(TCReplica::new(Box::new(storage))))
pub fn new_on_disk(path: String, create_if_missing: bool) -> anyhow::Result<Replica> {
Ok(Replica(TCReplica::new(
StorageConfig::OnDisk {
taskdb_dir: path.into(),
create_if_missing,
}
.into_storage()?,
)))
}

#[staticmethod]
pub fn new_inmemory() -> Self {
let storage = InMemoryStorage::new();

Replica(TCReplica::new(Box::new(storage)))
pub fn new_in_memory() -> anyhow::Result<Self> {
Ok(Replica(TCReplica::new(
StorageConfig::InMemory.into_storage()?,
)))
}

/// Create a new task
/// The task must not already exist.
pub fn create_task(&mut self, uuid: String) -> anyhow::Result<(Task, Vec<Operation>)> {
let mut ops = TCOperations::new();
let task = self
Expand Down
38 changes: 0 additions & 38 deletions src/storage.rs

This file was deleted.

4 changes: 3 additions & 1 deletion taskchampion.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ from typing import Optional, Iterator
class Replica:
def __init__(self, path: str, create_if_missing: bool): ...
@staticmethod
def new_inmemory(): ...
def new_on_disk(path: str, create_if_missing: bool): ...
@staticmethod
def new_in_memory(): ...
def create_task(self, uuid: str) -> tuple["Task", list["Operation"]]: ...
def all_task_uuids(self) -> list[str]: ...
def all_tasks(self) -> dict[str, "Task"]: ...
Expand Down
11 changes: 4 additions & 7 deletions tests/test_replica.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@
import pytest
from taskchampion import Replica

# TODO: instantiate the in-memory replica, this will do for now


@pytest.fixture
def empty_replica(tmp_path: Path) -> Replica:
return Replica(str(tmp_path), True)
def empty_replica() -> Replica:
return Replica.new_in_memory()


@pytest.fixture
Expand All @@ -30,14 +27,14 @@ def replica_with_tasks(empty_replica: Replica):


def test_constructor(tmp_path: Path):
r = Replica(str(tmp_path), True)
r = Replica.new_on_disk(str(tmp_path), True)

assert r is not None


def test_constructor_throws_error_with_missing_database(tmp_path: Path):
with pytest.raises(RuntimeError):
Replica(str(tmp_path), False)
Replica.new_on_disk(str(tmp_path), False)


def test_create_task(empty_replica: Replica):
Expand Down
20 changes: 10 additions & 10 deletions tests/test_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@


@pytest.fixture
def new_task(tmp_path):
r = Replica(str(tmp_path), True)
def new_task():
r = Replica.new_in_memory()
result = r.create_task(str(uuid.uuid4()))

assert result is not None
Expand All @@ -16,8 +16,8 @@ def new_task(tmp_path):


@pytest.fixture
def waiting_task(tmp_path):
r = Replica(str(tmp_path), True)
def waiting_task():
r = Replica.new_in_memory()
result = r.create_task(str(uuid.uuid4()))

assert result is not None
Expand All @@ -31,8 +31,8 @@ def waiting_task(tmp_path):


@pytest.fixture
def started_task(tmp_path):
r = Replica(str(tmp_path), True)
def started_task():
r = Replica.new_in_memory()

result = r.create_task(str(uuid.uuid4()))
assert result is not None
Expand All @@ -43,8 +43,8 @@ def started_task(tmp_path):


@pytest.fixture
def blocked_task(tmp_path):
r = Replica(str(tmp_path), True)
def blocked_task():
r = Replica.new_in_memory()
result = r.create_task(str(uuid.uuid4()))

assert result is not None
Expand All @@ -57,8 +57,8 @@ def blocked_task(tmp_path):


@pytest.fixture
def due_task(tmp_path):
r = Replica(str(tmp_path), True)
def due_task():
r = Replica.new_in_memory()
task, _ = r.create_task(str(uuid.uuid4()))

task.set_due(datetime.fromisoformat("2006-05-13T01:27:27+00:00"))
Expand Down
4 changes: 2 additions & 2 deletions tests/test_working_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@


@pytest.fixture
def working_set(tmp_path: Path):
r = Replica(str(tmp_path), True)
def working_set():
r = Replica.new_in_memory()

ops = []
task, op = r.create_task(str(uuid.uuid4()))
Expand Down

0 comments on commit cb3ea2d

Please sign in to comment.