ReMe/reme4/steps/file_io/daily_create.py
jinliyl 041f957a7f
Some checks are pending
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Tests ReMe / Unit Tests - py3.10 (push) Waiting to run
Tests ReMe / Unit Tests - py3.13 (push) Waiting to run
refactor(components) components and file I/O, fix method calls and validation (#268)
* refactor(components): extract shared component state into mixin

- Introduce ComponentMixin class with shared state for components and steps
- Move identity, config, and vault path functionality to ComponentMixin
- Update BaseComponent to inherit from ComponentMixin
- Update BaseStep to inherit from ComponentMixin
- Consolidate vault path helper methods in ComponentMixin
- Remove duplicate vault path implementations from BaseComponent and BaseStep
- Add ComponentMixin to components module exports

* refactor(file_io): implement path locks cache eviction mechanism

- Add _PATH_LOCKS_MAX constant set to 1024 for cache size limit
- Implement cache eviction logic when locks exceed maximum capacity
- Remove half of unlocked entries when cache limit is reached
- Use list comprehension to identify unlocked locks for removal
- Maintain existing path normalization and locking behavior

fix(edit): correct method call from public to private fail method

- Change self.fail to self._fail for internal error handling
- Maintain consistent private method usage within class

fix(mcp_client): change pop to get for optional command and args

- Replace kwargs.pop with kwargs.get to avoid removing keys
- Preserve original kwargs dictionary contents
- Maintain default empty string and list values

feat(reme): add client backend validation with error raising

- Check if client_cls is None before instantiation
- Raise ValueError with descriptive message for unknown backends
- Provide clear error feedback for invalid backend configurations

* fix(components): move directory creation to start method

- Moved component_metadata_path.mkdir call from __init__ to _start in base_keyword_index
- Moved component_metadata_path.mkdir call from __init__ to _start in local_file_graph
- Moved component_metadata_path.mkdir call from __init__ to _start in local_file_store
- Ensures directory creation happens after component initialization
- Prevents potential issues with path creation during object construction

* fix(steps): replace assertions with runtime errors for app_context validation

- Replace assert statements with explicit RuntimeError exceptions when app_context is None
- Add descriptive error messages for better debugging when resolving components
- Replace assert in resolve_component method with proper exception handling
- Replace assert in get_file_parser method with proper exception handling
- Maintain same functionality while improving error reporting clarity

* refactor(file_io): split file IO utilities into modular components

- Move daily note helpers to separate _daily_index module
- Extract path validation and resolution to new _path module
- Remove unused code and imports from _file_io module
- Update import statements across affected modules
- Introduce WikilinkHandler utility for link parsing
- Replace regex-based link extraction with WikilinkHandler
- Add integration JSONL files to gitignore
- Consolidate file locking mechanism in _file_io module

* style(formatter): fix spacing issues in file IO and chunked file parser

- Fixed whitespace around colon in slice notation in file_io.py
- Corrected spacing around colon in slice notation in chunked_file_parser.py
- Applied consistent formatting for array slicing operations
- Improved code readability by standardizing space placement in ranges

* refactor(steps): replace property-based component resolution with Ref descriptor

- Introduce Ref descriptor class for lazy component dependency resolution
- Replace _resolve method and individual properties with Ref descriptors
- Add as_llm, as_llm_formatter, as_token_counter, file_store, and embedding Ref attributes
- Remove legacy property methods and resolve logic from BaseStep
- Add cache clearing mechanism for Ref values during step calls
- Update UpdateCatalogStep to use Ref instead of property-based resolution
2026-06-01 11:35:19 +08:00

107 lines
4.5 KiB
Python

"""``daily_create`` — provision a session note under a daily folder.
When ``session_id`` is provided: ``daily/<date>/<session_id>.md``
When ``session_id`` is empty: ``daily/<date>.md`` (the day-level file)
Validates the session_id (when non-empty), mkdirs the day folder,
writes an empty-body note with frontmatter ``{name, description}``
if (and only if) the file does not already exist, refreshes the day
index (only when session_id is non-empty), and returns the
vault-relative path.
Idempotent: when the note already exists this is a no-op write.
The caller fills the body via ``file_write`` / ``file_edit`` /
``file_append`` (or a native editor); ``daily_create`` deliberately
does not accept a body.
Inputs:
session_id (optional) — the note's session identifier (also the file stem);
empty string → day-level file
date (optional, ``YYYY-MM-DD``; empty = today)
Outputs:
answer = one-line human-readable status
metadata = {date, session_id, path, created, index?}
"""
from datetime import date as _date
from pathlib import Path
import frontmatter
from ._daily_index import refresh_day_index, validate_session_id
from ._file_io import write_file_safe
from ..base_step import BaseStep
from ...components import R
@R.register("daily_create_step")
class DailyCreateStep(BaseStep):
"""Provision a daily note (idempotent); refresh day index when applicable."""
def _fail(self, message: str, **meta) -> None:
"""Mark response failed; copy ``meta`` into ``response.metadata``."""
assert self.context is not None
self.context.response.success = False
self.context.response.answer = f"Error: {message}"
if meta:
self.context.response.metadata.update(meta)
def _collect_params(self) -> tuple[str, str, str]:
"""Read ``session_id`` + ``date`` from context; default ``date`` today, ``daily_dir`` from app config."""
assert self.context is not None
session_id = self.context.get("session_id", "")
day = self.context.get("date", "") or _date.today().strftime("%Y-%m-%d")
daily_dir = self.app_context.app_config.daily_dir if self.app_context is not None else "daily"
return session_id, day, daily_dir
@staticmethod
def _empty_note_text(name: str) -> str:
"""Serialize an empty-body markdown note with frontmatter ``{name, description}``; trailing newline."""
text = frontmatter.dumps(frontmatter.Post("", name=name, description=""))
return text if text.endswith("\n") else text + "\n"
async def _create_if_missing(self, path_abs: Path, name: str) -> bool:
"""Write the empty note only when the file is absent. Returns ``True`` iff a new file was created."""
if path_abs.is_file():
return False
await write_file_safe(path_abs, self._empty_note_text(name), encoding="utf-8")
return True
def _set_success(self, payload: dict, created: bool) -> None:
"""Stamp the response with success + human-readable answer + metadata payload."""
assert self.context is not None
self.context.response.success = True
self.context.response.answer = f"{'Created' if created else 'Reused existing'} daily note {payload['path']}"
self.context.response.metadata.update(payload)
async def execute(self):
"""Provision the note file, optionally refresh the day index, stamp the response."""
assert self.context is not None
session_id, day, daily_dir = self._collect_params()
if session_id:
err = validate_session_id(session_id)
if err:
self._fail(err)
return None
path_rel = f"{daily_dir}/{day}/{session_id}.md"
name = session_id
else:
path_rel = f"{daily_dir}/{day}.md"
name = day
path_abs = (self.vault_path / path_rel).resolve()
try:
created = await self._create_if_missing(path_abs, name)
except Exception as e: # pylint: disable=broad-except
self._fail(f"create failed: {e}", date=day, session_id=session_id, path=path_rel)
return None
payload: dict = {"date": day, "session_id": session_id, "path": path_rel, "created": created}
if session_id:
payload["index"] = await refresh_day_index(self.file_store, day, daily_dir)
self._set_success(payload, created)
self.logger.info(f"[{self.name}] {'created' if created else 'reused'} path={path_rel}")
return self.context.response