mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c6ed53a4a | |||
| 462b15dc48 | |||
| aad2c750bc | |||
| ffbd091ad3 | |||
| e2882dbf84 | |||
| 2ef48bb34c | |||
| 5e501b5440 | |||
| 451216bba6 | |||
| 55c5e4ca10 | |||
| 3d2c0c8c32 | |||
| 9e509fefaa | |||
| 135bec181d | |||
| c429d6487f | |||
| 51a741e759 | |||
| 4eec820a32 | |||
| b8d0c7160f | |||
| 8092e6d38d | |||
| af6b7dc40a | |||
| c27a4bcdcd | |||
| 8b9377b593 | |||
| 3484e26631 |
@@ -0,0 +1,43 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: [ "3.12" ]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
pip install uv
|
||||
|
||||
- name: Create virtual env
|
||||
run: |
|
||||
uv venv
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
uv pip install -e .[dev]
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
uv pip install pytest pytest-cov
|
||||
uv run make test
|
||||
@@ -1,6 +1,39 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v0.1.0 (2025-02-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Create virtual env in test workflow
|
||||
([`8092e6d`](https://github.com/basicmachines-co/basic-memory/commit/8092e6d38d536bfb6f93c3d21ea9baf1814f9b0a))
|
||||
|
||||
- Fix permalink uniqueness violations on create/update/sync
|
||||
([`135bec1`](https://github.com/basicmachines-co/basic-memory/commit/135bec181d9b3d53725c8af3a0959ebc1aa6afda))
|
||||
|
||||
- Fix recent activity bug
|
||||
([`3d2c0c8`](https://github.com/basicmachines-co/basic-memory/commit/3d2c0c8c32fcfdaf70a1f96a59d8f168f38a1aa9))
|
||||
|
||||
- Install fastapi deps after removing basic-foundation
|
||||
([`51a741e`](https://github.com/basicmachines-co/basic-memory/commit/51a741e7593a1ea0e5eb24e14c70ff61670f9663))
|
||||
|
||||
- Remove basic-foundation from deps
|
||||
([`b8d0c71`](https://github.com/basicmachines-co/basic-memory/commit/b8d0c7160f29c97cdafe398a7e6a5240473e0c89))
|
||||
|
||||
- Run tests via uv
|
||||
([`4eec820`](https://github.com/basicmachines-co/basic-memory/commit/4eec820a32bc059a405e2f4dac4c73b245ca4722))
|
||||
|
||||
### Chores
|
||||
|
||||
- Rename import tool
|
||||
([`af6b7dc`](https://github.com/basicmachines-co/basic-memory/commit/af6b7dc40a55eaa2aa78d6ea831e613851081d52))
|
||||
|
||||
### Features
|
||||
|
||||
- Add memory-json importer, tweak observation content
|
||||
([`3484e26`](https://github.com/basicmachines-co/basic-memory/commit/3484e26631187f165ee6eb85517e94717b7cf2cf))
|
||||
|
||||
|
||||
## v0.0.1 (2025-02-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,34 +1,258 @@
|
||||
# Basic Memory
|
||||
|
||||
> Local-first knowledge management combining Zettelkasten with knowledge graphs.
|
||||
Basic Memory lets you build persistent knowledge through natural conversations with Large Language Models (LLMs) like
|
||||
Claude, while keeping everything in simple markdown files on your computer. It uses the Model Context Protocol (MCP) to
|
||||
enable any compatible LLM to read and write to your local knowledge base.
|
||||
|
||||
Basic Memory helps you capture and explore information the way your brain naturally works - across multiple dimensions and perspectives. Built on open standards and powered by proven technology, it's ready for the future of human-AI collaboration.
|
||||
## What is Basic Memory?
|
||||
|
||||
## Features
|
||||
Most people use LLMs like calculators - paste in some text, expect to get an answer back, repeat. Each conversation
|
||||
starts fresh,
|
||||
and any knowledge or context is lost. Some try to work around this by:
|
||||
|
||||
- Local-first design - your files stay yours
|
||||
- Knowledge graph with natural relations
|
||||
- Markdown files with automatic parsing
|
||||
- Rich context building and search
|
||||
- Native AI integration via MCP protocol
|
||||
- Saving chat histories (but they're hard to reference)
|
||||
- Copying and pasting previous conversations (messy and repetitive)
|
||||
- Using RAG systems to query documents (complex and often cloud-based)
|
||||
|
||||
## Quick Start
|
||||
Basic Memory takes a different approach by letting both humans and LLMs read and write knowledge naturally using
|
||||
standard markdown files. This means:
|
||||
|
||||
```bash
|
||||
pip install basic-memory
|
||||
- Your knowledge stays in files you control
|
||||
- Both you and the LLM can read and write notes
|
||||
- Context persists across conversations
|
||||
- Context stays local and user controlled
|
||||
|
||||
## How It Works in Practice
|
||||
|
||||
Let's say you're working on a new project and want to capture design decisions. Here's how it works:
|
||||
|
||||
1. Start by chatting normally:
|
||||
|
||||
```markdown
|
||||
We need to design a new auth system, some key features:
|
||||
|
||||
- local first, don't delegate users to third party system
|
||||
- support multiple platforms via jwt
|
||||
- want to keep it simple but secure
|
||||
```
|
||||
|
||||
Initialize a new knowledge base:
|
||||
```bash
|
||||
basic-memory init my-knowledge
|
||||
cd my-knowledge
|
||||
basic-memory status
|
||||
... continue conversation.
|
||||
|
||||
2. Ask Claude to help structure this knowledge:
|
||||
|
||||
```
|
||||
"Lets write a note about the auth system design."
|
||||
```
|
||||
|
||||
## Contributing
|
||||
Claude creates a new markdown file on your system (which you can see instantly in Obsidian or your editor):
|
||||
|
||||
Basic Memory is open source (AGPL-3.0) and welcomes contributions. See our [Contributing Guide](CONTRIBUTING.md) for details.
|
||||
```markdown
|
||||
---
|
||||
title: Auth System Design
|
||||
permalink: auth-system-design
|
||||
tags
|
||||
- design
|
||||
- auth
|
||||
---
|
||||
|
||||
# Auth System Design
|
||||
|
||||
## Observations
|
||||
|
||||
- [requirement] Local-first authentication without third party delegation
|
||||
- [tech] JWT-based auth for cross-platform support
|
||||
- [principle] Balance simplicity with security
|
||||
|
||||
## Relations
|
||||
|
||||
- implements [[Security Requirements]]
|
||||
- relates_to [[Platform Support]]
|
||||
- referenced_by [[JWT Implementation]]
|
||||
```
|
||||
|
||||
The note embeds semantic content (Observations) and links to other topics (Relations) via simple markdown formatting.
|
||||
|
||||
3. You can edit this file directly in your editor in real time:
|
||||
|
||||
```markdown
|
||||
# Auth System Design
|
||||
|
||||
## Observations
|
||||
|
||||
- [requirement] Local-first authentication without third party delegation
|
||||
- [tech] JWT-based auth for cross-platform support
|
||||
- [principle] Balance simplicity with security
|
||||
- [decision] Will use bcrypt for password hashing # Added by you
|
||||
|
||||
## Relations
|
||||
|
||||
- implements [[Security Requirements]]
|
||||
- relates_to [[Platform Support]]
|
||||
- referenced_by [[JWT Implementation]]
|
||||
- blocks [[User Service]] # Added by you
|
||||
```
|
||||
|
||||
4. In a new chat with Claude, you can reference this knowledge:
|
||||
|
||||
```
|
||||
"Claude, look at memory://auth-system-design for context about our auth system"
|
||||
```
|
||||
|
||||
Claude can now build rich context from the knowledge graph. For example:
|
||||
|
||||
```
|
||||
Following relation 'implements [[Security Requirements]]':
|
||||
- Found authentication best practices
|
||||
- OWASP guidelines for JWT
|
||||
- Rate limiting requirements
|
||||
|
||||
Following relation 'relates_to [[Platform Support]]':
|
||||
- Mobile auth requirements
|
||||
- Browser security considerations
|
||||
- JWT storage strategies
|
||||
```
|
||||
|
||||
Each related document can lead to more context, building a rich semantic understanding of your knowledge base. All of
|
||||
this context comes from standard markdown files that both humans and LLMs can read and write.
|
||||
|
||||
Everything stays in local markdown files that you can:
|
||||
|
||||
- Edit in any text editor
|
||||
- Version via git
|
||||
- Back up normally
|
||||
- Share when you want to
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
Under the hood, Basic Memory:
|
||||
|
||||
1. Stores everything in markdown files
|
||||
2. Uses a SQLite database just for searching and indexing
|
||||
3. Extracts semantic meaning from simple markdown patterns
|
||||
4. Maintains a local knowledge graph from file content
|
||||
|
||||
The file format is just markdown with some simple markup:
|
||||
|
||||
Frontmatter
|
||||
|
||||
- title
|
||||
- type
|
||||
- permalink
|
||||
- optional metadata
|
||||
|
||||
Observations
|
||||
|
||||
- facts about a topic
|
||||
|
||||
```markdown
|
||||
- [category] content #tag (optional context)
|
||||
```
|
||||
|
||||
Relations
|
||||
|
||||
- links to other topics
|
||||
|
||||
```markdown
|
||||
- relation_type [[WikiLink]] (optional context)
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: Note tile
|
||||
type: note
|
||||
permalink: unique/stable/id # Added automatically
|
||||
tags
|
||||
- tag1
|
||||
- tag2
|
||||
---
|
||||
|
||||
# Note Title
|
||||
|
||||
Regular markdown content...
|
||||
|
||||
## Observations
|
||||
|
||||
- [category] Structured knowledge #tag (optional context)
|
||||
- [idea] Another observation
|
||||
|
||||
## Relations
|
||||
|
||||
- links_to [[Other Note]]
|
||||
- implements [[Some Spec]]
|
||||
```
|
||||
|
||||
Basic Memory will parse the markdown and derive the semantic relationships in the content. When you run
|
||||
`basic-memory sync`:
|
||||
|
||||
1. New and changed files are detected
|
||||
2. Markdown patterns become semantic knowledge:
|
||||
|
||||
- `[tech]` becomes a categorized observation
|
||||
- `[[WikiLink]]` creates a relation in the knowledge graph
|
||||
- Tags and metadata are indexed for search
|
||||
|
||||
3. A SQLite database maintains these relationships for fast querying
|
||||
4. Claude and other MCP-compatible LLMs can access this knowledge via memory:// URLs
|
||||
|
||||
This creates a two-way flow where:
|
||||
|
||||
- Humans write and edit markdown files
|
||||
- LLMs read and write through the MCP protocol
|
||||
- Sync keeps everything consistent
|
||||
- All knowledge stays in local files.
|
||||
|
||||
## Using with Claude
|
||||
|
||||
Basic Memory works with the Claude desktop app (https://claude.ai/):
|
||||
|
||||
1. Install Basic Memory locally:
|
||||
|
||||
```bash
|
||||
{
|
||||
"mcpServers": {
|
||||
"basic-memory": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"basic-memory"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Add to Claude Desktop:
|
||||
|
||||
```
|
||||
Basic Memory is available with these tools:
|
||||
- write_note() for creating/updating notes
|
||||
- read_note() for loading notes
|
||||
- build_context() to load notes via memory:// URLs
|
||||
- recent_activity() to find recently updated information
|
||||
- search() to search infomation in the knowledge base
|
||||
```
|
||||
|
||||
3. Install via uv
|
||||
|
||||
```bash
|
||||
uv add basic-memory
|
||||
|
||||
# sync local knowledge updates
|
||||
basic-memory sync
|
||||
|
||||
# run realtime sync process
|
||||
basic-memory sync --watch
|
||||
```
|
||||
|
||||
## Design Philosophy
|
||||
|
||||
Basic Memory is built on some key ideas:
|
||||
|
||||
- Your knowledge should stay in files you control
|
||||
- Both humans and AI should use natural formats
|
||||
- Simple text patterns can capture rich meaning
|
||||
- Local-first doesn't mean feature-poor
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0 - See [LICENSE](LICENSE) for details.
|
||||
AGPL-3.0
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "basic-memory"
|
||||
version = "0.0.1"
|
||||
version = "0.1.0"
|
||||
description = "Local-first knowledge management combining Zettelkasten with knowledge graphs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12.1"
|
||||
@@ -20,13 +20,14 @@ dependencies = [
|
||||
"pydantic-settings>=2.6.1",
|
||||
"loguru>=0.7.3",
|
||||
"pyright>=1.1.390",
|
||||
"basic-foundation",
|
||||
"markdown-it-py>=3.0.0",
|
||||
"python-frontmatter>=1.1.0",
|
||||
"rich>=13.9.4",
|
||||
"unidecode>=1.3.8",
|
||||
"dateparser>=1.2.0",
|
||||
"watchfiles>=1.0.4",
|
||||
"fastapi[standard]>=0.115.8",
|
||||
"alembic>=1.14.1",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -7,18 +7,40 @@ from fastapi.exception_handlers import http_exception_handler
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory import db
|
||||
from .routers import knowledge, search, memory, resource
|
||||
from basic_memory.api.routers import knowledge, search, memory, resource
|
||||
from basic_memory.config import config
|
||||
from basic_memory.services import DatabaseService
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Lifecycle manager for the FastAPI app."""
|
||||
logger.info("Starting Basic Memory API")
|
||||
|
||||
# check the db state
|
||||
await check_db(app)
|
||||
yield
|
||||
logger.info("Shutting down Basic Memory API")
|
||||
await db.shutdown_db()
|
||||
|
||||
|
||||
async def check_db(app: FastAPI):
|
||||
logger.info("Checking database state")
|
||||
|
||||
# Initialize DB management service
|
||||
db_service = DatabaseService(
|
||||
config=config,
|
||||
)
|
||||
|
||||
# Check and initialize DB if needed
|
||||
if not await db_service.check_db():
|
||||
raise RuntimeError("Database initialization failed")
|
||||
|
||||
# Clean up old backups on shutdown
|
||||
await db_service.cleanup_backups()
|
||||
|
||||
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title="Basic Memory API",
|
||||
|
||||
@@ -47,7 +47,7 @@ async def to_graph_context(context, entity_repository: EntityRepository):
|
||||
|
||||
return RelationSummary(
|
||||
permalink=item.permalink,
|
||||
type=item.type,
|
||||
relation_type=item.type,
|
||||
from_id=from_entity.permalink,
|
||||
to_id=to_entity.permalink if to_entity else None,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
"""CLI commands package."""
|
||||
"""Command module exports."""
|
||||
|
||||
from . import init, status
|
||||
from . import init, status, sync, import_memory_json
|
||||
|
||||
|
||||
__all__ = [
|
||||
"init",
|
||||
"status",
|
||||
]
|
||||
__all__ = ["init", "status", "sync", "import_memory_json.py"]
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Import command for basic-memory CLI to import from JSON memory format."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter, Observation, Relation
|
||||
|
||||
|
||||
console = Console()
|
||||
|
||||
async def process_memory_json(json_path: Path, base_path: Path,markdown_processor: MarkdownProcessor):
|
||||
"""Import entities from memory.json using markdown processor."""
|
||||
|
||||
# First pass - collect all relations by source entity
|
||||
entity_relations: Dict[str, List[Relation]] = {}
|
||||
entities: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
console=console,
|
||||
) as progress:
|
||||
read_task = progress.add_task("Reading memory.json...", total=None)
|
||||
|
||||
# First pass - collect entities and relations
|
||||
with open(json_path) as f:
|
||||
lines = f.readlines()
|
||||
progress.update(read_task, total=len(lines))
|
||||
|
||||
for line in lines:
|
||||
data = json.loads(line)
|
||||
if data["type"] == "entity":
|
||||
entities[data["name"]] = data
|
||||
elif data["type"] == "relation":
|
||||
# Store relation with its source entity
|
||||
source = data.get("from") or data.get("from_id")
|
||||
if source not in entity_relations:
|
||||
entity_relations[source] = []
|
||||
entity_relations[source].append(
|
||||
Relation(
|
||||
type=data.get("relationType") or data.get("relation_type"),
|
||||
target=data.get("to") or data.get("to_id")
|
||||
)
|
||||
)
|
||||
progress.update(read_task, advance=1)
|
||||
|
||||
# Second pass - create and write entities
|
||||
write_task = progress.add_task("Creating entities...", total=len(entities))
|
||||
|
||||
entities_created = 0
|
||||
for name, entity_data in entities.items():
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": entity_data["entityType"],
|
||||
"title": name,
|
||||
"permalink": f"{entity_data['entityType']}/{name}"
|
||||
}
|
||||
),
|
||||
content=f"# {name}\n",
|
||||
observations=[
|
||||
Observation(content=obs)
|
||||
for obs in entity_data["observations"]
|
||||
],
|
||||
relations=entity_relations.get(name, []) # Add any relations where this entity is the source
|
||||
)
|
||||
|
||||
# Let markdown processor handle writing
|
||||
file_path = base_path / f"{entity_data['entityType']}/{name}.md"
|
||||
await markdown_processor.write_file(file_path, entity)
|
||||
entities_created += 1
|
||||
progress.update(write_task, advance=1)
|
||||
|
||||
return {
|
||||
"entities": entities_created,
|
||||
"relations": sum(len(rels) for rels in entity_relations.values())
|
||||
}
|
||||
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
|
||||
@app.command()
|
||||
def import_json(
|
||||
json_path: Path = typer.Argument(..., help="Path to memory.json file to import"),
|
||||
):
|
||||
"""Import entities and relations from a memory.json file.
|
||||
|
||||
This command will:
|
||||
1. Read entities and relations from the JSON file
|
||||
2. Create markdown files for each entity
|
||||
3. Include outgoing relations in each entity's markdown
|
||||
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
|
||||
if not json_path.exists():
|
||||
typer.echo(f"Error: File not found: {json_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Process the file
|
||||
base_path = config.home
|
||||
console.print(f"\nImporting from {json_path}...writing to {base_path}")
|
||||
results = asyncio.run(process_memory_json(json_path, base_path, markdown_processor))
|
||||
|
||||
# Show results
|
||||
console.print(Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Created {results['entities']} entities\n"
|
||||
f"Added {results['relations']} relations",
|
||||
expand=False
|
||||
))
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
@@ -14,7 +14,7 @@ __all__ = ["init", "status", "sync"]
|
||||
from basic_memory.config import config
|
||||
|
||||
|
||||
def setup_logging(home_dir: str = config.home, log_file: str = "./basic-memory/basic-memory-tools.log"):
|
||||
def setup_logging(home_dir: str = config.home, log_file: str = ".basic-memory/basic-memory-tools.log"):
|
||||
"""Configure logging for the application."""
|
||||
|
||||
# Remove default handler and any existing handlers
|
||||
|
||||
@@ -37,7 +37,11 @@ class ProjectConfig(BaseSettings):
|
||||
@property
|
||||
def database_path(self) -> Path:
|
||||
"""Get SQLite database path."""
|
||||
return self.home / DATA_DIR_NAME / DATABASE_NAME
|
||||
database_path = self.home / DATA_DIR_NAME / DATABASE_NAME
|
||||
if not database_path.exists():
|
||||
database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
database_path.touch()
|
||||
return database_path
|
||||
|
||||
@field_validator("home")
|
||||
@classmethod
|
||||
|
||||
+40
-21
@@ -14,8 +14,7 @@ from sqlalchemy.ext.asyncio import (
|
||||
async_scoped_session,
|
||||
)
|
||||
|
||||
from basic_memory.models import Base
|
||||
|
||||
from basic_memory.models import Base, SCHEMA_VERSION
|
||||
|
||||
# Module level state
|
||||
_engine: Optional[AsyncEngine] = None
|
||||
@@ -69,12 +68,31 @@ async def scoped_session(
|
||||
await factory.remove()
|
||||
|
||||
|
||||
async def init_db(session: AsyncSession):
|
||||
async def init_db() -> None:
|
||||
"""Initialize database with required tables."""
|
||||
await session.execute(text("PRAGMA foreign_keys=ON"))
|
||||
conn = await session.connection()
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
await session.commit()
|
||||
|
||||
logger.info("Initializing database...")
|
||||
|
||||
async with scoped_session(_session_maker) as session:
|
||||
await session.execute(text("PRAGMA foreign_keys=ON"))
|
||||
conn = await session.connection()
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
await session.commit()
|
||||
|
||||
async def drop_db():
|
||||
"""Drop all database tables."""
|
||||
global _engine, _session_maker
|
||||
|
||||
logger.info("Dropping tables...")
|
||||
async with scoped_session(_session_maker) as session:
|
||||
conn = await session.connection()
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await session.commit()
|
||||
|
||||
# reset global engine and session_maker
|
||||
_engine = None
|
||||
_session_maker = None
|
||||
|
||||
|
||||
async def get_or_create_db(
|
||||
@@ -83,7 +101,7 @@ async def get_or_create_db(
|
||||
) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
|
||||
"""Get or create database engine and session maker."""
|
||||
global _engine, _session_maker
|
||||
|
||||
|
||||
if _engine is None:
|
||||
db_url = DatabaseType.get_db_url(db_path, db_type)
|
||||
logger.debug(f"Creating engine for db_url: {db_url}")
|
||||
@@ -91,9 +109,7 @@ async def get_or_create_db(
|
||||
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
||||
|
||||
# Initialize database
|
||||
logger.debug("Initializing database...")
|
||||
async with scoped_session(_session_maker) as db_session:
|
||||
await init_db(db_session)
|
||||
await init_db()
|
||||
|
||||
return _engine, _session_maker
|
||||
|
||||
@@ -101,35 +117,38 @@ async def get_or_create_db(
|
||||
async def shutdown_db():
|
||||
"""Clean up database connections."""
|
||||
global _engine, _session_maker
|
||||
|
||||
|
||||
if _engine:
|
||||
await _engine.dispose()
|
||||
_engine = None
|
||||
_session_maker = None
|
||||
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def engine_session_factory(
|
||||
db_path: Path,
|
||||
db_type: DatabaseType = DatabaseType.FILESYSTEM,
|
||||
db_type: DatabaseType = DatabaseType.MEMORY,
|
||||
init: bool = True,
|
||||
) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
|
||||
"""Create engine and session factory.
|
||||
|
||||
|
||||
Note: This is primarily used for testing where we want a fresh database
|
||||
for each test. For production use, use get_or_create_db() instead.
|
||||
"""
|
||||
|
||||
global _engine, _session_maker
|
||||
|
||||
db_url = DatabaseType.get_db_url(db_path, db_type)
|
||||
logger.debug(f"Creating engine for db_url: {db_url}")
|
||||
engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
|
||||
|
||||
_engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
|
||||
try:
|
||||
factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
_session_maker = async_sessionmaker(_engine, expire_on_commit=False)
|
||||
|
||||
if init:
|
||||
logger.debug("Initializing database...")
|
||||
async with scoped_session(factory) as db_session:
|
||||
await init_db(db_session)
|
||||
await init_db()
|
||||
|
||||
yield engine, factory
|
||||
yield _engine, _session_maker
|
||||
finally:
|
||||
await engine.dispose()
|
||||
await _engine.dispose()
|
||||
|
||||
@@ -170,79 +170,44 @@ def remove_frontmatter(content: str) -> str:
|
||||
raise
|
||||
|
||||
|
||||
def remove_frontmatter_lenient(content: str) -> str:
|
||||
"""
|
||||
Remove frontmatter markers and anything between them without validation.
|
||||
|
||||
This is a more permissive version of remove_frontmatter that doesn't
|
||||
try to validate the YAML content. It simply removes everything between
|
||||
the first two '---' markers if they exist.
|
||||
async def update_frontmatter(path: Path, updates: Dict[str, Any]) -> str:
|
||||
"""Update frontmatter fields in a file while preserving all content.
|
||||
|
||||
Only modifies the frontmatter section, leaving all content untouched.
|
||||
Creates frontmatter section if none exists.
|
||||
Returns checksum of updated file.
|
||||
|
||||
Args:
|
||||
content: Content that may contain frontmatter
|
||||
path: Path to markdown file
|
||||
updates: Dict of frontmatter fields to update
|
||||
|
||||
Returns:
|
||||
Content with any frontmatter markers and content removed
|
||||
"""
|
||||
content = content.strip()
|
||||
if not content.startswith("---"):
|
||||
return content
|
||||
|
||||
# Find the second marker
|
||||
rest = content[3:].strip()
|
||||
if "---" not in rest:
|
||||
return content
|
||||
|
||||
# Split on the second marker and take everything after
|
||||
parts = rest.split("---", 1)
|
||||
return parts[1].strip()
|
||||
|
||||
|
||||
async def add_frontmatter(content: str, frontmatter: Dict[str, Any]) -> str:
|
||||
"""
|
||||
Add YAML frontmatter to content.
|
||||
|
||||
Args:
|
||||
content: Main content text
|
||||
frontmatter: Key-value pairs for frontmatter
|
||||
|
||||
Returns:
|
||||
Content with YAML frontmatter prepended
|
||||
|
||||
Raises:
|
||||
ParseError: If YAML serialization fails
|
||||
"""
|
||||
try:
|
||||
yaml_fm = yaml.dump(frontmatter, sort_keys=False)
|
||||
return f"---\n{yaml_fm}---\n\n{content.strip()}"
|
||||
except yaml.YAMLError as e:
|
||||
logger.error(f"Failed to add frontmatter: {e}")
|
||||
raise ParseError(f"Failed to add frontmatter: {e}")
|
||||
|
||||
|
||||
async def parse_content_with_frontmatter(content: str) -> Tuple[Dict[str, Any], str]:
|
||||
"""
|
||||
Parse both frontmatter and content.
|
||||
|
||||
Args:
|
||||
content: Text content with optional frontmatter
|
||||
|
||||
Returns:
|
||||
Tuple of (frontmatter dict, content without frontmatter)
|
||||
Checksum of updated file
|
||||
|
||||
Raises:
|
||||
ParseError: If parsing fails
|
||||
FileError: If file operations fail
|
||||
ParseError: If frontmatter parsing fails
|
||||
"""
|
||||
try:
|
||||
if not has_frontmatter(content):
|
||||
return {}, content.strip()
|
||||
# Read current content
|
||||
content = path.read_text()
|
||||
|
||||
frontmatter = parse_frontmatter(content)
|
||||
remaining = remove_frontmatter(content)
|
||||
return frontmatter, remaining
|
||||
# Parse current frontmatter
|
||||
current_fm = {}
|
||||
if has_frontmatter(content):
|
||||
current_fm = parse_frontmatter(content)
|
||||
content = remove_frontmatter(content)
|
||||
|
||||
# Update frontmatter
|
||||
new_fm = {**current_fm, **updates}
|
||||
|
||||
# Write new file with updated frontmatter
|
||||
yaml_fm = yaml.dump(new_fm, sort_keys=False)
|
||||
final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
|
||||
|
||||
await write_file_atomic(path, final_content)
|
||||
return await compute_checksum(final_content)
|
||||
|
||||
except Exception as e:
|
||||
if not isinstance(e, ParseError):
|
||||
logger.error(f"Failed to parse content with frontmatter: {e}")
|
||||
raise ParseError(f"Failed to parse content with frontmatter: {e}")
|
||||
raise
|
||||
logger.error(f"Failed to update frontmatter in {path}: {e}")
|
||||
raise FileError(f"Failed to update frontmatter: {e}")
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from basic_memory.file_utils import ParseError
|
||||
from basic_memory.markdown.entity_parser import EntityParser
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import (
|
||||
EntityMarkdown,
|
||||
EntityFrontmatter,
|
||||
@@ -13,6 +14,7 @@ __all__ = [
|
||||
"EntityMarkdown",
|
||||
"EntityFrontmatter",
|
||||
"EntityParser",
|
||||
"MarkdownProcessor",
|
||||
"Observation",
|
||||
"Relation",
|
||||
"ParseError",
|
||||
|
||||
@@ -116,7 +116,7 @@ class EntityParser:
|
||||
|
||||
metadata = post.metadata
|
||||
metadata["title"] = post.metadata.get("title", file_path.name)
|
||||
metadata["type"] = metadata.get("type", "note")
|
||||
metadata["type"] = post.metadata.get("type", "note")
|
||||
metadata["tags"] = parse_tags(post.metadata.get("tags", []))
|
||||
|
||||
# frontmatter
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
"""Process markdown files with structured sections.
|
||||
|
||||
This module follows a Read -> Modify -> Write pattern for all file operations:
|
||||
1. Read entire file and parse into EntityMarkdown schema
|
||||
2. Modify the schema (add relation, update content, etc)
|
||||
3. Write entire file atomically using temp file + swap
|
||||
|
||||
No in-place updates are performed. Each write reconstructs the entire file from the schema.
|
||||
The file format has two distinct types of content:
|
||||
1. User content - Free form text that is preserved exactly as written
|
||||
2. Structured sections - Observations and Relations that are always formatted
|
||||
in a standard way and can be overwritten since they're tracked in our schema
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from collections import OrderedDict
|
||||
@@ -33,6 +19,8 @@ class DirtyFileError(Exception):
|
||||
|
||||
class MarkdownProcessor:
|
||||
"""Process markdown files while preserving content and structure.
|
||||
|
||||
used only for import
|
||||
|
||||
This class handles the file I/O aspects of our markdown processing. It:
|
||||
1. Uses EntityParser for reading/parsing files into our schema
|
||||
|
||||
@@ -68,7 +68,7 @@ def parse_observation(token: Token) -> Dict[str, Any]:
|
||||
|
||||
return {
|
||||
'category': category,
|
||||
'content': ' '.join(content_parts).strip(),
|
||||
'content': content,
|
||||
'tags': list(tags) if tags else None,
|
||||
'context': context
|
||||
}
|
||||
|
||||
@@ -9,15 +9,13 @@ from pydantic import BaseModel
|
||||
class Observation(BaseModel):
|
||||
"""An observation about an entity."""
|
||||
|
||||
category: Optional[str] = None
|
||||
category: Optional[str] = "Note"
|
||||
content: str
|
||||
tags: Optional[List[str]] = None
|
||||
context: Optional[str] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
obs_string = f"- [{self.category}] {self.content}"
|
||||
if self.tags:
|
||||
obs_string += " " + " ".join(f"#{tag}" for tag in sorted(self.tags))
|
||||
if self.context:
|
||||
obs_string += f" ({self.context})"
|
||||
return obs_string
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Discussion context tools for Basic Memory MCP server."""
|
||||
|
||||
from typing import Optional, Literal
|
||||
from typing import Optional, Literal, List
|
||||
|
||||
from loguru import logger
|
||||
|
||||
@@ -9,6 +9,7 @@ from basic_memory.mcp.server import mcp
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
from basic_memory.schemas.memory import GraphContext, MemoryUrl, memory_url, memory_url_path, normalize_memory_url
|
||||
from basic_memory.schemas.base import TimeFrame
|
||||
from basic_memory.schemas.search import SearchItemType
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
@@ -83,7 +84,7 @@ async def build_context(
|
||||
""",
|
||||
)
|
||||
async def recent_activity(
|
||||
type: Literal["entity", "observation", "relation"] = None,
|
||||
type: List[Literal["entity", "observation", "relation"]] = None,
|
||||
depth: Optional[int] = 1,
|
||||
timeframe: Optional[TimeFrame] = "7d",
|
||||
max_results: int = 10,
|
||||
@@ -110,6 +111,9 @@ async def recent_activity(
|
||||
- metadata: Query details and statistics
|
||||
|
||||
Examples:
|
||||
# Get all entities for the last 10 days (default)
|
||||
recent_activity()
|
||||
|
||||
# Get all entities from yesterday
|
||||
recent_activity(type=["entity"], timeframe="yesterday")
|
||||
|
||||
@@ -131,8 +135,9 @@ async def recent_activity(
|
||||
"depth": depth,
|
||||
"timeframe": timeframe,
|
||||
"max_results": max_results,
|
||||
"type": type if type else None,
|
||||
}
|
||||
if type:
|
||||
params["type"] = type
|
||||
|
||||
response = await call_get(
|
||||
client,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""Models package for basic-memory."""
|
||||
|
||||
import basic_memory
|
||||
from basic_memory.models.base import Base
|
||||
from basic_memory.models.knowledge import Entity, Observation, Relation, ObservationCategory
|
||||
|
||||
SCHEMA_VERSION = basic_memory.__version__ + "-" + "003"
|
||||
|
||||
__all__ = [
|
||||
'Base',
|
||||
'Entity',
|
||||
'Observation',
|
||||
'ObservationCategory',
|
||||
'Relation'
|
||||
]
|
||||
"Base",
|
||||
"Entity",
|
||||
"Observation",
|
||||
"ObservationCategory",
|
||||
"Relation",
|
||||
]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Base model class for SQLAlchemy models."""
|
||||
|
||||
from sqlalchemy import String, Integer
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class Base(AsyncAttrs, DeclarativeBase):
|
||||
"""Base class for all models"""
|
||||
pass
|
||||
pass
|
||||
|
||||
|
||||
@@ -95,14 +95,15 @@ class SearchRepository:
|
||||
permalink_match: Optional[str] = None,
|
||||
title: Optional[str] = None,
|
||||
types: List[SearchItemType] = None,
|
||||
after_date: datetime = None,
|
||||
after_date: Optional[datetime] = None,
|
||||
entity_types: List[str] = None,
|
||||
limit: int = 10,
|
||||
) -> List[SearchIndexRow]:
|
||||
"""Search across all indexed content with fuzzy matching."""
|
||||
conditions = []
|
||||
params = {}
|
||||
|
||||
order_by_clause = ""
|
||||
|
||||
# Handle text search for title and content
|
||||
if search_text:
|
||||
search_text = self._quote_search_term(search_text.lower().strip())
|
||||
@@ -139,6 +140,9 @@ class SearchRepository:
|
||||
if after_date:
|
||||
params["after_date"] = after_date
|
||||
conditions.append("datetime(created_at) > datetime(:after_date)")
|
||||
|
||||
# order by most recent first
|
||||
order_by_clause = ", updated_at DESC"
|
||||
|
||||
# set limit on search query
|
||||
params["limit"] = limit
|
||||
@@ -165,7 +169,7 @@ class SearchRepository:
|
||||
bm25(search_index) as score
|
||||
FROM search_index
|
||||
WHERE {where_clause}
|
||||
ORDER BY score ASC
|
||||
ORDER BY score ASC {order_by_clause}
|
||||
LIMIT :limit
|
||||
"""
|
||||
|
||||
@@ -197,6 +201,7 @@ class SearchRepository:
|
||||
|
||||
#for r in results:
|
||||
# logger.debug(f"Search result: type:{r.type} title: {r.title} permalink: {r.permalink} score: {r.score}")
|
||||
|
||||
return results
|
||||
|
||||
async def index_item(
|
||||
|
||||
@@ -181,6 +181,9 @@ class Entity(BaseModel):
|
||||
- Optional relations to other entities
|
||||
- Optional description for high-level overview
|
||||
"""
|
||||
|
||||
# private field to override permalink
|
||||
_permalink: Optional[str] = None
|
||||
|
||||
title: str
|
||||
content: Optional[str] = None
|
||||
@@ -199,8 +202,8 @@ class Entity(BaseModel):
|
||||
|
||||
@property
|
||||
def permalink(self) -> PathId:
|
||||
"""Get the path ID in format {snake_case_title}."""
|
||||
return generate_permalink(self.file_path)
|
||||
"""Get a url friendly path}."""
|
||||
return self._permalink or generate_permalink(self.file_path)
|
||||
|
||||
@model_validator(mode="after")
|
||||
@classmethod
|
||||
|
||||
@@ -59,6 +59,7 @@ def memory_url_path(url: memory_url) -> str:
|
||||
class EntitySummary(BaseModel):
|
||||
"""Simplified entity representation."""
|
||||
|
||||
type: str = "entity"
|
||||
permalink: str
|
||||
title: str
|
||||
file_path: str
|
||||
@@ -68,8 +69,9 @@ class EntitySummary(BaseModel):
|
||||
class RelationSummary(BaseModel):
|
||||
"""Simplified relation representation."""
|
||||
|
||||
type: str = "relation"
|
||||
permalink: str
|
||||
type: str
|
||||
relation_type: str
|
||||
from_id: str
|
||||
to_id: Optional[str] = None
|
||||
|
||||
@@ -77,6 +79,7 @@ class RelationSummary(BaseModel):
|
||||
class ObservationSummary(BaseModel):
|
||||
"""Simplified observation representation."""
|
||||
|
||||
type: str = "observation"
|
||||
permalink: str
|
||||
category: str
|
||||
content: str
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Services package."""
|
||||
|
||||
from .database_service import DatabaseService
|
||||
from .service import BaseService
|
||||
from .file_service import FileService
|
||||
from .entity_service import EntityService
|
||||
@@ -8,4 +8,5 @@ __all__ = [
|
||||
"BaseService",
|
||||
"FileService",
|
||||
"EntityService",
|
||||
"DatabaseService"
|
||||
]
|
||||
|
||||
@@ -76,7 +76,7 @@ class ContextService:
|
||||
primary = await self.search_repository.search(permalink=path)
|
||||
else:
|
||||
logger.debug(f"Build context for '{types}'")
|
||||
primary = await self.search_repository.search(types=types)
|
||||
primary = await self.search_repository.search(types=types, after_date=since)
|
||||
|
||||
# Get type_id pairs for traversal
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Service for managing database lifecycle and schema validation."""
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, List
|
||||
|
||||
from alembic.runtime.migration import MigrationContext
|
||||
from alembic.autogenerate import compare_metadata
|
||||
from loguru import logger
|
||||
from sqlalchemy import MetaData
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.models import Base
|
||||
|
||||
|
||||
async def check_schema_matches_models(session: AsyncSession) -> Tuple[bool, List[str]]:
|
||||
"""Check if database schema matches SQLAlchemy models.
|
||||
|
||||
Returns:
|
||||
tuple[bool, list[str]]: (matches, list of differences)
|
||||
"""
|
||||
# Get current DB schema via migration context
|
||||
conn = await session.connection()
|
||||
|
||||
def _compare_schemas(connection):
|
||||
context = MigrationContext.configure(connection)
|
||||
return compare_metadata(context, Base.metadata)
|
||||
|
||||
# Run comparison in sync context
|
||||
differences = await conn.run_sync(_compare_schemas)
|
||||
|
||||
if not differences:
|
||||
return True, []
|
||||
|
||||
# Format differences into readable messages
|
||||
diff_messages = []
|
||||
for diff in differences:
|
||||
if diff[0] == 'add_table':
|
||||
diff_messages.append(f"Missing table: {diff[1].name}")
|
||||
elif diff[0] == 'remove_table':
|
||||
diff_messages.append(f"Extra table: {diff[1].name}")
|
||||
elif diff[0] == 'add_column':
|
||||
diff_messages.append(f"Missing column: {diff[3]} in table {diff[2]}")
|
||||
elif diff[0] == 'remove_column':
|
||||
diff_messages.append(f"Extra column: {diff[3]} in table {diff[2]}")
|
||||
elif diff[0] == 'modify_type':
|
||||
diff_messages.append(f"Column type mismatch: {diff[3]} in table {diff[2]}")
|
||||
|
||||
return False, diff_messages
|
||||
|
||||
|
||||
class DatabaseService:
|
||||
"""Manages database lifecycle including schema validation and backups."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: ProjectConfig,
|
||||
db_type: db.DatabaseType = db.DatabaseType.FILESYSTEM,
|
||||
):
|
||||
self.config = config
|
||||
self.db_path = Path(config.database_path)
|
||||
self.db_type = db_type
|
||||
|
||||
async def create_backup(self) -> Optional[Path]:
|
||||
"""Create backup of existing database file.
|
||||
|
||||
Returns:
|
||||
Optional[Path]: Path to backup file if created, None if no DB exists
|
||||
"""
|
||||
if self.db_type == db.DatabaseType.MEMORY:
|
||||
return None # Skip backups for in-memory DB
|
||||
|
||||
if not self.db_path.exists():
|
||||
return None
|
||||
|
||||
# Create backup with timestamp
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = self.db_path.with_suffix(f".{timestamp}.backup")
|
||||
|
||||
try:
|
||||
self.db_path.rename(backup_path)
|
||||
logger.info(f"Created database backup: {backup_path}")
|
||||
|
||||
# make a new empty file
|
||||
self.db_path.touch()
|
||||
return backup_path
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create database backup: {e}")
|
||||
return None
|
||||
|
||||
async def initialize_db(self):
|
||||
"""Initialize database with current schema."""
|
||||
logger.info("Initializing database...")
|
||||
|
||||
if self.db_type == db.DatabaseType.FILESYSTEM:
|
||||
await self.create_backup()
|
||||
|
||||
# Drop existing tables if any
|
||||
await db.drop_db()
|
||||
|
||||
# Create tables with current schema
|
||||
await db.get_or_create_db(
|
||||
db_path=self.db_path,
|
||||
db_type=self.db_type
|
||||
)
|
||||
|
||||
logger.info("Database initialized with current schema")
|
||||
|
||||
async def check_db(self) -> bool:
|
||||
"""Check database state and rebuild if schema doesn't match models.
|
||||
|
||||
Returns:
|
||||
bool: True if DB is ready for use, False if initialization failed
|
||||
"""
|
||||
try:
|
||||
_, session_maker = await db.get_or_create_db(
|
||||
db_path=self.db_path,
|
||||
db_type=self.db_type
|
||||
)
|
||||
async with db.scoped_session(session_maker) as db_session:
|
||||
# Check actual schema matches
|
||||
matches, differences = await check_schema_matches_models(db_session)
|
||||
if not matches:
|
||||
logger.warning("Database schema does not match models:")
|
||||
for diff in differences:
|
||||
logger.warning(f" {diff}")
|
||||
logger.info("Rebuilding database to match current models...")
|
||||
await self.initialize_db()
|
||||
return True
|
||||
|
||||
logger.info("Database schema matches models")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Database initialization failed: {e}")
|
||||
return False
|
||||
|
||||
async def cleanup_backups(self, keep_count: int = 5):
|
||||
"""Clean up old database backups, keeping the N most recent."""
|
||||
if self.db_type == db.DatabaseType.MEMORY:
|
||||
return # Skip cleanup for in-memory DB
|
||||
|
||||
backup_pattern = "*.backup" # Use relative pattern
|
||||
backups = sorted(
|
||||
self.db_path.parent.glob(backup_pattern),
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
# Remove old backups
|
||||
for backup in backups[keep_count:]:
|
||||
try:
|
||||
backup.unlink()
|
||||
logger.debug(f"Removed old backup: {backup}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to remove backup {backup}: {e}")
|
||||
@@ -19,6 +19,7 @@ from basic_memory.services import FileService
|
||||
from basic_memory.services import BaseService
|
||||
from basic_memory.services.link_resolver import LinkResolver
|
||||
from basic_memory.markdown.entity_parser import EntityParser
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
class EntityService(BaseService[EntityModel]):
|
||||
@@ -40,6 +41,51 @@ class EntityService(BaseService[EntityModel]):
|
||||
self.file_service = file_service
|
||||
self.link_resolver = link_resolver
|
||||
|
||||
async def resolve_permalink(
|
||||
self,
|
||||
file_path: Path,
|
||||
markdown: Optional[EntityMarkdown] = None
|
||||
) -> str:
|
||||
"""Get or generate unique permalink for an entity.
|
||||
|
||||
Priority:
|
||||
1. If markdown has permalink and it's not used by another file -> use as is
|
||||
2. If markdown has permalink but it's used by another file -> make unique
|
||||
3. For existing files, keep current permalink from db
|
||||
4. Generate new unique permalink from file path
|
||||
"""
|
||||
file_path = str(file_path)
|
||||
|
||||
# If markdown has explicit permalink, try to validate it
|
||||
if markdown and markdown.frontmatter.permalink:
|
||||
desired_permalink = markdown.frontmatter.permalink
|
||||
existing = await self.repository.get_by_permalink(desired_permalink)
|
||||
|
||||
# If no conflict or it's our own file, use as is
|
||||
if not existing or existing.file_path == file_path:
|
||||
return desired_permalink
|
||||
|
||||
# For existing files, try to find current permalink
|
||||
existing = await self.repository.get_by_file_path(file_path)
|
||||
if existing:
|
||||
return existing.permalink
|
||||
|
||||
# New file - generate permalink
|
||||
if markdown and markdown.frontmatter.permalink:
|
||||
desired_permalink = markdown.frontmatter.permalink
|
||||
else:
|
||||
desired_permalink = generate_permalink(file_path)
|
||||
|
||||
# Make unique if needed
|
||||
permalink = desired_permalink
|
||||
suffix = 1
|
||||
while await self.repository.get_by_permalink(permalink):
|
||||
permalink = f"{desired_permalink}-{suffix}"
|
||||
suffix += 1
|
||||
logger.debug(f"creating unique permalink: {permalink}")
|
||||
|
||||
return permalink
|
||||
|
||||
async def create_or_update_entity(self, schema: EntitySchema) -> (EntityModel, bool):
|
||||
"""Create new entity or update existing one.
|
||||
if a new entity is created, the return value is (entity, True)
|
||||
@@ -66,9 +112,13 @@ class EntityService(BaseService[EntityModel]):
|
||||
|
||||
if await self.file_service.exists(file_path):
|
||||
raise EntityCreationError(
|
||||
f"file_path {file_path} for entity {schema.permalink} already exists: {file_path}"
|
||||
f"file for entity {schema.folder}/{schema.title} already exists: {file_path}"
|
||||
)
|
||||
|
||||
# Get unique permalink
|
||||
permalink = await self.resolve_permalink(schema.permalink or file_path)
|
||||
schema._permalink = permalink
|
||||
|
||||
post = await schema_to_markdown(schema)
|
||||
|
||||
# write file
|
||||
@@ -184,7 +234,7 @@ class EntityService(BaseService[EntityModel]):
|
||||
Creates the entity with null checksum to indicate sync not complete.
|
||||
Relations will be added in second pass.
|
||||
"""
|
||||
logger.debug(f"Creating entity: {markdown.frontmatter.title}")
|
||||
logger.debug(f"Creating entity: {markdown.frontmatter.title}")
|
||||
model = entity_model_from_markdown(file_path, markdown)
|
||||
|
||||
# Mark as incomplete sync
|
||||
|
||||
@@ -35,7 +35,6 @@ class FileService:
|
||||
"""Generate absolute filesystem path for entity."""
|
||||
return self.base_path / f"{entity.file_path}"
|
||||
|
||||
# TODO move to tests
|
||||
async def write_entity_file(
|
||||
self,
|
||||
entity: EntityModel,
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Dict
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory import file_utils
|
||||
from basic_memory.markdown import EntityParser, EntityMarkdown
|
||||
from basic_memory.repository import EntityRepository, RelationRepository
|
||||
from basic_memory.services import EntityService
|
||||
@@ -92,8 +93,32 @@ class SyncService:
|
||||
# First pass: Create/update entities
|
||||
# entities will have a null checksum to indicate they are not complete
|
||||
for file_path, entity_markdown in parsed_entities.items():
|
||||
|
||||
# Get unique permalink and update markdown if needed
|
||||
permalink = await self.entity_service.resolve_permalink(
|
||||
file_path,
|
||||
markdown=entity_markdown
|
||||
)
|
||||
|
||||
if permalink != entity_markdown.frontmatter.permalink:
|
||||
# Add/update permalink in frontmatter
|
||||
logger.info(f"Adding permalink '{permalink}' to file: {file_path}")
|
||||
|
||||
# update markdown
|
||||
entity_markdown.frontmatter.metadata["permalink"] = permalink
|
||||
|
||||
# update file frontmatter
|
||||
updated_checksum = await file_utils.update_frontmatter(
|
||||
directory / file_path,
|
||||
{"permalink": permalink}
|
||||
)
|
||||
|
||||
# Update checksum in changes report since file was modified
|
||||
changes.checksums[file_path] = updated_checksum
|
||||
|
||||
# if the file is new, create an entity
|
||||
if file_path in changes.new:
|
||||
# Create entity with final permalink
|
||||
logger.debug(f"Creating new entity_markdown: {file_path}")
|
||||
await self.entity_service.create_entity_from_markdown(
|
||||
file_path, entity_markdown
|
||||
|
||||
@@ -68,7 +68,7 @@ async def test_create_entity_observations_relations(client: AsyncClient, file_se
|
||||
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0].category == "note"
|
||||
assert entity.observations[0].content == "This is notable"
|
||||
assert entity.observations[0].content == "This is notable #tag1"
|
||||
assert entity.observations[0].tags == ["tag1"]
|
||||
assert entity.observations[0].context == "testing"
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Test import-json command functionality."""
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from rich.console import Console
|
||||
|
||||
from basic_memory.cli.commands.import_memory_json import process_memory_json
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def console():
|
||||
"""Create test console that captures output."""
|
||||
output = StringIO()
|
||||
return Console(file=output), output
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_memory_json(tmp_path) -> Path:
|
||||
"""Create a sample memory.json file with test data."""
|
||||
json_path = tmp_path / "memory.json"
|
||||
|
||||
# Create test data modeling the real format
|
||||
test_data = [
|
||||
{
|
||||
"type": "entity",
|
||||
"name": "Basic_Memory",
|
||||
"entityType": "software_system",
|
||||
"observations": [
|
||||
"A core component of Basic Machines",
|
||||
"Local-first knowledge management system",
|
||||
"Combines filesystem persistence with graph-based knowledge representation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"name": "Basic_Machines",
|
||||
"entityType": "project",
|
||||
"observations": [
|
||||
"Local-first knowledge management system",
|
||||
"Focuses on enhancing human agency and understanding",
|
||||
"Current focus includes basic-memory system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"from": "Basic_Memory",
|
||||
"to": "Basic_Machines",
|
||||
"relationType": "is_component_of"
|
||||
}
|
||||
]
|
||||
|
||||
# Write each item as a JSON line
|
||||
with open(json_path, 'w') as f:
|
||||
for item in test_data:
|
||||
f.write(json.dumps(item) + '\n')
|
||||
|
||||
return json_path
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_memory_json(
|
||||
sample_memory_json: Path,
|
||||
markdown_processor: MarkdownProcessor,
|
||||
test_config,
|
||||
):
|
||||
"""Test importing from memory.json format."""
|
||||
# Process the import
|
||||
results = await process_memory_json(sample_memory_json, test_config.home, markdown_processor)
|
||||
|
||||
# Check results
|
||||
assert results["entities"] == 2
|
||||
assert results["relations"] == 1
|
||||
|
||||
# Verify Basic_Memory entity file was created correctly
|
||||
basic_memory_path = test_config.home / "software_system/Basic_Memory.md"
|
||||
entity = await markdown_processor.read_file(basic_memory_path)
|
||||
|
||||
assert entity.frontmatter.title == "Basic_Memory"
|
||||
assert entity.frontmatter.type == "software_system"
|
||||
assert len(entity.observations) == 3
|
||||
assert len(entity.relations) == 1 # Should have the outgoing relation
|
||||
assert entity.relations[0].type == "is_component_of"
|
||||
assert entity.relations[0].target == "Basic_Machines"
|
||||
|
||||
# Verify Basic_Machines entity file
|
||||
basic_machines_path = test_config.home / "project/Basic_Machines.md"
|
||||
entity = await markdown_processor.read_file(basic_machines_path)
|
||||
|
||||
assert entity.frontmatter.title == "Basic_Machines"
|
||||
assert entity.frontmatter.type == "project"
|
||||
assert len(entity.observations) == 3
|
||||
assert len(entity.relations) == 0 # No outgoing relations
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_memory_json_empty_observations(
|
||||
tmp_path: Path,
|
||||
markdown_processor: MarkdownProcessor,
|
||||
test_config,
|
||||
):
|
||||
"""Test handling entities with no observations."""
|
||||
# Create test data
|
||||
json_path = tmp_path / "memory.json"
|
||||
test_data = [
|
||||
{
|
||||
"type": "entity",
|
||||
"name": "Empty_Entity",
|
||||
"entityType": "test",
|
||||
"observations": [] # Empty observations
|
||||
}
|
||||
]
|
||||
|
||||
with open(json_path, 'w') as f:
|
||||
for item in test_data:
|
||||
f.write(json.dumps(item) + '\n')
|
||||
|
||||
# Process import
|
||||
results = await process_memory_json(json_path, test_config.home, markdown_processor)
|
||||
|
||||
# Check results
|
||||
assert results["entities"] == 1
|
||||
assert results["relations"] == 0
|
||||
|
||||
# Verify file was created
|
||||
entity_path = test_config.home / "test/Empty_Entity.md"
|
||||
entity = await markdown_processor.read_file(entity_path)
|
||||
|
||||
assert entity.frontmatter.title == "Empty_Entity"
|
||||
assert entity.observations == []
|
||||
assert entity.relations == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_memory_json_special_characters(
|
||||
tmp_path: Path,
|
||||
markdown_processor: MarkdownProcessor,
|
||||
test_config,
|
||||
):
|
||||
"""Test handling entities with special characters in text."""
|
||||
# Create test data
|
||||
json_path = tmp_path / "memory.json"
|
||||
test_data = [
|
||||
{
|
||||
"type": "entity",
|
||||
"name": "Special_Entity",
|
||||
"entityType": "test",
|
||||
"observations": [
|
||||
"Contains *markdown* formatting",
|
||||
"Has #hashtags and @mentions",
|
||||
"Uses [square brackets] and {curly braces}"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
with open(json_path, 'w') as f:
|
||||
for item in test_data:
|
||||
f.write(json.dumps(item) + '\n')
|
||||
|
||||
# Process import
|
||||
results = await process_memory_json(json_path, test_config.home, markdown_processor)
|
||||
assert results["entities"] == 1
|
||||
|
||||
# Verify file was created and content preserved
|
||||
entity_path = test_config.home / "test/Special_Entity.md"
|
||||
entity = await markdown_processor.read_file(entity_path)
|
||||
|
||||
assert len(entity.observations) == 3
|
||||
assert entity.observations[0].content == "Contains *markdown* formatting"
|
||||
assert entity.observations[1].content == "Has #hashtags and @mentions"
|
||||
assert entity.observations[2].content == "Uses [square brackets] and {curly braces}"
|
||||
@@ -1,170 +0,0 @@
|
||||
# """Test status command functionality."""
|
||||
#
|
||||
# from io import StringIO
|
||||
#
|
||||
# import pytest
|
||||
# from rich.console import Console
|
||||
#
|
||||
# from basic_memory.cli.commands.status import display_changes, run_status
|
||||
# from basic_memory.sync.file_change_scanner import FileState
|
||||
# from basic_memory.sync.utils import SyncReport
|
||||
# from basic_memory.file_utils import compute_checksum
|
||||
#
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def console():
|
||||
# """Create test console that captures output."""
|
||||
# output = StringIO()
|
||||
# return Console(file=output), output
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_display_no_changes(console):
|
||||
# """Test display with no changes."""
|
||||
# test_console, output = console
|
||||
# changes = SyncReport()
|
||||
# display_changes("Test Files", changes, verbose=False)
|
||||
# assert "No changes" in output.getvalue()
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_display_compact_changes(console):
|
||||
# """Test compact display of changes."""
|
||||
# test_console, output = console
|
||||
# changes = SyncReport(
|
||||
# new={"docs/new.md"},
|
||||
# modified={"docs/mod.md"},
|
||||
# deleted={"old/deleted.md"},
|
||||
# moved={
|
||||
# "new/location.md": FileState(
|
||||
# permalink="new/location.md", checksum="abc123", moved_from="old/location.md"
|
||||
# )
|
||||
# },
|
||||
# )
|
||||
# display_changes("Test Files", changes, verbose=False)
|
||||
# output_text = output.getvalue()
|
||||
#
|
||||
# # Check directory summaries
|
||||
# assert "docs/ +1 new" in output_text.replace(" ", " ")
|
||||
# assert "docs/ ~1 modified" in output_text.replace(" ", " ")
|
||||
# assert "old/ -1 deleted" in output_text.replace(" ", " ")
|
||||
# assert "new/ ->1 moved" in output_text.replace(" ", " ")
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_display_verbose_changes(console):
|
||||
# """Test verbose display of changes."""
|
||||
# test_console, output = console
|
||||
# changes = SyncReport(
|
||||
# new={"docs/new.md"},
|
||||
# modified={"docs/mod.md"},
|
||||
# deleted={"old/deleted.md"},
|
||||
# moved={
|
||||
# "new/location.md": FileState(
|
||||
# permalink="new/location.md",
|
||||
# checksum="abc123def", # 8 chars for display
|
||||
# moved_from="old/location.md",
|
||||
# )
|
||||
# },
|
||||
# checksums={
|
||||
# "docs/new.md": "def456789abcdef",
|
||||
# "docs/mod.md": "ghi789abcdef123",
|
||||
# },
|
||||
# )
|
||||
# display_changes("Test Files", changes, verbose=True)
|
||||
# output_text = output.getvalue()
|
||||
#
|
||||
# # Verify sections
|
||||
# assert "New Files" in output_text
|
||||
# assert "Modified" in output_text
|
||||
# assert "Deleted" in output_text
|
||||
# assert "Moved" in output_text
|
||||
#
|
||||
# # Check file listings with checksums
|
||||
# assert "new.md (def45678)" in output_text
|
||||
# assert "mod.md (ghi78789)" in output_text
|
||||
# assert "location.md (abc123de)" in output_text
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_end_to_end_status(file_change_scanner, test_config, entity_repository):
|
||||
# """Test complete status command with real files."""
|
||||
# # Create test files in both knowledge and documents directories
|
||||
# project_dir = test_config.home
|
||||
#
|
||||
# # Create knowledge files
|
||||
# component_dir = project_dir / "component"
|
||||
# component_dir.mkdir(exist_ok=True)
|
||||
# component_path = component_dir / "test.md"
|
||||
# component_path.write_text("test component")
|
||||
#
|
||||
# # Add some files to DB with different paths to test moves
|
||||
# await entity_repository.create(
|
||||
# {"permalink": "old/doc.md", "file_path": "old/doc.md", "checksum": "abc123"}
|
||||
# )
|
||||
#
|
||||
# # Run status check
|
||||
# await run_status(file_change_scanner, verbose=True)
|
||||
#
|
||||
# # Verify changes through sync service directly
|
||||
# doc_changes = await file_change_scanner.find_knowledge_changes(project_dir)
|
||||
# assert len(doc_changes.new) == 2 # test.md and nested.md
|
||||
# assert "test.md" in doc_changes.new
|
||||
# assert "subdir/nested.md" in doc_changes.new
|
||||
# assert len(doc_changes.deleted) == 1 # old/doc.md
|
||||
#
|
||||
# knowledge_changes = await file_change_scanner.find_knowledge_changes(project_dir)
|
||||
# assert len(knowledge_changes.new) == 1
|
||||
# assert "component/test.md" in knowledge_changes.new
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_status_with_case_changes(file_change_scanner, test_config, entity_repository):
|
||||
# """Test status detection with case-sensitive path changes."""
|
||||
# docs_dir = test_config.documents_dir
|
||||
# docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
#
|
||||
# # Create file with initial case
|
||||
# content = "test content"
|
||||
# original_path = "Test.md"
|
||||
# orig_file = docs_dir / original_path
|
||||
# orig_file.write_text(content)
|
||||
# checksum = await compute_checksum(content)
|
||||
#
|
||||
# # Add to DB
|
||||
# await entity_repository.create(
|
||||
# {"permalink": original_path, "file_path": original_path, "checksum": checksum}
|
||||
# )
|
||||
#
|
||||
# # Simulate case change in filesystem
|
||||
# orig_file.rename(docs_dir / "test.md")
|
||||
#
|
||||
# # Check changes
|
||||
# changes = await file_change_scanner.find_knowledge_changes(docs_dir)
|
||||
# assert len(changes.moved) == 1
|
||||
# assert "test.md" in changes.moved
|
||||
# assert changes.moved["test.md"].moved_from == original_path
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_status_with_spaces(file_change_scanner, test_config, entity_repository):
|
||||
# """Test status handling files with spaces and special characters."""
|
||||
# docs_dir = test_config.documents_dir
|
||||
# docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
#
|
||||
# # Create file with spaces
|
||||
# content = "test content"
|
||||
# path = "My Document.md"
|
||||
# file_path = docs_dir / path
|
||||
# file_path.write_text(content)
|
||||
# checksum = await compute_checksum(content)
|
||||
#
|
||||
# # Add to DB with same path
|
||||
# await entity_repository.create({"permalink": path, "file_path": path, "checksum": checksum})
|
||||
#
|
||||
# # Check changes
|
||||
# changes = await file_change_scanner.find_knowledge_changes(docs_dir)
|
||||
# assert not changes.modified # File unchanged
|
||||
# assert not changes.moved # Path matches exactly
|
||||
# assert not changes.deleted
|
||||
# assert not changes.new
|
||||
+3
-3
@@ -11,7 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine, async_sessionmaker
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.db import DatabaseType
|
||||
from basic_memory.db import DatabaseType, init_db
|
||||
from basic_memory.markdown import EntityParser
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.models import Base
|
||||
@@ -21,7 +21,8 @@ from basic_memory.repository.observation_repository import ObservationRepository
|
||||
from basic_memory.repository.relation_repository import RelationRepository
|
||||
from basic_memory.repository.search_repository import SearchRepository
|
||||
from basic_memory.services import (
|
||||
EntityService,
|
||||
EntityService,
|
||||
DatabaseService,
|
||||
)
|
||||
from basic_memory.services.file_service import FileService
|
||||
from basic_memory.services.link_resolver import LinkResolver
|
||||
@@ -392,4 +393,3 @@ def watch_service(sync_service, file_service, test_config):
|
||||
file_service=file_service,
|
||||
config=test_config
|
||||
)
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ async def test_parse_complete_file(test_config, entity_parser, valid_entity_cont
|
||||
assert len(entity.observations) == 3
|
||||
obs = entity.observations[0]
|
||||
assert obs.category == "design"
|
||||
assert obs.content == "Stateless authentication"
|
||||
assert obs.content == "Stateless authentication #security #architecture"
|
||||
assert set(obs.tags or []) == {"security", "architecture"}
|
||||
assert obs.context == "JWT based"
|
||||
|
||||
@@ -174,7 +174,7 @@ async def test_parse_file_without_section_headers(test_config, entity_parser):
|
||||
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0].category == "note"
|
||||
assert entity.observations[0].content == "Basic observation"
|
||||
assert entity.observations[0].content == "Basic observation #test"
|
||||
assert entity.observations[0].tags == ["test"]
|
||||
|
||||
assert len(entity.relations) == 2
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_observation_plugin():
|
||||
obs = obs_token.meta['observation']
|
||||
|
||||
assert obs['category'] == 'design'
|
||||
assert obs['content'] == 'Core feature'
|
||||
assert obs['content'] == 'Core feature #important #mvp'
|
||||
assert set(obs['tags']) == {'important', 'mvp'}
|
||||
assert obs['context'] is None
|
||||
|
||||
@@ -26,7 +26,7 @@ def test_observation_plugin():
|
||||
obs = obs_token.meta['observation']
|
||||
|
||||
assert obs['category'] == 'feature'
|
||||
assert obs['content'] == 'Authentication system'
|
||||
assert obs['content'] == 'Authentication system #security'
|
||||
assert set(obs['tags']) == {'security'}
|
||||
assert obs['context'] == 'Required for MVP'
|
||||
|
||||
@@ -36,7 +36,7 @@ def test_observation_plugin():
|
||||
obs = obs_token.meta['observation']
|
||||
|
||||
assert obs['category'] is None
|
||||
assert obs['content'] == 'Authentication system'
|
||||
assert obs['content'] == 'Authentication system #security'
|
||||
assert set(obs['tags']) == {'security'}
|
||||
assert obs['context'] == 'Required for MVP'
|
||||
|
||||
@@ -61,7 +61,7 @@ def test_observation_edge_cases():
|
||||
tokens = md.parse("- [code] Function (x) returns y #function")
|
||||
obs_token = next(t for t in tokens if t.meta and 'observation' in t.meta)
|
||||
obs = obs_token.meta['observation']
|
||||
assert obs['content'] == 'Function (x) returns y'
|
||||
assert obs['content'] == 'Function (x) returns y #function'
|
||||
assert obs['context'] is None
|
||||
|
||||
# Multiple hashtags together
|
||||
|
||||
@@ -71,7 +71,7 @@ async def test_write_new_file_with_content(markdown_processor: MarkdownProcessor
|
||||
content="# Custom Title\n\nMy content here.\nMultiple lines.",
|
||||
observations=[
|
||||
Observation(
|
||||
content="Test observation",
|
||||
content="Test observation #test",
|
||||
category="tech",
|
||||
tags=["test"],
|
||||
context="test context",
|
||||
|
||||
@@ -50,13 +50,13 @@ def test_complex_format():
|
||||
obs = parse_observation(token)
|
||||
assert obs["category"] == "complex test"
|
||||
assert set(obs["tags"]) == {"tag1", "tag2", "tag3"}
|
||||
assert obs["content"] == "This is with content"
|
||||
assert obs["content"] == "This is #tag1#tag2 with #tag3 content"
|
||||
|
||||
# Pydantic model validation
|
||||
observation = Observation.model_validate(obs)
|
||||
assert observation.category == "complex test"
|
||||
assert set(observation.tags) == {"tag1", "tag2", "tag3"}
|
||||
assert observation.content == "This is with content"
|
||||
assert observation.content == "This is #tag1#tag2 with #tag3 content"
|
||||
|
||||
|
||||
def test_malformed_category():
|
||||
@@ -91,20 +91,6 @@ def test_no_category():
|
||||
assert observation.content == "No category"
|
||||
|
||||
|
||||
def test_whitespace_handling():
|
||||
"""Test handling of various whitespace in content."""
|
||||
md = MarkdownIt().use(observation_plugin)
|
||||
|
||||
# Various whitespace in content
|
||||
test_chars = {" ": "space", "\t": "tab", "\r": "return"}
|
||||
for char, name in test_chars.items():
|
||||
content = f"- [test] Content{char}with{char}{name}"
|
||||
tokens = md.parse(content)
|
||||
token = next(t for t in tokens if t.type == "inline")
|
||||
obs = parse_observation(token)
|
||||
observation = Observation.model_validate(obs)
|
||||
assert observation.content == f"Content with {name}"
|
||||
|
||||
|
||||
def test_unicode_content():
|
||||
"""Test handling of Unicode content."""
|
||||
|
||||
@@ -44,7 +44,7 @@ async def test_unicode_content(tmp_path):
|
||||
assert "🧪" in entity.content
|
||||
|
||||
# Verify Unicode in observations
|
||||
assert any(o.content == "Emoji test 👍" for o in entity.observations)
|
||||
assert any(o.content == "Emoji test 👍 #emoji #test" for o in entity.observations)
|
||||
assert any(o.category == "中文" for o in entity.observations)
|
||||
assert any(o.category == "русский" for o in entity.observations)
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Tests for DatabaseService."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import AsyncGenerator
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy import Column, String, Table, text
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, AsyncSession
|
||||
|
||||
from basic_memory import db
|
||||
from basic_memory.config import ProjectConfig
|
||||
from basic_memory.db import DatabaseType
|
||||
from basic_memory.models import Base
|
||||
from basic_memory.services.database_service import DatabaseService
|
||||
from basic_memory.sync import SyncService
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(scope="function")
|
||||
async def engine_factory(
|
||||
test_config,
|
||||
) -> AsyncGenerator[tuple[AsyncEngine, async_sessionmaker[AsyncSession]], None]:
|
||||
"""Special version of the engine factory fixture that uses a FILESYSTEM db_type"""
|
||||
async with db.engine_session_factory(
|
||||
db_path=test_config.database_path, db_type=DatabaseType.FILESYSTEM
|
||||
) as (engine, session_maker):
|
||||
# Initialize database
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
await session.execute(text("PRAGMA foreign_keys=ON"))
|
||||
conn = await session.connection()
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
yield engine, session_maker
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def database_service(
|
||||
test_config: ProjectConfig,
|
||||
sync_service: SyncService,
|
||||
) -> DatabaseService:
|
||||
"""Create DatabaseManagementService instance for testing."""
|
||||
return DatabaseService(
|
||||
config=test_config,
|
||||
db_type = DatabaseType.FILESYSTEM
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_db_initializes_new_db(
|
||||
database_service: DatabaseService,
|
||||
):
|
||||
"""Test that check_db initializes new database."""
|
||||
# Ensure DB doesn't exist
|
||||
if Path(database_service.db_path).exists():
|
||||
Path(database_service.db_path).unlink()
|
||||
|
||||
# Check DB - should initialize
|
||||
assert await database_service.check_db()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_db_rebuilds_on_schema_mismatch(
|
||||
database_service: DatabaseService,
|
||||
session_maker,
|
||||
):
|
||||
"""Test that check_db rebuilds DB when schema doesn't match."""
|
||||
# Initialize DB first
|
||||
assert await database_service.check_db()
|
||||
|
||||
# Alter an existing table to remove a column
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
conn = await session.connection()
|
||||
# Create temp table
|
||||
await conn.execute(text("""
|
||||
CREATE TABLE entity_temp (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title TEXT,
|
||||
entity_type TEXT,
|
||||
content_type TEXT,
|
||||
permalink TEXT,
|
||||
file_path TEXT,
|
||||
checksum TEXT,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
-- Deliberately omit entity_metadata column
|
||||
)
|
||||
"""))
|
||||
# Drop original table
|
||||
await conn.execute(text("DROP TABLE entity"))
|
||||
# Rename temp table
|
||||
await conn.execute(text("ALTER TABLE entity_temp RENAME TO entity"))
|
||||
await session.commit()
|
||||
|
||||
# Check DB - should detect missing column and rebuild
|
||||
assert await database_service.check_db()
|
||||
|
||||
# Verify entity_metadata column exists now
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
result = await session.execute(text("""
|
||||
SELECT sql FROM sqlite_master
|
||||
WHERE type='table' AND name='entity'
|
||||
"""))
|
||||
create_sql = result.scalar()
|
||||
assert 'entity_metadata' in create_sql.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_backup_creates_timestamped_file(
|
||||
database_service: DatabaseService,
|
||||
):
|
||||
"""Test that backup creates properly named backup file."""
|
||||
if database_service.db_type == db.DatabaseType.MEMORY:
|
||||
return
|
||||
|
||||
# Create dummy DB file
|
||||
database_service.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
database_service.db_path.write_text("test content")
|
||||
|
||||
# Create backup
|
||||
backup_path = await database_service.create_backup()
|
||||
|
||||
assert backup_path is not None
|
||||
assert backup_path.exists()
|
||||
assert backup_path.suffix == ".backup"
|
||||
assert datetime.now().strftime("%Y%m%d") in backup_path.name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cleanup_backups_keeps_recent(
|
||||
database_service: DatabaseService,
|
||||
):
|
||||
"""Test that cleanup_backups keeps N most recent backups."""
|
||||
if database_service.db_type == db.DatabaseType.MEMORY:
|
||||
return
|
||||
|
||||
# Create backup directory
|
||||
backup_dir = database_service.db_path.parent
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create some test backup files with different timestamps
|
||||
backup_times = [
|
||||
datetime.now() - timedelta(days=i)
|
||||
for i in range(7) # Create 7 backups
|
||||
]
|
||||
|
||||
for dt in backup_times:
|
||||
timestamp = dt.strftime("%Y%m%d_%H%M%S")
|
||||
backup_path = database_service.db_path.with_suffix(f".{timestamp}.backup")
|
||||
backup_path.write_text("test backup")
|
||||
# Set mtime to match our timestamp
|
||||
backup_path.touch()
|
||||
ts = dt.timestamp()
|
||||
|
||||
# Clean up keeping 5 most recent
|
||||
await database_service.cleanup_backups(keep_count=5)
|
||||
|
||||
# Check that we have exactly 5 backups left
|
||||
backup_pattern = "*.backup"
|
||||
remaining = list(backup_dir.glob(backup_pattern))
|
||||
assert len(remaining) == 5
|
||||
@@ -6,11 +6,12 @@ import pytest
|
||||
import yaml
|
||||
|
||||
from basic_memory.models import Entity as EntityModel
|
||||
from basic_memory.repository import EntityRepository
|
||||
from basic_memory.schemas import Entity as EntitySchema
|
||||
from basic_memory.services import FileService
|
||||
from basic_memory.services.entity_service import EntityService
|
||||
from basic_memory.services.exceptions import EntityNotFoundError
|
||||
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -51,7 +52,36 @@ async def test_create_entity(entity_service: EntityService, file_service: FileSe
|
||||
assert metadata["permalink"] == entity.permalink
|
||||
assert metadata["type"] == entity.entity_type
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_entity_unique_permalink(test_config, entity_service: EntityService, file_service: FileService, entity_repository: EntityRepository):
|
||||
"""Test successful entity creation."""
|
||||
entity_data = EntitySchema(
|
||||
title="Test Entity",
|
||||
folder="test",
|
||||
entity_type="test",
|
||||
)
|
||||
|
||||
entity = await entity_service.create_entity(entity_data)
|
||||
|
||||
# default permalink
|
||||
assert entity.permalink == generate_permalink(entity.file_path)
|
||||
|
||||
# move file
|
||||
file_path = file_service.get_entity_path(entity)
|
||||
file_path.rename(test_config.home / "new_path.md")
|
||||
await entity_repository.update(entity.id, {"file_path": "new_path.md"})
|
||||
|
||||
# create again
|
||||
entity2 = await entity_service.create_entity(entity_data)
|
||||
assert entity2.permalink == f"{entity.permalink}-1"
|
||||
|
||||
file_path = file_service.get_entity_path(entity2)
|
||||
file_content, _ = await file_service.read_file(file_path)
|
||||
_, frontmatter, doc_content = file_content.split("---", 2)
|
||||
metadata = yaml.safe_load(frontmatter)
|
||||
|
||||
# Verify frontmatter contents
|
||||
assert metadata["permalink"] == entity2.permalink
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_permalink(entity_service: EntityService):
|
||||
@@ -326,7 +356,7 @@ See the [[Git Cheat Sheet]] for reference.
|
||||
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0].category == "design"
|
||||
assert entity.observations[0].content == "Keep feature branches short-lived"
|
||||
assert entity.observations[0].content == "Keep feature branches short-lived #git #workflow"
|
||||
assert set(entity.observations[0].tags) == {"git", "workflow"}
|
||||
assert entity.observations[0].context == "Reduces merge conflicts"
|
||||
|
||||
@@ -416,7 +446,7 @@ See the [[Git Cheat Sheet]] for reference.
|
||||
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0].category == "design"
|
||||
assert entity.observations[0].content == "Keep feature branches short-lived"
|
||||
assert entity.observations[0].content == "Keep feature branches short-lived #git #workflow"
|
||||
assert set(entity.observations[0].tags) == {"git", "workflow"}
|
||||
assert entity.observations[0].context == "Reduces merge conflicts"
|
||||
|
||||
@@ -440,4 +470,3 @@ See the [[Git Cheat Sheet]] for reference.
|
||||
|
||||
|
||||
|
||||
# TODO handle permalink conflicts
|
||||
|
||||
@@ -10,7 +10,7 @@ from basic_memory.config import ProjectConfig
|
||||
from basic_memory.models import Entity
|
||||
from basic_memory.repository import EntityRepository
|
||||
from basic_memory.schemas.search import SearchQuery
|
||||
from basic_memory.services import EntityService
|
||||
from basic_memory.services import EntityService, FileService
|
||||
from basic_memory.services.search_service import SearchService
|
||||
from basic_memory.sync.sync_service import SyncService
|
||||
|
||||
@@ -557,9 +557,6 @@ async def test_handle_entity_deletion(
|
||||
rel_results = await search_service.search(SearchQuery(text="connects_to"))
|
||||
assert len(rel_results) == 0
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_preserves_timestamps(
|
||||
sync_service: SyncService,
|
||||
@@ -681,3 +678,169 @@ modified: 2024-01-01
|
||||
# Verify entity was properly synced
|
||||
updated = await entity_service.get_by_permalink("concept/incomplete")
|
||||
assert updated.checksum is not None
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_permalink_resolved(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
file_service: FileService,
|
||||
):
|
||||
"""Test that we resolve duplicate permalinks on sync ."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# Create initial file
|
||||
content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Test Move
|
||||
Content for move test
|
||||
"""
|
||||
old_path = project_dir / "old" / "test_move.md"
|
||||
old_path.parent.mkdir(parents=True)
|
||||
await create_test_file(old_path, content)
|
||||
|
||||
# Initial sync
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Move the file
|
||||
new_path = project_dir / "new" / "moved_file.md"
|
||||
new_path.parent.mkdir(parents=True)
|
||||
old_path.rename(new_path)
|
||||
|
||||
# Sync again
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
file_content, _ = await file_service.read_file(new_path)
|
||||
assert "permalink: old/test-move" in file_content
|
||||
|
||||
# Create another that has the same permalink
|
||||
content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Test Move
|
||||
Content for move test
|
||||
"""
|
||||
old_path = project_dir / "old" / "test_move.md"
|
||||
old_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
await create_test_file(old_path, content)
|
||||
|
||||
# Sync new file
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# assert permalink is unique
|
||||
file_content, _ = await file_service.read_file(old_path)
|
||||
assert "permalink: old/test-move-1" in file_content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_permalink_resolved_on_update(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
file_service: FileService,
|
||||
):
|
||||
"""Test that sync resolves permalink conflicts on update."""
|
||||
project_dir = test_config.home
|
||||
|
||||
one_file = project_dir / "one.md"
|
||||
two_file = project_dir / "two.md"
|
||||
await create_test_file(one_file)
|
||||
await create_test_file(two_file)
|
||||
|
||||
# Run sync
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Check permalinks
|
||||
file_one_content, _ = await file_service.read_file(one_file)
|
||||
assert "permalink: one" in file_one_content
|
||||
|
||||
file_two_content, _ = await file_service.read_file(two_file)
|
||||
assert "permalink: two" in file_two_content
|
||||
|
||||
# update the second file with a duplicate permalink
|
||||
updated_content = """
|
||||
---
|
||||
title: two.md
|
||||
type: note
|
||||
permalink: one
|
||||
tags: []
|
||||
---
|
||||
|
||||
test content
|
||||
"""
|
||||
two_file.write_text(updated_content)
|
||||
|
||||
# Run sync
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Check permalinks
|
||||
file_two_content, _ = await file_service.read_file(two_file)
|
||||
assert "permalink: two" in file_two_content
|
||||
|
||||
# new content with duplicate permalink
|
||||
new_content = """
|
||||
---
|
||||
title: new.md
|
||||
type: note
|
||||
permalink: one
|
||||
tags: []
|
||||
---
|
||||
|
||||
test content
|
||||
"""
|
||||
new_file = project_dir / "new.md"
|
||||
await create_test_file(new_file)
|
||||
|
||||
# Run another time
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Should still have same permalink
|
||||
new_file_content, _ = await file_service.read_file(new_file)
|
||||
assert "permalink: new" in new_file_content
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_duplicate_observations(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
file_service: FileService,
|
||||
):
|
||||
"""Test that sync resolves permalink conflicts on update."""
|
||||
project_dir = test_config.home
|
||||
|
||||
content = """
|
||||
---
|
||||
title: a note
|
||||
type: note
|
||||
tags: []
|
||||
---
|
||||
|
||||
test content
|
||||
|
||||
- [note] one observation
|
||||
"""
|
||||
|
||||
note_file = project_dir / "note.md"
|
||||
await create_test_file(note_file, content)
|
||||
|
||||
# Run sync
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Check permalinks
|
||||
file_one_content, _ = await file_service.read_file(note_file)
|
||||
assert """---
|
||||
title: a note
|
||||
type: note
|
||||
tags: []
|
||||
permalink: note
|
||||
---
|
||||
|
||||
test content
|
||||
|
||||
- [note] one observation
|
||||
""".strip() == file_one_content
|
||||
|
||||
@@ -74,7 +74,7 @@ async def test_handle_file_moved(test_config, watch_service, sync_service, sampl
|
||||
assert len(watch_service.state.recent_events) == 2
|
||||
assert watch_service.state.synced_files == 1
|
||||
event = watch_service.state.recent_events[0]
|
||||
assert event.path == "test.md"
|
||||
assert event.path == "test.md -> moved.md"
|
||||
assert event.action == "moved"
|
||||
assert event.status == "success"
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ from basic_memory import __version__
|
||||
|
||||
|
||||
def test_version():
|
||||
"""Test version is set correctly"""
|
||||
assert __version__ == "0.1.0"
|
||||
"""Test version is set"""
|
||||
assert __version__ is not None
|
||||
@@ -8,14 +8,12 @@ from basic_memory.file_utils import (
|
||||
compute_checksum,
|
||||
ensure_directory,
|
||||
write_file_atomic,
|
||||
add_frontmatter,
|
||||
parse_frontmatter,
|
||||
has_frontmatter,
|
||||
remove_frontmatter,
|
||||
parse_content_with_frontmatter,
|
||||
FileError,
|
||||
FileWriteError,
|
||||
ParseError,
|
||||
ParseError, update_frontmatter,
|
||||
)
|
||||
|
||||
|
||||
@@ -69,26 +67,6 @@ async def test_write_file_atomic_error(tmp_path: Path):
|
||||
await write_file_atomic(test_file, "test content")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_frontmatter():
|
||||
"""Test adding frontmatter."""
|
||||
content = "test content"
|
||||
metadata = {"title": "Test", "tags": ["a", "b"]}
|
||||
|
||||
result = await add_frontmatter(content, metadata)
|
||||
|
||||
# Should have frontmatter delimiters
|
||||
assert result.startswith("---\n")
|
||||
assert "---\n\n" in result
|
||||
|
||||
# Should include metadata
|
||||
assert "title: Test" in result
|
||||
assert "- a\n- b" in result or "['a', 'b']" in result
|
||||
|
||||
# Should preserve content
|
||||
assert result.endswith("test content")
|
||||
|
||||
|
||||
def test_has_frontmatter():
|
||||
"""Test frontmatter detection."""
|
||||
# Valid frontmatter
|
||||
@@ -186,69 +164,72 @@ content""")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_parse_content_with_frontmatter():
|
||||
"""Test combined frontmatter and content parsing."""
|
||||
# Full document
|
||||
content = """---
|
||||
title: Test
|
||||
tags:
|
||||
- a
|
||||
- b
|
||||
---
|
||||
test content"""
|
||||
async def test_update_frontmatter(tmp_path: Path):
|
||||
"""Test updating frontmatter in a file."""
|
||||
test_file = tmp_path / "test.md"
|
||||
|
||||
frontmatter, body = await parse_content_with_frontmatter(content)
|
||||
assert frontmatter == {"title": "Test", "tags": ["a", "b"]}
|
||||
assert body == "test content"
|
||||
# Test 1: Add frontmatter to file without any
|
||||
content = "# Test Content\n\nSome content here"
|
||||
test_file.write_text(content)
|
||||
|
||||
# No frontmatter
|
||||
content = "test content"
|
||||
frontmatter, body = await parse_content_with_frontmatter(content)
|
||||
assert frontmatter == {}
|
||||
assert body == "test content"
|
||||
updates = {"title": "Test", "type": "note"}
|
||||
checksum = await update_frontmatter(test_file, updates)
|
||||
|
||||
# Empty document
|
||||
frontmatter, body = await parse_content_with_frontmatter("")
|
||||
assert frontmatter == {}
|
||||
assert body == ""
|
||||
# Verify content
|
||||
updated = test_file.read_text()
|
||||
assert "title: Test" in updated
|
||||
assert "type: note" in updated
|
||||
assert "Test Content" in updated
|
||||
assert "Some content here" in updated
|
||||
|
||||
# Only frontmatter
|
||||
content = """---
|
||||
title: Test
|
||||
---
|
||||
"""
|
||||
frontmatter, body = await parse_content_with_frontmatter(content)
|
||||
assert frontmatter == {"title": "Test"}
|
||||
assert body == ""
|
||||
# Verify structure
|
||||
fm = parse_frontmatter(updated)
|
||||
assert fm == updates
|
||||
assert remove_frontmatter(updated).strip() == content
|
||||
|
||||
# Test 2: Update existing frontmatter
|
||||
updates = {"type": "doc", "tags": ["test"]}
|
||||
new_checksum = await update_frontmatter(test_file, updates)
|
||||
|
||||
# Verify checksum changed
|
||||
assert new_checksum != checksum
|
||||
|
||||
# Verify content
|
||||
updated = test_file.read_text()
|
||||
fm = parse_frontmatter(updated)
|
||||
assert fm == {"title": "Test", "type": "doc", "tags": ["test"]}
|
||||
assert "Test Content" in updated
|
||||
|
||||
# Test 3: Update with empty dict shouldn't change anything
|
||||
checksum_before = await compute_checksum(test_file.read_text())
|
||||
new_checksum = await update_frontmatter(test_file, {})
|
||||
assert new_checksum == checksum_before
|
||||
|
||||
# Test 4: Handle multi-line content properly
|
||||
content = """# Heading
|
||||
|
||||
Some content
|
||||
|
||||
## Section
|
||||
- Point 1
|
||||
- Point 2
|
||||
|
||||
### Subsection
|
||||
More content here"""
|
||||
|
||||
test_file.write_text(content)
|
||||
await update_frontmatter(test_file, {"title": "Test"})
|
||||
|
||||
updated = test_file.read_text()
|
||||
assert remove_frontmatter(updated).strip() == content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_frontmatter_whitespace_handling():
|
||||
"""Test frontmatter handling with various whitespace."""
|
||||
# Extra newlines before frontmatter
|
||||
content = """
|
||||
async def test_update_frontmatter_errors(tmp_path: Path):
|
||||
"""Test error handling in update_frontmatter."""
|
||||
test_file = tmp_path / "test.md"
|
||||
|
||||
---
|
||||
title: Test
|
||||
---
|
||||
content"""
|
||||
assert has_frontmatter(content.strip())
|
||||
frontmatter = parse_frontmatter(content.strip())
|
||||
assert frontmatter == {"title": "Test"}
|
||||
|
||||
# Extra newlines after frontmatter
|
||||
content = """---
|
||||
title: Test
|
||||
---
|
||||
|
||||
|
||||
content"""
|
||||
result = await add_frontmatter("content", {"title": "Test"})
|
||||
assert result.count("\n\n") == 1 # Should normalize to single blank line
|
||||
|
||||
# Spaces around content
|
||||
content = """---
|
||||
title: Test
|
||||
---
|
||||
content """
|
||||
assert remove_frontmatter(content).strip() == "content"
|
||||
# Test 1: Invalid file path
|
||||
nonexistent = tmp_path / "nonexistent" / "test.md"
|
||||
with pytest.raises(FileError):
|
||||
await update_frontmatter(nonexistent, {"title": "Test"})
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
version = 1
|
||||
requires-python = ">=3.12.1"
|
||||
|
||||
[[package]]
|
||||
name = "aiosmtpd"
|
||||
version = "1.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "atpublic" },
|
||||
{ name = "attrs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/ca/b2b7cc880403ef24be77383edaadfcf0098f5d7b9ddbf3e2c17ef0a6af0d/aiosmtpd-1.4.6.tar.gz", hash = "sha256:5a811826e1a5a06c25ebc3e6c4a704613eb9a1bcf6b78428fbe865f4f6c9a4b8", size = 152775 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/39/d401756df60a8344848477d54fdf4ce0f50531f6149f3b8eaae9c06ae3dc/aiosmtpd-1.4.6-py3-none-any.whl", hash = "sha256:72c99179ba5aa9ae0abbda6994668239b64a5ce054471955fe75f581d2592475", size = 154263 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
version = "0.20.0"
|
||||
@@ -26,6 +13,20 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/c4/c93eb22025a2de6b83263dfe3d7df2e19138e345bca6f18dba7394120930/aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6", size = 15564 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alembic"
|
||||
version = "1.14.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mako" },
|
||||
{ name = "sqlalchemy" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/09/f844822e4e847a3f0bd41797f93c4674cd4d2462a3f6c459aa528cdf786e/alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213", size = 1918219 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/7e/ac0991d1745f7d755fc1cd381b3990a45b404b4d008fc75e2a983516fbfe/alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", size = 233565 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
@@ -58,132 +59,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asyncpg"
|
||||
version = "0.30.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/64/9d3e887bb7b01535fdbc45fbd5f0a8447539833b97ee69ecdbb7a79d0cb4/asyncpg-0.30.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c902a60b52e506d38d7e80e0dd5399f657220f24635fee368117b8b5fce1142e", size = 673162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/eb/8b236663f06984f212a087b3e849731f917ab80f84450e943900e8ca4052/asyncpg-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aca1548e43bbb9f0f627a04666fedaca23db0a31a84136ad1f868cb15deb6e3a", size = 637025 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/57/2dc240bb263d58786cfaa60920779af6e8d32da63ab9ffc09f8312bd7a14/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c2a2ef565400234a633da0eafdce27e843836256d40705d83ab7ec42074efb3", size = 3496243 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/40/0ae9d061d278b10713ea9021ef6b703ec44698fe32178715a501ac696c6b/asyncpg-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1292b84ee06ac8a2ad8e51c7475aa309245874b61333d97411aab835c4a2f737", size = 3575059 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/75/d6b895a35a2c6506952247640178e5f768eeb28b2e20299b6a6f1d743ba0/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f5712350388d0cd0615caec629ad53c81e506b1abaaf8d14c93f54b35e3595a", size = 3473596 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/e7/3693392d3e168ab0aebb2d361431375bd22ffc7b4a586a0fc060d519fae7/asyncpg-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db9891e2d76e6f425746c5d2da01921e9a16b5a71a1c905b13f30e12a257c4af", size = 3641632 },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/ea/15670cea95745bba3f0352341db55f506a820b21c619ee66b7d12ea7867d/asyncpg-0.30.0-cp312-cp312-win32.whl", hash = "sha256:68d71a1be3d83d0570049cd1654a9bdfe506e794ecc98ad0873304a9f35e411e", size = 560186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/6b/fe1fad5cee79ca5f5c27aed7bd95baee529c1bf8a387435c8ba4fe53d5c1/asyncpg-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:9a0292c6af5c500523949155ec17b7fe01a00ace33b68a476d6b5059f9630305", size = 621064 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471 },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atpublic"
|
||||
version = "5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5d/18/b1d247792440378abeeb0853f9daa2a127284b68776af6815990be7fcdb0/atpublic-5.0.tar.gz", hash = "sha256:d5cb6cbabf00ec1d34e282e8ce7cbc9b74ba4cb732e766c24e2d78d1ad7f723f", size = 14646 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/03/2cb0e5326e19b7d877bc9c3a7ef436a30a06835b638580d1f5e21a0409ed/atpublic-5.0-py3-none-any.whl", hash = "sha256:b651dcd886666b1042d1e38158a22a4f2c267748f4e97fde94bc492a4a28a3f3", size = 5207 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "24.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "authlib"
|
||||
version = "1.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/7d/67f064784776afc59d6deda9f4b8e117843d7cda8ddf34bbdbbae39641ca/authlib-1.4.0.tar.gz", hash = "sha256:1c1e6608b5ed3624aeeee136ca7f8c120d6f51f731aa152b153d54741840e1f2", size = 147327 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/c6/6a14bb00f963597d675fa63f060eb6db3fdb54bbf01d94768f51d067f578/Authlib-1.4.0-py2.py3-none-any.whl", hash = "sha256:4bb20b978c8b636222b549317c1815e1fe62234fc1c5efe8855d84aebf3a74e3", size = 225531 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basic-foundation"
|
||||
version = "0.1.9"
|
||||
source = { editable = "../basic-foundation" }
|
||||
dependencies = [
|
||||
{ name = "aiosmtpd" },
|
||||
{ name = "asyncpg" },
|
||||
{ name = "copier" },
|
||||
{ name = "emails" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "fastapi-jwt", extra = ["authlib"] },
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "jinjax" },
|
||||
{ name = "loguru" },
|
||||
{ name = "passlib", extra = ["bcrypt"] },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "psycopg2" },
|
||||
{ name = "pydantic-settings" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "sqlalchemy", extra = ["asyncio"] },
|
||||
{ name = "starlette-wtf" },
|
||||
{ name = "typer" },
|
||||
{ name = "uvicorn" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiosmtpd", specifier = ">=1.4.6" },
|
||||
{ name = "asgi-lifespan", marker = "extra == 'dev'", specifier = ">=2.1.0" },
|
||||
{ name = "asyncpg", specifier = ">=0.29.0" },
|
||||
{ name = "bump2version", marker = "extra == 'dev'", specifier = ">=1.0.1" },
|
||||
{ name = "copier", specifier = ">=9.3.1" },
|
||||
{ name = "coverage", marker = "extra == 'dev'", specifier = ">=7.4.0" },
|
||||
{ name = "emails", specifier = ">=0.6" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.112.0" },
|
||||
{ name = "fastapi-jwt", extras = ["authlib"], specifier = ">=0.3.0" },
|
||||
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.26.0" },
|
||||
{ name = "httpx-oauth", marker = "extra == 'dev'", specifier = ">=0.13.1" },
|
||||
{ name = "itsdangerous", specifier = ">=2.2.0" },
|
||||
{ name = "jinja2", specifier = ">=3.1.2" },
|
||||
{ name = "jinjax", specifier = ">=0.46" },
|
||||
{ name = "loguru", specifier = ">=0.7.2" },
|
||||
{ name = "mockito", marker = "extra == 'dev'", specifier = ">=1.5.0" },
|
||||
{ name = "passlib", extras = ["bcrypt"], specifier = ">=0.7.2" },
|
||||
{ name = "playwright", marker = "extra == 'dev'", specifier = ">=1.41.2" },
|
||||
{ name = "pre-commit", specifier = ">=3.8.0" },
|
||||
{ name = "psycopg2", specifier = ">=2.9.9" },
|
||||
{ name = "pydantic-settings", specifier = ">=2.1.0" },
|
||||
{ name = "pyjwt", specifier = ">=2.9.0" },
|
||||
{ name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.381" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.4" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.3" },
|
||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" },
|
||||
{ name = "pytest-github-actions-annotate-failures", marker = "extra == 'github-actions'", specifier = ">=0.1.7" },
|
||||
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" },
|
||||
{ name = "pytest-playwright", marker = "extra == 'dev'", specifier = ">=0.4.4" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.2.1" },
|
||||
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.25" },
|
||||
{ name = "starlette-wtf", specifier = ">=0.4.5" },
|
||||
{ name = "typer", specifier = ">=0.12.3" },
|
||||
{ name = "uvicorn", specifier = ">=0.25.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "basic-memory"
|
||||
version = "0.1.0"
|
||||
version = "0.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiosqlite" },
|
||||
{ name = "basic-foundation" },
|
||||
{ name = "alembic" },
|
||||
{ name = "dateparser" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "greenlet" },
|
||||
{ name = "icecream" },
|
||||
{ name = "loguru" },
|
||||
@@ -218,8 +102,9 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiosqlite", specifier = ">=0.20.0" },
|
||||
{ name = "basic-foundation", editable = "../basic-foundation" },
|
||||
{ name = "alembic", specifier = ">=1.14.1" },
|
||||
{ name = "dateparser", specifier = ">=1.2.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.8" },
|
||||
{ name = "greenlet", specifier = ">=3.1.1" },
|
||||
{ name = "icecream", specifier = ">=2.1.3" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
@@ -245,45 +130,6 @@ requires-dist = [
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "icecream", specifier = ">=2.1.3" }]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt"
|
||||
version = "4.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/56/8c/dd696962612e4cd83c40a9e6b3db77bfe65a830f4b9af44098708584686c/bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe", size = 24427 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/ca/e17b08c523adb93d5f07a226b2bd45a7c6e96b359e31c1e99f9db58cb8c3/bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17", size = 489982 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/be/e7c6e0fd6087ee8fc6d77d8d9e817e9339d879737509019b9a9012a1d96f/bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f", size = 273108 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/53/ac084b7d985aee1a5f2b086d501f550862596dbf73220663b8c17427e7f2/bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad", size = 278733 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/ab/b8710a3d6231c587e575ead0b1c45bb99f5454f9f579c9d7312c17b069cc/bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea", size = 273856 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/e5/2fd1ea6395358ffdfd4afe370d5b52f71408f618f781772a48971ef3b92b/bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396", size = 279067 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/ef/f2cb7a0f7e1ed800a604f8ab256fb0afcf03c1540ad94ff771ce31e794aa/bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425", size = 306851 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/cb/578b0023c6a5ca16a177b9044ba6bd6032277bd3ef020fb863eccd22e49b/bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685", size = 310793 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/bc/9d501ee9d754f63d4b1086b64756c284facc3696de9b556c146279a124a5/bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6", size = 320957 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/25/2ec4ce5740abc43182bfc064b9acbbf5a493991246985e8b2bfe231ead64/bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139", size = 339958 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/64/fd67788f64817727897d31e9cdeeeba3941eaad8540733c05c7eac4aa998/bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005", size = 160912 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/8f/fe834eaa54abbd7cab8607e5020fa3a0557e929555b9e4ca404b4adaab06/bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526", size = 152981 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/57/23b46933206daf5384b5397d9878746d2249fe9d45efaa8e1467c87d3048/bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413", size = 489842 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/28/3ea8a39ddd4938b6c6b6136816d72ba5e659e2d82b53d843c8c53455ac4d/bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a", size = 272500 },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/7f/b43622999f5d4de06237a195ac5501ac83516adf571b907228cd14bac8fe/bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c", size = 278368 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/68/f2e3959014b4d8874c747e6e171d46d3e63a3a39aaca8417a8d837eda0a8/bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99", size = 273335 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/c3/4b4bad4da852924427c651589d464ad1aa624f94dd904ddda8493b0a35e5/bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54", size = 278614 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/5a/ee107961e84c41af2ac201d0460f962b6622ff391255ffd46429e9e09dc1/bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837", size = 306464 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/72/916e14fa12d2b1d1fc6c26ea195337419da6dd23d0bf53ac61ef3739e5c5/bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331", size = 310674 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/92/3dc76d8bfa23300591eec248e950f85bd78eb608c96bd4747ce4cc06acdb/bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84", size = 320577 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/ab/a6c0da5c2cf86600f74402a72b06dfe365e1a1d30783b1bbeec460fd57d1/bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d", size = 339836 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/b4/e75b6e9a72a030a04362034022ebe317c5b735d04db6ad79237101ae4a5c/bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf", size = 160911 },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/b9/d51d34e6cd6d887adddb28a8680a1d34235cc45b9d6e238ce39b98199ca0/bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c", size = 153078 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "5.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/38/a0f315319737ecf45b4319a8cd1f3a908e29d9277b46942263292115eee7/cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a", size = 27661 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/07/14f8ad37f2d12a5ce41206c21820d8cb6561b728e51fad4530dff0552a67/cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", size = 9524 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.12.14"
|
||||
@@ -293,92 +139,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "5.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
@@ -400,29 +160,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copier"
|
||||
version = "9.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama" },
|
||||
{ name = "dunamai" },
|
||||
{ name = "funcy" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "jinja2-ansible-filters" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "plumbum" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pygments" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "questionary" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/84/b99005e18cb07986a9fa1c1314c9bb461851dc115ab24d3d9ac668daad7f/copier-9.4.1.tar.gz", hash = "sha256:cc81d8204cb17fbc8c4a14996a8ce764166c34c77236de38cfbeb960c887b68f", size = 41510 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/5e8edaef8be1bd0a17943e6a011d4fc311f340e45733ef910a6f0b688587/copier-9.4.1-py3-none-any.whl", hash = "sha256:863385b7ba8a9090c832cd12ca072dba9153397dbe7c5f337bf8c3d8859efa32", size = 43188 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.6.10"
|
||||
@@ -461,58 +198,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "44.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/4c/45dfa6829acffa344e3967d6006ee4ae8be57af746ae2eba1c431949b32c/cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02", size = 710657 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/09/8cc67f9b84730ad330b3b72cf867150744bf07ff113cda21a15a1c6d2c7c/cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123", size = 6541833 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/5b/3759e30a103144e29632e7cb72aec28cedc79e514b2ea8896bb17163c19b/cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092", size = 3922710 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/58/3b14bf39f1a0cfd679e753e8647ada56cddbf5acebffe7db90e184c76168/cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f", size = 4137546 },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/65/13d9e76ca19b0ba5603d71ac8424b5694415b348e719db277b5edc985ff5/cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb", size = 3915420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/07/40fe09ce96b91fc9276a9ad272832ead0fddedcba87f1190372af8e3039c/cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b", size = 4154498 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/ea/af65619c800ec0a7e4034207aec543acdf248d9bffba0533342d1bd435e1/cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543", size = 3932569 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/af/d1deb0c04d59612e3d5e54203159e284d3e7a6921e565bb0eeb6269bdd8a/cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e", size = 4016721 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/69/7ca326c55698d0688db867795134bdfac87136b80ef373aaa42b225d6dd5/cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e", size = 4240915 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/d4/cae11bf68c0f981e0413906c6dd03ae7fa864347ed5fac40021df1ef467c/cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053", size = 2757925 },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/b1/50d7739254d2002acae64eed4fc43b24ac0cc44bf0a0d388d1ca06ec5bb1/cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd", size = 3202055 },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/18/61e52a3d28fc1514a43b0ac291177acd1b4de00e9301aaf7ef867076ff8a/cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591", size = 6542801 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/07/5f165b6c65696ef75601b781a280fc3b33f1e0cd6aa5a92d9fb96c410e97/cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7", size = 3922613 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/34/6b3ac1d80fc174812486561cf25194338151780f27e438526f9c64e16869/cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc", size = 4137925 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/c7/c656eb08fd22255d21bc3129625ed9cd5ee305f33752ef2278711b3fa98b/cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289", size = 3915417 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/82/72403624f197af0db6bac4e58153bc9ac0e6020e57234115db9596eee85d/cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7", size = 4155160 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/cd/2f3c440913d4329ade49b146d74f2e9766422e1732613f57097fea61f344/cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c", size = 3932331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/df/8be88797f0a1cca6e255189a57bb49237402b1880d6e8721690c5603ac23/cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64", size = 4017372 },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/36/5ccc376f025a834e72b8e52e18746b927f34e4520487098e283a719c205e/cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285", size = 4239657 },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/b0/f4f7d0d0bcfbc8dd6296c1449be326d04217c57afb8b2594f017eed95533/cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417", size = 2758672 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/9b/443270b9210f13f6ef240eff73fd32e02d381e7103969dc66ce8e89ee901/cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede", size = 3202071 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssselect"
|
||||
version = "1.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/91/d51202cc41fbfca7fa332f43a5adac4b253962588c7cc5a54824b019081c/cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc", size = 41423 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/06/a9/2da08717a6862c48f1d61ef957a7bba171e7eefa6c0aa0ceb96a140c2a6b/cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e", size = 18687 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssutils"
|
||||
version = "2.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "more-itertools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/33/9f/329d26121fe165be44b1dfff21aa0dc348f04633931f1d20ed6cf448a236/cssutils-2.11.1.tar.gz", hash = "sha256:0563a76513b6af6eebbe788c3bf3d01c920e46b3f90c8416738c5cfc773ff8e2", size = 711657 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/ec/bb273b7208c606890dc36540fe667d06ce840a6f62f9fae7e658fcdc90fb/cssutils-2.11.1-py3-none-any.whl", hash = "sha256:a67bfdfdff4f3867fab43698ec4897c1a828eca5973f4073321b3bccaf1199b1", size = 385747 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dateparser"
|
||||
version = "1.2.0"
|
||||
@@ -528,15 +213,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/29/db12aa4dda81580be1999824a689bd52aa40061fc12c9ccdc3feab5ea718/dateparser-1.2.0-py2.py3-none-any.whl", hash = "sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830", size = 294995 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.3.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
version = "2.7.0"
|
||||
@@ -546,18 +222,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dunamai"
|
||||
version = "1.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/4e/a5c8c337a1d9ac0384298ade02d322741fb5998041a5ea74d1cd2a4a1d47/dunamai-1.23.0.tar.gz", hash = "sha256:a163746de7ea5acb6dacdab3a6ad621ebc612ed1e528aaa8beedb8887fccd2c4", size = 44681 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/21/4c/963169386309fec4f96fd61210ac0a0666887d0fb0a50205395674d20b71/dunamai-1.23.0-py3-none-any.whl", hash = "sha256:a0906d876e92441793c6a423e16a4802752e723e9c9a5aabdc5535df02dbe041", size = 26342 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "2.2.0"
|
||||
@@ -571,23 +235,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "emails"
|
||||
version = "0.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "chardet" },
|
||||
{ name = "cssutils" },
|
||||
{ name = "lxml" },
|
||||
{ name = "premailer" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/f9/c1e315aa82ed9f037186c30109200fb4b4c51b5483b8065daa0ca836a336/emails-0.6.tar.gz", hash = "sha256:a4c2d67ea8b8831967a750d8edc6e77040d7693143fe280e6d2a367d9c36ff88", size = 44066 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/7e/b648d640d88d31de49e566832aca9cce025c52d6349b0a0fc65e9df1f4c5/emails-0.6-py2.py3-none-any.whl", hash = "sha256:72c1e3198075709cc35f67e1b49e2da1a2bc087e9b444073db61a379adfb7f3c", size = 56250 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "executing"
|
||||
version = "2.1.0"
|
||||
@@ -599,16 +246,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.115.6"
|
||||
version = "0.115.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/72/d83b98cd106541e8f5e5bfab8ef2974ab45a62e8a6c5b5e6940f26d2ed4b/fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654", size = 301336 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/b2/5a5dc4affdb6661dea100324e19a7721d5dc524b464fe8e366c093fd7d87/fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9", size = 295403 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b3/7e4df40e585df024fac2f80d1a2d579c854ac37109675db2b0cc22c0bb9e/fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305", size = 94843 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/7d/2d6ce181d7a5f51dedb8c06206cbf0ec026a99bf145edd309f9e17c3282f/fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf", size = 94814 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -640,40 +287,6 @@ standard = [
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastapi-jwt"
|
||||
version = "0.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "fastapi" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/4a/60eb509cd08a591967fbdaab7f158c18d12172336432f3634ed4068b4101/fastapi_jwt-0.3.0-py3-none-any.whl", hash = "sha256:b4255ce929f1c59c2bbcf5295b485995c8263466aa733a0dd164b052b4a476a3", size = 9059 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
authlib = [
|
||||
{ name = "authlib" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.16.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "funcy"
|
||||
version = "2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/b8/c6081521ff70afdff55cd9512b2220bbf4fa88804dae51d1b57b4b58ef32/funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb", size = 537931 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "greenlet"
|
||||
version = "3.1.1"
|
||||
@@ -790,15 +403,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/57/1d/43ef7a6875190e6745ffcd1b12c7aaa7efed082897401e311ee1cd75c8b2/icecream-2.1.4-py3-none-any.whl", hash = "sha256:7bb715f69102cae871b3a361c3b656536db02cfcadac9664c673581cac4df4fd", size = 14782 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/92/69934b9ef3c31ca2470980423fda3d00f0460ddefdf30a67adf7f17e2e00/identify-2.6.5.tar.gz", hash = "sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc", size = 99213 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/fa/dce098f4cdf7621aa8f7b4f919ce545891f489482f0bfa5102f3eca8608b/identify-2.6.5-py2.py3-none-any.whl", hash = "sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566", size = 99078 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
@@ -817,15 +421,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2"
|
||||
version = "3.1.5"
|
||||
@@ -838,32 +433,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinja2-ansible-filters"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinja2" },
|
||||
{ name = "pyyaml" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/27/fa186af4b246eb869ffca8ffa42d92b05abaec08c99329e74d88b2c46ec7/jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b", size = 16945 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34", size = 18975 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jinjax"
|
||||
version = "0.48"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinja2" },
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a1/b2/54eb5f5d1db6764abf80ad499f81b5f84a798187e638fb60ffabda706d57/jinjax-0.48.tar.gz", hash = "sha256:689847fb095846ebd7ce1c254ae407698689a4b33300c461ff337f79194b69bc", size = 15551 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/ee/08c15d61fb46bbb9c53e28ca46ccdb1051d87cdb7116152efa2d36a1e7cf/jinjax-0.48-py3-none-any.whl", hash = "sha256:c556e7e73d9d9a9b7dd6f590bdca37bf9c514ca1af83ee94059dc727a66bd69b", size = 16721 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loguru"
|
||||
version = "0.7.3"
|
||||
@@ -878,45 +447,15 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "5.3.0"
|
||||
name = "mako"
|
||||
version = "1.3.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/6b/20c3a4b24751377aaa6307eb230b66701024012c29dd374999cc92983269/lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f", size = 3679318 }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/4f/ddb1965901bc388958db9f0c991255b2c469349a741ae8c9cd8a562d70a6/mako-1.3.9.tar.gz", hash = "sha256:b5d65ff3462870feec922dbccf38f6efb44e5714d7b593a656be86663d8600ac", size = 392195 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/d1f1c5e40c64bf62afd7a3f9b34ce18a586a1cccbf71e783cd0a6d8e8971/lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859", size = 8171753 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/83/26b1864921869784355459f374896dcf8b44d4af3b15d7697e9156cb2de9/lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e", size = 4441955 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/d2/e9bff9fb359226c25cda3538f664f54f2804f4b37b0d7c944639e1a51f69/lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f", size = 5050778 },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/69/6972bfafa8cd3ddc8562b126dd607011e218e17be313a8b1b9cc5a0ee876/lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e", size = 4748628 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/ea/a6523c7c7f6dc755a6eed3d2f6d6646617cad4d3d6d8ce4ed71bfd2362c8/lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179", size = 5322215 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/37/396fbd24a70f62b31d988e4500f2068c7f3fd399d2fd45257d13eab51a6f/lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a", size = 4813963 },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/91/e6136f17459a11ce1757df864b213efbeab7adcb2efa63efb1b846ab6723/lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3", size = 4923353 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/7c/2eeecf87c9a1fca4f84f991067c693e67340f2b7127fc3eca8fa29d75ee3/lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1", size = 4740541 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ed/4c38ba58defca84f5f0d0ac2480fdcd99fc7ae4b28fc417c93640a6949ae/lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d", size = 5346504 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/22/bbd3995437e5745cb4c2b5d89088d70ab19d4feabf8a27a24cecb9745464/lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c", size = 4898077 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/6e/94537acfb5b8f18235d13186d247bca478fea5e87d224644e0fe907df976/lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99", size = 4946543 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/e8/4b15df533fe8e8d53363b23a41df9be907330e1fa28c7ca36893fad338ee/lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff", size = 4816841 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/e7/03f390ea37d1acda50bc538feb5b2bda6745b25731e4e76ab48fae7106bf/lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a", size = 5417341 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/99/d1133ab4c250da85a883c3b60249d3d3e7c64f24faff494cf0fd23f91e80/lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8", size = 5327539 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ed/e6276c8d9668028213df01f598f385b05b55a4e1b4662ee12ef05dab35aa/lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d", size = 5012542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/88/684d4e800f5aa28df2a991a6a622783fb73cf0e46235cfa690f9776f032e/lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30", size = 3486454 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/82/ace5a5676051e60355bd8fb945df7b1ba4f4fb8447f2010fb816bfd57724/lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f", size = 3816857 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/6a/42141e4d373903bfea6f8e94b2f554d05506dfda522ada5343c651410dc8/lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a", size = 8156284 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/5e/fa097f0f7d8b3d113fb7312c6308af702f2667f22644441715be961f2c7e/lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd", size = 4432407 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/a1/b901988aa6d4ff937f2e5cfc114e4ec561901ff00660c3e56713642728da/lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51", size = 5048331 },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/0f/b2a54f48e52de578b71bbe2a2f8160672a8a5e103df3a78da53907e8c7ed/lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b", size = 4744835 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/9d/b000c15538b60934589e83826ecbc437a1586488d7c13f8ee5ff1f79a9b8/lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002", size = 5316649 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/ee/ffbb9eaff5e541922611d2c56b175c45893d1c0b8b11e5a497708a6a3b3b/lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4", size = 4812046 },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/ff/7ff89d567485c7b943cdac316087f16b2399a8b997007ed352a1248397e5/lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492", size = 4918597 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/a3/535b6ed8c048412ff51268bdf4bf1cf052a37aa7e31d2e6518038a883b29/lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3", size = 4738071 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/8f/cbbfa59cb4d4fd677fe183725a76d8c956495d7a3c7f111ab8f5e13d2e83/lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4", size = 5342213 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/fb/db4c10dd9958d4b52e34d1d1f7c1f434422aeaf6ae2bbaaff2264351d944/lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367", size = 4893749 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/38/bb4581c143957c47740de18a3281a0cab7722390a77cc6e610e8ebf2d736/lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832", size = 4945901 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/d5/18b7de4960c731e98037bd48fa9f8e6e8f2558e6fbca4303d9b14d21ef3b/lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff", size = 4815447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a8/cd51ceaad6eb849246559a8ef60ae55065a3df550fc5fcd27014361c1bab/lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd", size = 5411186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c3/1e3dabab519481ed7b1fdcba21dcfb8832f57000733ef0e71cf6d09a5e03/lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb", size = 5324481 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/17/71e9984cf0570cd202ac0a1c9ed5c1b8889b0fc8dc736f5ef0ffb181c284/lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b", size = 5011053 },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/68/9f7e6d3312a91e30829368c2b3217e750adef12a6f8eb10498249f4e8d72/lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957", size = 3485634 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/db/214290d58ad68c587bd5d6af3d34e56830438733d0d0856c0275fde43652/lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d", size = 3814417 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/83/de0a49e7de540513f53ab5d2e105321dedeb08a8f5850f0208decf4390ec/Mako-1.3.9-py3-none-any.whl", hash = "sha256:95920acccb578427a9aa38e37a186b1e43156c87260d7ba18ca63aa4c7cbd3a1", size = 78456 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -997,15 +536,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "more-itertools"
|
||||
version = "10.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/88/3b/7fa1fe835e2e93fd6d7b52b2f95ae810cf5ba133e1845f726f5a992d62c2/more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b", size = 125009 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/23/62/0fe302c6d1be1c777cab0616e6302478251dfbf9055ad426f5d0def75c89/more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89", size = 63038 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
@@ -1024,38 +554,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "passlib"
|
||||
version = "1.7.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
bcrypt = [
|
||||
{ name = "bcrypt" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.12.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.3.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.5.0"
|
||||
@@ -1065,82 +563,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plumbum"
|
||||
version = "1.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pywin32", marker = "platform_python_implementation != 'PyPy' and platform_system == 'Windows'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f0/5d/49ba324ad4ae5b1a4caefafbce7a1648540129344481f2ed4ef6bb68d451/plumbum-1.9.0.tar.gz", hash = "sha256:e640062b72642c3873bd5bdc3effed75ba4d3c70ef6b6a7b907357a84d909219", size = 319083 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/9d/d03542c93bb3d448406731b80f39c3d5601282f778328c22c77d270f4ed4/plumbum-1.9.0-py3-none-any.whl", hash = "sha256:9fd0d3b0e8d86e4b581af36edf3f3bbe9d1ae15b45b8caab28de1bcb27aaa7f5", size = 127970 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
{ name = "identify" },
|
||||
{ name = "nodeenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/c8/e22c292035f1bac8b9f5237a2622305bc0304e776080b246f3df57c4ff9f/pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2", size = 191678 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/16/8f/496e10d51edd6671ebe0432e33ff800aa86775d2d147ce7d43389324a525/pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878", size = 218713 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "premailer"
|
||||
version = "3.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cachetools" },
|
||||
{ name = "cssselect" },
|
||||
{ name = "cssutils" },
|
||||
{ name = "lxml" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/e49bd31941eff2987076383fa6d811eb785a28f498f5bb131e981bd71e13/premailer-3.10.0.tar.gz", hash = "sha256:d1875a8411f5dc92b53ef9f193db6c0f879dc378d618e0ad292723e388bfe4c2", size = 24342 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/07/4e8d94f94c7d41ca5ddf8a9695ad87b888104e2fd41a35546c1dc9ca74ac/premailer-3.10.0-py2.py3-none-any.whl", hash = "sha256:021b8196364d7df96d04f9ade51b794d0b77bcc19e998321c515633a2273be1a", size = 19544 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.48"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "wcwidth" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2d/4f/feb5e137aff82f7c7f3248267b97451da3644f6cdc218edfe549fb354127/prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90", size = 424684 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6a/fd08d94654f7e67c52ca30523a178b3f8ccc4237fce4be90d39c938a831a/prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e", size = 386595 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2"
|
||||
version = "2.9.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/51/2007ea29e605957a17ac6357115d0c1a1b60c8c984951c19419b3474cdfd/psycopg2-2.9.10.tar.gz", hash = "sha256:12ec0b40b0273f95296233e8750441339298e6a572f7039da5b260e3c8b60e11", size = 385672 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/16/4623fad6076448df21c1a870c93a9774ad8a7b4dd1660223b59082dd8fec/psycopg2-2.9.10-cp312-cp312-win32.whl", hash = "sha256:65a63d7ab0e067e2cdb3cf266de39663203d38d6a8ed97f5ca0cb315c73fe067", size = 1025113 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/de/baed128ae0fc07460d9399d82e631ea31a1f171c0c4ae18f9808ac6759e3/psycopg2-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:4a579d6243da40a7b3182e0430493dbd55950c493d8c68f4eec0b302f6bbf20e", size = 1163951 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/49/a6cfc94a9c483b1fa401fbcb23aca7892f60c7269c5ffa2ac408364f80dc/psycopg2-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:91fd603a2155da8d0cfcdbf8ab24a2d54bca72795b90d2a3ed2b6da8d979dee2", size = 2569060 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.5"
|
||||
@@ -1224,15 +646,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.392.post0"
|
||||
@@ -1349,19 +762,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
version = "308"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/00/7c/d00d6bdd96de4344e06c4afbf218bc86b54436a94c01c71a8701f613aa56/pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897", size = 5939729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/27/0c8811fbc3ca188f93b5354e7c286eb91f80a53afa4e11007ef661afa746/pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47", size = 6543015 },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/0f/d40f8373608caed2255781a3ad9a51d03a594a1248cd632d6a298daca693/pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091", size = 7976033 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/a4/aa562d8935e3df5e49c161b427a3a2efad2ed4e9cf81c3de636f1fdddfd0/pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed", size = 5938579 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/50/b0efb8bb66210da67a53ab95fd7a98826a97ee21f1d22949863e6d588b22/pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4", size = 6542056 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/df/2b63e3e4f2df0224f8aaf6d131f54fe4e8c96400eb9df563e2aae2e1a1f9/pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd", size = 7974986 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
@@ -1388,18 +788,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "questionary"
|
||||
version = "2.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "prompt-toolkit" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/b8/d16eb579277f3de9e56e5ad25280fab52fc5774117fb70362e8c2e016559/questionary-2.1.0.tar.gz", hash = "sha256:6302cdd645b19667d8f6e6634774e9538bfcd1aad9be287e743d96cacaf95587", size = 26775 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/3f/11dd4cd4f39e05128bfd20138faea57bec56f9ffba6185d276e3107ba5b2/questionary-2.1.0-py3-none-any.whl", hash = "sha256:44174d237b68bc828e4878c763a9ad6790ee61990e0ae72927694ead57bab8ec", size = 36747 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2024.11.6"
|
||||
@@ -1438,21 +826,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/45/94/bc295babb3062a731f52621cdc992d123111282e291abaf23faa413443ea/regex-2024.11.6-cp313-cp313-win_amd64.whl", hash = "sha256:2b3361af3198667e99927da8b84c1b010752fa4b1115ee30beaa332cabc3ef1a", size = 273545 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "13.9.4"
|
||||
@@ -1561,11 +934,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/36/59cc97c365f2f79ac9f3f51446cae56dfd82c4f2dd98497e6be6de20fb91/SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1", size = 1894113 },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
asyncio = [
|
||||
{ name = "greenlet" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.2.1"
|
||||
@@ -1591,18 +959,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "starlette-wtf"
|
||||
version = "0.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "itsdangerous" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "starlette" },
|
||||
{ name = "wtforms" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a6/15/e20a9eb32c1e51c08bec4b37859f49b5d70c76cd93d925862627d271cbd3/Starlette-WTF-0.4.5.tar.gz", hash = "sha256:411e94c19f47302fe3b12752b3c531581c0bad4cbe603a730bbe516733a99527", size = 15990 }
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.15.1"
|
||||
@@ -1657,15 +1013,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b7/6ec57841fb67c98f52fc8e4a2d96df60059637cba077edc569a302a8ffc7/Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39", size = 235494 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.0"
|
||||
@@ -1710,20 +1057,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.29.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/ca/f23dcb02e161a9bba141b1c08aa50e8da6ea25e6d780528f1d385a3efe25/virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35", size = 7658028 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/9b/599bcfc7064fbe5740919e78c5df18e5dceb0887e676256a1061bb5ae232/virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779", size = 4282379 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "watchfiles"
|
||||
version = "1.0.4"
|
||||
@@ -1760,44 +1093,35 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.2.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "websockets"
|
||||
version = "14.1"
|
||||
version = "14.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/1b/380b883ce05bb5f45a905b61790319a28958a9ab1e4b6b95ff5464b60ca1/websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8", size = 162840 }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/54/8359678c726243d19fae38ca14a334e740782336c9f19700858c4eb64a1e/websockets-14.2.tar.gz", hash = "sha256:5059ed9c54945efb321f097084b4c7e52c246f2c869815876a69d1efc4ad6eb5", size = 164394 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/55/64/55698544ce29e877c9188f1aee9093712411a8fc9732cca14985e49a8e9c/websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed", size = 161957 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/b1/b088f67c2b365f2c86c7b48edb8848ac27e508caf910a9d9d831b2f343cb/websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d", size = 159620 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/89/2a09db1bbb40ba967a1b8225b07b7df89fea44f06de9365f17f684d0f7e6/websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707", size = 159852 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/c1/f983138cd56e7d3079f1966e81f77ce6643f230cd309f73aa156bb181749/websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a", size = 169675 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/c8/84191455d8660e2a0bdb33878d4ee5dfa4a2cedbcdc88bbd097303b65bfa/websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45", size = 168619 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/a7/62e551fdcd7d44ea74a006dc193aba370505278ad76efd938664531ce9d6/websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58", size = 169042 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/ed/1532786f55922c1e9c4d329608e36a15fdab186def3ca9eb10d7465bc1cc/websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058", size = 169345 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/fb/160f66960d495df3de63d9bcff78e1b42545b2a123cc611950ffe6468016/websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4", size = 168725 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/53/1bf0c06618b5ac35f1d7906444b9958f8485682ab0ea40dee7b17a32da1e/websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05", size = 168712 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/22/5ec2f39fff75f44aa626f86fa7f20594524a447d9c3be94d8482cd5572ef/websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0", size = 162838 },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/28f07df09f2983178db7bf6c9cccc847205d2b92ced986cd79565d68af4f/websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f", size = 163277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/77/812b3ba5110ed8726eddf9257ab55ce9e85d97d4aa016805fdbecc5e5d48/websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9", size = 161966 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/24/4fcb7aa6986ae7d9f6d083d9d53d580af1483c5ec24bdec0978307a0f6ac/websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b", size = 159625 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/47/2a0a3a2fc4965ff5b9ce9324d63220156bd8bedf7f90824ab92a822e65fd/websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3", size = 159857 },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/c8/d7b425011a15e35e17757e4df75b25e1d0df64c0c315a44550454eaf88fc/websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59", size = 169635 },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/39/6e3b5cffa11036c40bd2f13aba2e8e691ab2e01595532c46437b56575678/websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2", size = 168578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/03/8faa5c9576299b2adf34dcccf278fc6bbbcda8a3efcc4d817369026be421/websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da", size = 169018 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/05/ea1fec05cc3a60defcdf0bb9f760c3c6bd2dd2710eff7ac7f891864a22ba/websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9", size = 169383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/1d/eac1d9ed787f80754e51228e78855f879ede1172c8b6185aca8cef494911/websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7", size = 168773 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/1b/e808685530185915299740d82b3a4af3f2b44e56ccf4389397c7a5d95d39/websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a", size = 168757 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/19/6ab716d02a3b068fbbeb6face8a7423156e12c446975312f1c7c0f4badab/websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6", size = 162834 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/fd/ab6b7676ba712f2fc89d1347a4b5bdc6aa130de10404071f2b2606450209/websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0", size = 163277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/0b/c7e5d11020242984d9d37990310520ed663b942333b83a033c2f20191113/websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e", size = 156277 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/81/04f7a397653dc8bec94ddc071f34833e8b99b13ef1a3804c149d59f92c18/websockets-14.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f20522e624d7ffbdbe259c6b6a65d73c895045f76a93719aa10cd93b3de100c", size = 163096 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/c5/de30e88557e4d70988ed4d2eabd73fd3e1e52456b9f3a4e9564d86353b6d/websockets-14.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:647b573f7d3ada919fd60e64d533409a79dcf1ea21daeb4542d1d996519ca967", size = 160758 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/8c/d130d668781f2c77d106c007b6c6c1d9db68239107c41ba109f09e6c218a/websockets-14.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6af99a38e49f66be5a64b1e890208ad026cda49355661549c507152113049990", size = 160995 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/bc/f6678a0ff17246df4f06765e22fc9d98d1b11a258cc50c5968b33d6742a1/websockets-14.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:091ab63dfc8cea748cc22c1db2814eadb77ccbf82829bac6b2fbe3401d548eda", size = 170815 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/b2/8070cb970c2e4122a6ef38bc5b203415fd46460e025652e1ee3f2f43a9a3/websockets-14.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b374e8953ad477d17e4851cdc66d83fdc2db88d9e73abf755c94510ebddceb95", size = 169759 },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/da/72f7caabd94652e6eb7e92ed2d3da818626e70b4f2b15a854ef60bf501ec/websockets-14.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a39d7eceeea35db85b85e1169011bb4321c32e673920ae9c1b6e0978590012a3", size = 170178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/e0/812725b6deca8afd3a08a2e81b3c4c120c17f68c9b84522a520b816cda58/websockets-14.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0a6f3efd47ffd0d12080594f434faf1cd2549b31e54870b8470b28cc1d3817d9", size = 170453 },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/d3/8275dbc231e5ba9bb0c4f93144394b4194402a7a0c8ffaca5307a58ab5e3/websockets-14.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:065ce275e7c4ffb42cb738dd6b20726ac26ac9ad0a2a48e33ca632351a737267", size = 169830 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/ae/e7d1a56755ae15ad5a94e80dd490ad09e345365199600b2629b18ee37bc7/websockets-14.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e9d0e53530ba7b8b5e389c02282f9d2aa47581514bd6049d3a7cffe1385cf5fe", size = 169824 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/32/88ccdd63cb261e77b882e706108d072e4f1c839ed723bf91a3e1f216bf60/websockets-14.2-cp312-cp312-win32.whl", hash = "sha256:20e6dd0984d7ca3037afcb4494e48c74ffb51e8013cac71cf607fffe11df7205", size = 163981 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/7d/32cdb77990b3bdc34a306e0a0f73a1275221e9a66d869f6ff833c95b56ef/websockets-14.2-cp312-cp312-win_amd64.whl", hash = "sha256:44bba1a956c2c9d268bdcdf234d5e5ff4c9b6dc3e300545cbe99af59dda9dcce", size = 164421 },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/94/4f9b55099a4603ac53c2912e1f043d6c49d23e94dd82a9ce1eb554a90215/websockets-14.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f1372e511c7409a542291bce92d6c83320e02c9cf392223272287ce55bc224e", size = 163102 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/b7/7484905215627909d9a79ae07070057afe477433fdacb59bf608ce86365a/websockets-14.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4da98b72009836179bb596a92297b1a61bb5a830c0e483a7d0766d45070a08ad", size = 160766 },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/a4/edb62efc84adb61883c7d2c6ad65181cb087c64252138e12d655989eec05/websockets-14.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8a86a269759026d2bde227652b87be79f8a734e582debf64c9d302faa1e9f03", size = 160998 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/79/036d320dc894b96af14eac2529967a6fc8b74f03b83c487e7a0e9043d842/websockets-14.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86cf1aaeca909bf6815ea714d5c5736c8d6dd3a13770e885aafe062ecbd04f1f", size = 170780 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/75/5737d21ee4dd7e4b9d487ee044af24a935e36a9ff1e1419d684feedcba71/websockets-14.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b0f6c3ba3b1240f602ebb3971d45b02cc12bd1845466dd783496b3b05783a5", size = 169717 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/3c/bf9b2c396ed86a0b4a92ff4cdaee09753d3ee389be738e92b9bbd0330b64/websockets-14.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:669c3e101c246aa85bc8534e495952e2ca208bd87994650b90a23d745902db9a", size = 170155 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/2d/83a5aca7247a655b1da5eb0ee73413abd5c3a57fc8b92915805e6033359d/websockets-14.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eabdb28b972f3729348e632ab08f2a7b616c7e53d5414c12108c29972e655b20", size = 170495 },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/dd/699238a92761e2f943885e091486378813ac8f43e3c84990bc394c2be93e/websockets-14.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2066dc4cbcc19f32c12a5a0e8cc1b7ac734e5b64ac0a325ff8353451c4b15ef2", size = 169880 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/c9/67a8f08923cf55ce61aadda72089e3ed4353a95a3a4bc8bf42082810e580/websockets-14.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab95d357cd471df61873dadf66dd05dd4709cae001dd6342edafc8dc6382f307", size = 169856 },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/b1/1ffdb2680c64e9c3921d99db460546194c40d4acbef999a18c37aa4d58a3/websockets-14.2-cp313-cp313-win32.whl", hash = "sha256:a9e72fb63e5f3feacdcf5b4ff53199ec8c18d66e325c34ee4c551ca748623bbc", size = 163974 },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/13/8b7fc4cb551b9cfd9890f0fd66e53c18a06240319915533b033a56a3d520/websockets-14.2-cp313-cp313-win_amd64.whl", hash = "sha256:b439ea828c4ba99bb3176dc8d9b933392a2413c0f6b149fdcba48393f573377f", size = 164420 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/c8/d529f8a32ce40d98309f4470780631e971a5a842b60aec864833b3615786/websockets-14.2-py3-none-any.whl", hash = "sha256:7a6ceec4ea84469f15cf15807a747e9efe57e369c384fa86e022b3bea679b79b", size = 157416 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1808,15 +1132,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b66
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wtforms"
|
||||
version = "3.2.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454 },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user