Compare commits

..

45 Commits

Author SHA1 Message Date
semantic-release 3d45227b72 chore(release): 0.11.0 [skip ci] 2025-03-29 00:19:46 +00:00
Paul Hernandez 069c0a21c6 feat: add bm command alias for basic-memory (#67)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-28 19:12:25 -05:00
Paul Hernandez b27827671d feat: rename search tool to search_notes (#66)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-28 19:11:35 -05:00
Paul Hernandez 0743ade5fc fix: just delete db for reset db instead of using migrations. (#65)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-28 19:11:11 -05:00
Paul Hernandez f1c95709cb fix: make logs for each process - mcp, sync, cli (#64)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-28 19:10:43 -05:00
Matias Forbord 3c68b7d5dd docs: Update broken "Multiple Projects" link in README.md (#55) 2025-03-26 14:31:51 -05:00
semantic-release 3d0077990f chore(release): 0.10.1 [skip ci] 2025-03-25 18:12:28 +00:00
phernandez 731b502d36 docs: add help docs to mcp cli tools
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-25 13:01:11 -05:00
phernandez 2a881b1425 refactor: move project stats into projct subcommand
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-25 12:50:11 -05:00
phernandez 681af5d450 chore: remove duplicate code in entity_service.py from bad merge
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-25 12:27:37 -05:00
phernandez b35037eb86 fix broken merge
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-24 23:18:22 -05:00
phernandez b667bca5a2 fix test coverage and type checks
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-24 22:46:47 -05:00
phernandez e716946b44 fix: preserve custom frontmatter fields when updating notes
Fixes #36 by modifying entity_service.update_entity() to read existing
frontmatter from files before updating them. Custom metadata fields
such as Status, Priority, and Version are now preserved when notes
are updated through the write_note MCP tool.

Added test case that verifies this behavior by creating a note with
custom frontmatter and then updating it.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-24 22:46:47 -05:00
phernandez 46c4fd2164 fix: make set_default_project also activate project for current session to fix #37
This change makes the 'basic-memory project default <name>' command both:
1. Set the default project for future invocations (persistent change)
2. Activate the project for the current session (immediate change)

Added tests to verify this behavior, which resolves issue #37 where the
project name and path weren't changing properly when the default project
was changed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-24 22:46:47 -05:00
phernandez 9c791259a0 \fix: improve tags handling in write_note tool to fix #38\n\nThis change makes the write_note tool more flexible with tag inputs by:\n1. Adding a parse_tags helper function to handle various input formats\n2. Removing explicit type annotation from the write_note signature\n3. Adding documentation on how to pass tags from external MCP clients\n4. Adding a test that reproduces the issue from bug report #38\n\nThe issue occurred in external MCP clients like Cursor where the tags\nparameter was causing type mismatch errors.\n\n\ud83e\udd16 Generated with [Claude Code](https://claude.ai/code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-24 22:46:47 -05:00
phernandez cc2cae72c1 fix: move ai_assistant_guide.md into package resources to fix #39
This change relocates the AI assistant guide from the static directory
into the package resources directory, ensuring it gets properly included
in the distribution package and is accessible when installed via pip/uv.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-24 22:46:47 -05:00
phernandez 0a1fdd8f15 update docs
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-24 22:46:47 -05:00
phernandez 78f234b180 fix: preserve custom frontmatter fields when updating notes
Fixes #36 by modifying entity_service.update_entity() to read existing
frontmatter from files before updating them. Custom metadata fields
such as Status, Priority, and Version are now preserved when notes
are updated through the write_note MCP tool.

Added test case that verifies this behavior by creating a note with
custom frontmatter and then updating it.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-24 19:50:38 -05:00
phernandez cbe72be10a fix: make set_default_project also activate project for current session to fix #37
This change makes the 'basic-memory project default <name>' command both:
1. Set the default project for future invocations (persistent change)
2. Activate the project for the current session (immediate change)

Added tests to verify this behavior, which resolves issue #37 where the
project name and path weren't changing properly when the default project
was changed.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-24 18:56:51 -05:00
phernandez 0ade6b0603 \fix: improve tags handling in write_note tool to fix #38\n\nThis change makes the write_note tool more flexible with tag inputs by:\n1. Adding a parse_tags helper function to handle various input formats\n2. Removing explicit type annotation from the write_note signature\n3. Adding documentation on how to pass tags from external MCP clients\n4. Adding a test that reproduces the issue from bug report #38\n\nThe issue occurred in external MCP clients like Cursor where the tags\nparameter was causing type mismatch errors.\n\n\ud83e\udd16 Generated with [Claude Code](https://claude.ai/code)\n\nCo-Authored-By: Claude <noreply@anthropic.com>\ 2025-03-24 18:30:21 -05:00
phernandez 390ff9d31c fix: move ai_assistant_guide.md into package resources to fix #39
This change relocates the AI assistant guide from the static directory
into the package resources directory, ensuring it gets properly included
in the distribution package and is accessible when installed via pip/uv.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-24 17:34:47 -05:00
phernandez 3ed74b4a4d update docs 2025-03-24 17:28:30 -05:00
Ikko Eltociear Ashimine dfaf0fea9c docs: update CLAUDE.md (#33)
fix spelling in CLAUDE.md: enviroment -> environment
Signed-off-by: Ikko Eltociear Ashimine <eltociear@gmail.com>
2025-03-24 13:15:12 -05:00
phernandez b26afa927f docs: add mcp badge, update cli reference, llms-install.md
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-16 14:09:08 -05:00
phernandez 3806943f8c add star history to README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-16 11:24:08 -05:00
phernandez 3bffb2e190 Add updated getting started docs and utf8 test
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 16:52:17 -05:00
semantic-release 9fe34dcddf chore(release): 0.10.0 [skip ci] 2025-03-15 20:49:20 +00:00
Paul Hernandez eb5e4ec6bd fix: improve utf-8 support for file reading/writing (#32)
fixes #29

Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 15:43:37 -05:00
Paul Hernandez 6b110b28dd fix: don't sync *.tmp files on watch (#31)
Fixes #30

Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 15:43:28 -05:00
phernandez 88d193e0aa Edig content in README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 11:46:53 -05:00
phernandez fa9175f049 Remove redundant info in README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 11:45:28 -05:00
phernandez 7c787a413c Simpify README.md, ONTRIBUTING.md
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 11:31:18 -05:00
Paul Hernandez 0fa1adf3b5 Update README.md
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-03-15 11:17:29 -05:00
phernandez cbe488fbd2 remove .obsidian dir in docs, try to fix video
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 11:00:27 -05:00
Paul Hernandez 99ff8f60c2 Update README.md
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-03-15 10:57:59 -05:00
phernandez 06ee2852a0 try to fix video in README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 10:53:08 -05:00
phernandez 939bab1a24 fix demo link in README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 10:48:58 -05:00
phernandez 67736487fb update README.md
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 10:43:41 -05:00
phernandez 2ac620f98b Merge branch 'main' of github.com:basicmachines-co/basic-memory 2025-03-15 10:40:16 -05:00
phernandez 4cbda7a9d4 update docs
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-15 10:40:09 -05:00
Paul Hernandez 3070ade69a Create dependabot.yml
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-03-15 10:34:53 -05:00
Paul Hernandez 9af913da4f docs: add glama badge. Fix typos in README.md (#28)
Signed-off-by: phernandez <paul@basicmachines.co>
2025-03-13 13:24:39 -05:00
Paul Hernandez 7b7bec9557 Create SECURITY.md
Signed-off-by: Paul Hernandez <60959+phernandez@users.noreply.github.com>
2025-03-12 15:51:23 -05:00
bm-claudeai fea2f40d1b docs: Update CLAUDE.md with GitHub integration capabilities (#25)
This PR updates the CLAUDE.md file to document the GitHub integration
capabilities that enable Claude to participate directly in the
development workflow.
2025-03-11 16:32:30 -05:00
bm-claudeai eb1e7b6088 feat: Add Smithery integration for easier installation (#24)
This PR adds support for deploying Basic Memory on the Smithery platform.

Signed-off-by: bm-claudeai <claude@basicmachines.co>
2025-03-11 16:21:51 -05:00
62 changed files with 2177 additions and 983 deletions
+12
View File
@@ -0,0 +1,12 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
+2 -1
View File
@@ -46,4 +46,5 @@ ENV/
# obsidian docs:
/docs/.obsidian/
/examples/.obsidian/
/examples/.obsidian/
/examples/.basic-memory/
+228
View File
@@ -1,6 +1,234 @@
# CHANGELOG
## v0.11.0 (2025-03-29)
### Bug Fixes
- Just delete db for reset db instead of using migrations.
([#65](https://github.com/basicmachines-co/basic-memory/pull/65),
[`0743ade`](https://github.com/basicmachines-co/basic-memory/commit/0743ade5fc07440f95ecfd816ba7e4cfd74bca12))
Signed-off-by: phernandez <paul@basicmachines.co>
- Make logs for each process - mcp, sync, cli
([#64](https://github.com/basicmachines-co/basic-memory/pull/64),
[`f1c9570`](https://github.com/basicmachines-co/basic-memory/commit/f1c95709cbffb1b88292547b0b8f29fcca22d186))
Signed-off-by: phernandez <paul@basicmachines.co>
### Documentation
- Update broken "Multiple Projects" link in README.md
([#55](https://github.com/basicmachines-co/basic-memory/pull/55),
[`3c68b7d`](https://github.com/basicmachines-co/basic-memory/commit/3c68b7d5dd689322205c67637dca7d188111ee6b))
### Features
- Add bm command alias for basic-memory
([#67](https://github.com/basicmachines-co/basic-memory/pull/67),
[`069c0a2`](https://github.com/basicmachines-co/basic-memory/commit/069c0a21c630784e1bf47d2b7de5d6d1f6fadd7a))
Signed-off-by: phernandez <paul@basicmachines.co>
- Rename search tool to search_notes
([#66](https://github.com/basicmachines-co/basic-memory/pull/66),
[`b278276`](https://github.com/basicmachines-co/basic-memory/commit/b27827671dc010be3e261b8b221aca6b7f836661))
Signed-off-by: phernandez <paul@basicmachines.co>
## v0.10.1 (2025-03-25)
### Bug Fixes
- Make set_default_project also activate project for current session to fix #37
([`cbe72be`](https://github.com/basicmachines-co/basic-memory/commit/cbe72be10a646c0b03931bb39aff9285feae47f9))
This change makes the 'basic-memory project default <name>' command both: 1. Set the default project
for future invocations (persistent change) 2. Activate the project for the current session
(immediate change)
Added tests to verify this behavior, which resolves issue #37 where the project name and path
weren't changing properly when the default project was changed.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Make set_default_project also activate project for current session to fix #37
([`46c4fd2`](https://github.com/basicmachines-co/basic-memory/commit/46c4fd21645b109af59eb2a0201c7bd849b34a49))
This change makes the 'basic-memory project default <name>' command both: 1. Set the default project
for future invocations (persistent change) 2. Activate the project for the current session
(immediate change)
Added tests to verify this behavior, which resolves issue #37 where the project name and path
weren't changing properly when the default project was changed.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
- Move ai_assistant_guide.md into package resources to fix #39
([`390ff9d`](https://github.com/basicmachines-co/basic-memory/commit/390ff9d31ccee85bef732e8140b5eeecd7ee176f))
This change relocates the AI assistant guide from the static directory into the package resources
directory, ensuring it gets properly included in the distribution package and is accessible when
installed via pip/uv.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Move ai_assistant_guide.md into package resources to fix #39
([`cc2cae7`](https://github.com/basicmachines-co/basic-memory/commit/cc2cae72c14b380f78ffeb67c2261e4dbee45faf))
This change relocates the AI assistant guide from the static directory into the package resources
directory, ensuring it gets properly included in the distribution package and is accessible when
installed via pip/uv.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
- Preserve custom frontmatter fields when updating notes
([`78f234b`](https://github.com/basicmachines-co/basic-memory/commit/78f234b1806b578a0a833e8ee4184015b7369a97))
Fixes #36 by modifying entity_service.update_entity() to read existing frontmatter from files before
updating them. Custom metadata fields such as Status, Priority, and Version are now preserved when
notes are updated through the write_note MCP tool.
Added test case that verifies this behavior by creating a note with custom frontmatter and then
updating it.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Preserve custom frontmatter fields when updating notes
([`e716946`](https://github.com/basicmachines-co/basic-memory/commit/e716946b4408d017eca4be720956d5a210b4e6b1))
Fixes #36 by modifying entity_service.update_entity() to read existing frontmatter from files before
updating them. Custom metadata fields such as Status, Priority, and Version are now preserved when
notes are updated through the write_note MCP tool.
Added test case that verifies this behavior by creating a note with custom frontmatter and then
updating it.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
### Chores
- Remove duplicate code in entity_service.py from bad merge
([`681af5d`](https://github.com/basicmachines-co/basic-memory/commit/681af5d4505dadc40b4086630f739d76bac9201d))
Signed-off-by: phernandez <paul@basicmachines.co>
### Documentation
- Add help docs to mcp cli tools
([`731b502`](https://github.com/basicmachines-co/basic-memory/commit/731b502d36cec253d114403d73b48fab3c47786e))
Signed-off-by: phernandez <paul@basicmachines.co>
- Add mcp badge, update cli reference, llms-install.md
([`b26afa9`](https://github.com/basicmachines-co/basic-memory/commit/b26afa927f98021246cd8b64858e57333595ea90))
Signed-off-by: phernandez <paul@basicmachines.co>
- Update CLAUDE.md ([#33](https://github.com/basicmachines-co/basic-memory/pull/33),
[`dfaf0fe`](https://github.com/basicmachines-co/basic-memory/commit/dfaf0fea9cf5b97d169d51a6276ec70162c21a7e))
fix spelling in CLAUDE.md: enviroment -> environment Signed-off-by: Ikko Eltociear Ashimine
<eltociear@gmail.com>
### Refactoring
- Move project stats into projct subcommand
([`2a881b1`](https://github.com/basicmachines-co/basic-memory/commit/2a881b1425c73947f037fbe7ac5539c015b62526))
Signed-off-by: phernandez <paul@basicmachines.co>
## v0.10.0 (2025-03-15)
### Bug Fixes
- Ai_resource_guide.md path
([`da97353`](https://github.com/basicmachines-co/basic-memory/commit/da97353cfc3acc1ceb0eca22ac6af326f77dc199))
Signed-off-by: phernandez <paul@basicmachines.co>
- Ai_resource_guide.md path
([`c4732a4`](https://github.com/basicmachines-co/basic-memory/commit/c4732a47b37dd2e404139fb283b65556c81ce7c9))
- Ai_resource_guide.md path
([`2e9d673`](https://github.com/basicmachines-co/basic-memory/commit/2e9d673e54ad6a63a971db64f01fc2f4e59c2e69))
Signed-off-by: phernandez <paul@basicmachines.co>
- Don't sync *.tmp files on watch ([#31](https://github.com/basicmachines-co/basic-memory/pull/31),
[`6b110b2`](https://github.com/basicmachines-co/basic-memory/commit/6b110b28dd8ba705ebfc0bcb41faf2cb993da2c3))
Fixes #30
Signed-off-by: phernandez <paul@basicmachines.co>
- Drop search_index table on db reindex
([`31cca6f`](https://github.com/basicmachines-co/basic-memory/commit/31cca6f913849a0ab8fc944803533e3072e9ef88))
Signed-off-by: phernandez <paul@basicmachines.co>
- Improve utf-8 support for file reading/writing
([#32](https://github.com/basicmachines-co/basic-memory/pull/32),
[`eb5e4ec`](https://github.com/basicmachines-co/basic-memory/commit/eb5e4ec6bd4d2fe757087be030d867f4ca1d38ba))
fixes #29
Signed-off-by: phernandez <paul@basicmachines.co>
### Chores
- Remove logfire
([`9bb8a02`](https://github.com/basicmachines-co/basic-memory/commit/9bb8a020c3425a02cb3a88f6f02adcd281bccee2))
Signed-off-by: phernandez <paul@basicmachines.co>
### Documentation
- Add glama badge. Fix typos in README.md
([#28](https://github.com/basicmachines-co/basic-memory/pull/28),
[`9af913d`](https://github.com/basicmachines-co/basic-memory/commit/9af913da4fba7bb4908caa3f15f2db2aa03777ec))
Signed-off-by: phernandez <paul@basicmachines.co>
- Update CLAUDE.md with GitHub integration capabilities
([#25](https://github.com/basicmachines-co/basic-memory/pull/25),
[`fea2f40`](https://github.com/basicmachines-co/basic-memory/commit/fea2f40d1b54d0c533e6d7ee7ce1aa7b83ad9a47))
This PR updates the CLAUDE.md file to document the GitHub integration capabilities that enable
Claude to participate directly in the development workflow.
### Features
- Add Smithery integration for easier installation
([#24](https://github.com/basicmachines-co/basic-memory/pull/24),
[`eb1e7b6`](https://github.com/basicmachines-co/basic-memory/commit/eb1e7b6088b0b3dead9c104ee44174b2baebf417))
This PR adds support for deploying Basic Memory on the Smithery platform.
Signed-off-by: bm-claudeai <claude@basicmachines.co>
## v0.9.0 (2025-03-07)
### Chores
+46 -4
View File
@@ -37,6 +37,7 @@ See the [README.md](README.md) file for a project overview.
- API uses FastAPI for endpoints
- Follow the repository pattern for data access
- Tools communicate to api routers via the httpx ASGI client (in process)
- avoid using "private" functions in modules or classes (prepended with _)
### Codebase Architecture
@@ -63,7 +64,7 @@ See the [README.md](README.md) file for a project overview.
- Testing uses pytest with asyncio support (strict mode)
- Test database uses in-memory SQLite
- Avoid creating mocks in tests in most circumstances.
- Each test runs in a standalone enviroment with in memory SQLite and tmp_file directory
- Each test runs in a standalone environment with in memory SQLite and tmp_file directory
## BASIC MEMORY PRODUCT USAGE
@@ -106,7 +107,7 @@ See the [README.md](README.md) file for a project overview.
1d", "1 week")
**Search & Discovery:**
- `search(query, page, page_size)` - Full-text search across all content with filtering options
- `search_notes(query, page, page_size)` - Full-text search across all content with filtering options
**Visualization:**
- `canvas(nodes, edges, title, folder)` - Generate Obsidian canvas files for knowledge graph visualization
@@ -114,7 +115,7 @@ See the [README.md](README.md) file for a project overview.
- MCP Prompts for better AI interaction:
- `ai_assistant_guide()` - Guidance on effectively using Basic Memory tools for AI assistants
- `continue_conversation(topic, timeframe)` - Continue previous conversations with relevant historical context
- `search(query, after_date)` - Search with detailed, formatted results for better context understanding
- `search_notes(query, after_date)` - Search with detailed, formatted results for better context understanding
- `recent_activity(timeframe)` - View recently changed items with formatted output
- `json_canvas_spec()` - Full JSON Canvas specification for Obsidian visualization
@@ -130,4 +131,45 @@ of using AI just for code generation, we've developed a true collaborative workf
5. Results improve through iterative collaboration and shared understanding
This approach has allowed us to tackle more complex challenges and build a more robust system than either humans or AI
could achieve independently.
could achieve independently.
## GitHub Integration
Basic Memory uses Claude directly into the development workflow through GitHub:
### GitHub MCP Tools
Using the GitHub Model Context Protocol server, Claude can:
- **Repository Management**:
- View repository files and structure
- Read file contents
- Create new branches
- Create and update files
- **Issue Management**:
- Create new issues
- Comment on existing issues
- Close and update issues
- Search across issues
- **Pull Request Workflow**:
- Create pull requests
- Review code changes
- Add comments to PRs
This integration enables Claude to participate as a full team member in the development process, not just as a code
generation tool. Claude's GitHub account ([bm-claudeai](https://github.com/bm-claudeai)) is a member of the Basic
Machines organization with direct contributor access to the codebase.
### Collaborative Development Process
With GitHub integration, the development workflow includes:
1. **Direct code review** - Claude can analyze PRs and provide detailed feedback
2. **Contribution tracking** - All of Claude's contributions are properly attributed in the Git history
3. **Branch management** - Claude can create feature branches for implementations
4. **Documentation maintenance** - Claude can keep documentation updated as the code evolves
With this integration, the AI assistant is a full-fledged team member rather than just a tool for generating code
snippets.
+38 -34
View File
@@ -1,6 +1,7 @@
# Contributing to Basic Memory
Thank you for considering contributing to Basic Memory! This document outlines the process for contributing to the project and how to get started as a developer.
Thank you for considering contributing to Basic Memory! This document outlines the process for contributing to the
project and how to get started as a developer.
## Getting Started
@@ -65,61 +66,64 @@ Thank you for considering contributing to Basic Memory! This document outlines t
This project is designed for collaborative development between humans and LLMs (Large Language Models):
1. **CLAUDE.md**: The repository includes a `CLAUDE.md` file that serves as a project guide for both humans and LLMs. This file contains:
- Key project information and architectural overview
- Development commands and workflows
- Code style guidelines
- Documentation standards
1. **CLAUDE.md**: The repository includes a `CLAUDE.md` file that serves as a project guide for both humans and LLMs.
This file contains:
- Key project information and architectural overview
- Development commands and workflows
- Code style guidelines
- Documentation standards
2. **AI-Human Collaborative Workflow**:
- We encourage using LLMs like Claude for code generation, reviews, and documentation
- When possible, save context in markdown files that can be referenced later
- This enables seamless knowledge transfer between different development sessions
- Claude can help with implementation details while you focus on architecture and design
- We encourage using LLMs like Claude for code generation, reviews, and documentation
- When possible, save context in markdown files that can be referenced later
- This enables seamless knowledge transfer between different development sessions
- Claude can help with implementation details while you focus on architecture and design
3. **Adding to CLAUDE.md**:
- If you discover useful project information or common commands, consider adding them to CLAUDE.md
- This helps all contributors (human and AI) maintain consistent knowledge of the project
- If you discover useful project information or common commands, consider adding them to CLAUDE.md
- This helps all contributors (human and AI) maintain consistent knowledge of the project
## Pull Request Process
1. **Create a Pull Request**: Open a PR against the `main` branch with a clear title and description.
2. **Sign the Developer Certificate of Origin (DCO)**: All contributions require signing our DCO, which certifies that you have the right to submit your contributions. This will be automatically checked by our CLA assistant when you create a PR.
2. **Sign the Developer Certificate of Origin (DCO)**: All contributions require signing our DCO, which certifies that
you have the right to submit your contributions. This will be automatically checked by our CLA assistant when you
create a PR.
3. **PR Description**: Include:
- What the PR changes
- Why the change is needed
- How you tested the changes
- Any related issues (use "Fixes #123" to automatically close issues)
- What the PR changes
- Why the change is needed
- How you tested the changes
- Any related issues (use "Fixes #123" to automatically close issues)
4. **Code Review**: Wait for code review and address any feedback.
5. **CI Checks**: Ensure all CI checks pass.
6. **Merge**: Once approved, a maintainer will merge your PR.
## Developer Certificate of Origin
By contributing to this project, you agree to the [Developer Certificate of Origin (DCO)](CLA.md). This means you certify that:
By contributing to this project, you agree to the [Developer Certificate of Origin (DCO)](CLA.md). This means you
certify that:
- You have the right to submit your contributions
- You're not knowingly submitting code with patent or copyright issues
- Your contributions are provided under the project's license (AGPL-3.0)
This is a lightweight alternative to a Contributor License Agreement and helps ensure that all contributions can be properly incorporated into the project and potentially used in commercial applications.
This is a lightweight alternative to a Contributor License Agreement and helps ensure that all contributions can be
properly incorporated into the project and potentially used in commercial applications.
### Signing Your Commits
You can sign your commits in one of two ways:
Sign your commit:
1. **Using the `-s` or `--signoff` flag**:
```bash
git commit -s -m "Your commit message"
```
This adds a `Signed-off-by` line to your commit message, certifying that you adhere to the DCO.
**Using the `-s` or `--signoff` flag**:
2. **Configuring Git to automatically sign off**:
```bash
git config --global alias.cs 'commit -s'
```
Then use `git cs -m "Your commit message"` to commit with sign-off.
```bash
git commit -s -m "Your commit message"
```
The sign-off certifies that you have the right to submit your contribution under the project's license and verifies your agreement to the DCO.
This adds a `Signed-off-by` line to your commit message, certifying that you adhere to the DCO.
The sign-off certifies that you have the right to submit your contribution under the project's license and verifies your
agreement to the DCO.
## Code Style Guidelines
@@ -135,10 +139,10 @@ The sign-off certifies that you have the right to submit your contribution under
- **Coverage Target**: We aim for 100% test coverage for all code
- **Test Framework**: Use pytest for unit and integration tests
- **Mocking**: Use pytest-mock for mocking dependencies
- **Mocking**: Use pytest-mock for mocking dependencies only when necessary
- **Edge Cases**: Test both normal operation and edge cases
- **Database Testing**: Use in-memory SQLite for testing database operations
- **Fixtures**: Use pytest fixtures for setup and teardown
- **Fixtures**: Use async pytest fixtures for setup and teardown
## Creating Issues
@@ -156,4 +160,4 @@ All contributors must follow the [Code of Conduct](CODE_OF_CONDUCT.md).
## Thank You!
Your contributions help make Basic Memory better for everyone. We appreciate your time and effort!
Your contributions help make Basic Memory better. We appreciate your time and effort!
+2 -2
View File
@@ -8,9 +8,9 @@ COPY . .
# Install pip and build dependencies
RUN pip install --upgrade pip \
&& pip install . --no-cache-dir --ignore-installed
&& pip install . --no-cache-dir --ignore-installed
# Expose port if necessary (e.g., uv might use a port, but MCP over stdio so not needed here)
# Use the basic-memory entrypoint to run the MCP server
CMD ["basic-memory", "mcp"]
CMD ["basic-memory", "mcp"]
+55 -393
View File
@@ -1,12 +1,14 @@
# Basic Memory
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![PyPI version](https://badge.fury.io/py/basic-memory.svg)](https://badge.fury.io/py/basic-memory)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![Tests](https://github.com/basicmachines-co/basic-memory/workflows/Tests/badge.svg)](https://github.com/basicmachines-co/basic-memory/actions)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
![](https://badge.mcpx.dev?type=server 'MCP Server')
![](https://badge.mcpx.dev?type=dev 'MCP Dev')
[![smithery badge](https://smithery.ai/badge/@basicmachines-co/basic-memory)](https://smithery.ai/server/@basicmachines-co/basic-memory)
# Basic Memory
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.
@@ -14,26 +16,19 @@ enable any compatible LLM to read and write to your local knowledge base.
- Website: http://basicmachines.co
- Documentation: http://memory.basicmachines.co
![Example Gif](docs/attachments/Obsidian-CoffeeKnowledgeBase-examples-overlays.gif)
## Pick up your conversation right where you left off
Basic Memory provides persistent contextual awareness across sessions through a structured knowledge graph.
The system enables LLMs to access and reference prior conversations, track semantic relationships between concepts, and
incorporate human edits made directly to knowledge files.
- AI assistants can load context from local files in a new conversation
- Notes are saved locally as Markdown files in real time
- No project knowledge or special prompting required
https://github.com/user-attachments/assets/a55d8238-8dd0-454a-be4c-8860dbbd0ddc
## Quick Start
### Installing via Smithery
To install Basic Memory for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@basicmachines-co/basic-memory):
```bash
npx -y @smithery/cli install @basicmachines-co/basic-memory --client claude
```
### Installing Manually
```bash
# Install with uv (recommended)
uv install basic-memory
uv tool install basic-memory
# Configure Claude Desktop (edit ~/Library/Application Support/Claude/claude_desktop_config.json)
# Add this to your config:
@@ -57,33 +52,23 @@ uv install basic-memory
You can view shared context via files in `~/basic-memory` (default directory location).
You can also install the cli tools to sync files or manage projects.
### Alternative Installation via Smithery
```bash
uv tool install basic-memory
You can use [Smithery](https://smithery.ai/server/@basicmachines-co/basic-memory) to automatically configure Basic
Memory for Claude Desktop:
# create a new project in a different directory
basic-memory project add coffee ./examples/coffee
# you can set the project to the default
basic-memory project default coffee
```bash
npx -y @smithery/cli install @basicmachines-co/basic-memory --client claude
```
View available projects
This installs and configures Basic Memory without requiring manual edits to the Claude Desktop configuration file. The
Smithery server hosts the MCP server component, while your data remains stored locally as Markdown files.
```bash
basic-memory project list
Basic Memory Projects
┏━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓
┃ Name ┃ Path ┃ Default ┃ Active ┃
┡━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩
│ main │ ~/basic-memory │ ✓ │ ✓ │
│ coffee │ ~/dev/basicmachines/basic-memory/examples/coffee │ │ │
└────────┴──────────────────────────────────────────────────┴─────────┴────────┘
```
### Glama.ai
Basic Memory will write notes in Markdown format. Open you project directory in your text editor to view project files
while you have conversations with an LLM.
<a href="https://glama.ai/mcp/servers/o90kttu9ym">
<img width="380" height="200" src="https://glama.ai/mcp/servers/o90kttu9ym/badge" alt="basic-memory MCP server" />
</a>
## Why Basic Memory?
@@ -164,36 +149,13 @@ tags:
- affects [[Flavor Extraction]]
```
The note embeds semantic content and links to other topics via simple Markdown
formatting.
The note embeds semantic content and links to other topics via simple Markdown formatting.
3. You see this file on your computer in real time in the `~/$HOME/basic-memory` directory:
3. You see this file on your computer in real time in the current project directory (default `~/$HOME/basic-memory`).
```markdown
---
title: Coffee Brewing Methods
permalink: coffee-brewing-methods
type: note
---
- Realtime sync can be enabled via running `basic-memory sync --watch`
# Coffee Brewing Methods
## Observations
- [method] Pour over provides more clarity and highlights subtle flavors
- [technique] Water temperature at 205°F (96°C) extracts optimal compounds
- [principle] Freshly ground beans preserve aromatics and flavor
- [preference] Medium-light roasts work best for pour over # Added by you
## Relations
- relates_to [[Coffee Bean Origins]]
- requires [[Proper Grinding Technique]]
- affects [[Flavor Extraction]]
- pairs_with [[Breakfast Pastries]] # Added by you
```
4. In a new chat with the LLM, you can reference this knowledge:
4. In a chat with the LLM, you can reference a topic:
```
Look at `coffee-brewing-methods` for context about pour over coffee
@@ -213,15 +175,14 @@ Following relation 'requires [[Proper Grinding Technique]]':
- Impact of consistent particle size on extraction
```
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.
Each related document can lead to more context, building a rich semantic understanding of your knowledge base.
Every time the LLM writes notes,they are saved in local Markdown files that you can:
This creates a two-way flow where:
- Edit in any text editor
- Version via git
- Back up normally
- Share when you want to
- Humans write and edit Markdown files
- LLMs read and write through the MCP protocol
- Sync keeps everything consistent
- All knowledge stays in local files.
## Technical Implementation
@@ -257,7 +218,7 @@ permalink: <a uri slug>
Observations are facts about a topic.
They can be added by creating a Markdown list with a special format that can reference a `category`, `tags` using a
"#" charactor, and an optional `context`.
"#" character, and an optional `context`.
Observation Markdown format:
@@ -301,73 +262,6 @@ Examples of relations:
- documented_in [[Coffee Journal]]
```
### Complete Example
Here's a complete example of a note with frontmatter, observations, and relations:
```markdown
---
title: Pour Over Coffee Method
type: note
permalink: pour-over-coffee-method
tags:
- brewing
- coffee
- techniques
---
# Pour Over Coffee Method
This note documents the pour over brewing method and my experiences with it.
## Overview
The pour over method involves pouring hot water through coffee grounds in a filter. The water drains through the coffee
and filter into a carafe or cup.
## Observations
- [equipment] Hario V60 dripper produces clean, bright cup #gear
- [technique] Pour in concentric circles to ensure even extraction
- [ratio] 1:16 coffee-to-water ratio works best for balanced flavor
- [timing] Total brew time should be 2:30-3:00 minutes for medium roast
- [temperature] Water at 205°F (96°C) extracts optimal flavor compounds
- [grind] Medium-fine grind similar to table salt texture
- [tip] 30-45 second bloom with double the coffee weight in water
- [result] Produces a cleaner cup with more distinct flavor notes than immersion methods
## Relations
- complements [[Light Roast Beans]]
- requires [[Gooseneck Kettle]]
- contrasts_with [[French Press Method]]
- pairs_with [[Breakfast Pastries]]
- documented_in [[Brewing Journal]]
- inspired_by [[Japanese Brewing Techniques]]
- affects [[Flavor Extraction]]
- part_of [[Morning Ritual]]
```
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. 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 Desktop
Basic Memory is built using the MCP (Model Context Protocol) and works with the Claude desktop app (https://claude.ai/):
@@ -391,7 +285,8 @@ for OS X):
}
```
If you want to use a specific project (see [Multiple Projects](#multiple-projects) below), update your Claude Desktop
If you want to use a specific project (see [Multiple Projects](docs/User%20Guide.md#multiple-projects)), update your
Claude Desktop
config:
```json
@@ -426,7 +321,7 @@ basic-memory sync --watch
write_note(title, content, folder, tags) - Create or update notes
read_note(identifier, page, page_size) - Read notes by title or permalink
build_context(url, depth, timeframe) - Navigate knowledge graph via memory:// URLs
search(query, page, page_size) - Search across your knowledge base
search_notes(query, page, page_size) - Search across your knowledge base
recent_activity(type, depth, timeframe) - Find recently updated information
canvas(nodes, edges, title, folder) - Generate knowledge visualizations
```
@@ -441,263 +336,30 @@ canvas(nodes, edges, title, folder) - Generate knowledge visualizations
"What have I been working on in the past week?"
```
## Multiple Projects
## Futher info
Basic Memory supports managing multiple separate knowledge bases through projects. This feature allows you to maintain
separate knowledge graphs for different purposes (e.g., personal notes, work projects, research topics).
See the [Documentation](https://memory.basicmachines.co/) for more info, including:
### Managing Projects
```bash
# List all configured projects
basic-memory project list
# Add a new project
basic-memory project add work ~/work-basic-memory
# Set the default project
basic-memory project default work
# Remove a project (doesn't delete files)
basic-memory project remove personal
# Show current project
basic-memory project current
```
### Using Projects in Commands
All commands support the `--project` flag to specify which project to use:
```bash
# Sync a specific project
basic-memory --project=work sync
# Run MCP server for a specific project
basic-memory --project=personal mcp
```
You can also set the `BASIC_MEMORY_PROJECT` environment variable:
```bash
BASIC_MEMORY_PROJECT=work basic-memory sync
```
### Project Isolation
Each project maintains:
- Its own collection of markdown files in the specified directory
- A separate SQLite database for that project
- Complete knowledge graph isolation from other projects
## 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
- Knowledge should persist across conversations
- AI assistants should build on past context
- File formats should be human-readable and editable
- Semantic structure should emerge from natural patterns
- Knowledge graphs should be both AI and human navigable
- Systems should augment human memory, not replace it
## Importing Existing Data
Basic Memory provides CLI commands to import data from various sources, converting them into the structured Markdown
format:
### Claude.ai
First, request an export of your data from your Claude account. The data will be emailed to you in several files,
including
`conversations.json` and `projects.json`.
Import Claude.ai conversation data
```bash
basic-memory import claude conversations
```
The conversations will be turned into Markdown files and placed in the "conversations" folder by default (this can be
changed with the --folder arg).
Example:
```bash
Importing chats from conversations.json...writing to .../basic-memory
Reading chat data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
╭────────────────────────────╮
│ Import complete! │
│ │
│ Imported 307 conversations │
│ Containing 7769 messages │
╰────────────────────────────╯
```
Next, you can run the `sync` command to import the data into basic-memory
```bash
basic-memory sync
```
You can also import project data from Claude.ai
```bash
➜ basic-memory import claude projects
Importing projects from projects.json...writing to .../basic-memory/projects
Reading project data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
╭────────────────────────────────╮
│ Import complete! │
│ │
│ Imported 101 project documents │
│ Imported 32 prompt templates │
╰────────────────────────────────╯
Run 'basic-memory sync' to index the new files.
```
### OpenAI ChatGPT
```bash
➜ basic-memory import chatgpt
Importing chats from conversations.json...writing to .../basic-memory/conversations
Reading chat data... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
╭────────────────────────────╮
│ Import complete! │
│ │
│ Imported 198 conversations │
│ Containing 11777 messages │
╰────────────────────────────╯
```
### Knowledge Graph Memory Server
From the MCP Server: https://github.com/modelcontextprotocol/servers/tree/main/src/memory
```bash
➜ basic-memory import memory-json
Importing from memory.json...writing to .../basic-memory
Reading memory.json... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
Creating entities... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
╭──────────────────────╮
│ Import complete! │
│ │
│ Created 126 entities │
│ Added 252 relations │
╰──────────────────────╯
```
## Working with Your Knowledge Base
Once you've built up a knowledge base, you can interact with it in several ways:
### Command Line Interface
Basic Memory provides a powerful CLI for managing your knowledge:
```bash
# See all available commands
basic-memory --help
# Check the status of your knowledge sync
basic-memory status
# Access specific tool functionality directly
basic-memory tools
# Start a continuous sync process
basic-memory sync --watch
```
### Obsidian Integration
Basic Memory works seamlessly with [Obsidian](https://obsidian.md/), a popular knowledge management app:
1. Point Obsidian to your Basic Memory directory
2. Use standard Obsidian features like backlinks and graph view
3. See your knowledge graph visually
4. Use the canvas visualization generated by Basic Memory
### File Organization
Basic Memory is flexible about how you organize your files:
- Group by topic in folders
- Use a flat structure with descriptive filenames
- Add custom metadata in frontmatter
- Tag files for better searchability
The system will build the semantic knowledge graph regardless of your file organization preference.
## Using stdin with Basic Memory's `write_note` Tool
The `write-note` tool supports reading content from standard input (stdin), allowing for more flexible workflows when
creating or updating notes in your Basic Memory knowledge base.
### Use Cases
This feature is particularly useful for:
1. **Piping output from other commands** directly into Basic Memory notes
2. **Creating notes with multi-line content** without having to escape quotes or special characters
3. **Integrating with AI assistants** like Claude Code that can generate content and pipe it to Basic Memory
4. **Processing text data** from files or other sources
## Basic Usage
### Method 1: Using a Pipe
You can pipe content from another command into `write_note`:
```bash
# Pipe output of a command into a new note
echo "# My Note\n\nThis is a test note" | basic-memory tools write-note --title "Test Note" --folder "notes"
# Pipe output of a file into a new note
cat README.md | basic-memory tools write-note --title "Project README" --folder "documentation"
# Process text through other tools before saving as a note
cat data.txt | grep "important" | basic-memory tools write-note --title "Important Data" --folder "data"
```
### Method 2: Using Heredoc Syntax
For multi-line content, you can use heredoc syntax:
```bash
# Create a note with heredoc
cat << EOF | basic-memory tools write_note --title "Project Ideas" --folder "projects"
# Project Ideas for Q2
## AI Integration
- Improve recommendation engine
- Add semantic search to product catalog
## Infrastructure
- Migrate to Kubernetes
- Implement CI/CD pipeline
EOF
```
### Method 3: Input Redirection
You can redirect input from a file:
```bash
# Create a note from file content
basic-memory tools write-note --title "Meeting Notes" --folder "meetings" < meeting_notes.md
```
- [Complete User Guide](https://memory.basicmachines.co/docs/user-guide)
- [CLI tools](https://memory.basicmachines.co/docs/cli-reference)
- [Managing multiple Projects](https://memory.basicmachines.co/docs/cli-reference#project)
- [Importing data from OpenAI/Claude Projects](https://memory.basicmachines.co/docs/cli-reference#import)
## License
AGPL-3.0
Contributions are welcome. See the [Contributing](CONTRIBUTING.md) guide for info about setting up the project locally
and submitting PRs.
## Star History
<a href="https://www.star-history.com/#basicmachines-co/basic-memory&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=basicmachines-co/basic-memory&type=Date" />
</picture>
</a>
Built with ♥️ by Basic Machines
+13
View File
@@ -0,0 +1,13 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 0.x.x | :white_check_mark: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
If you find a vulnerability, please contact hello@basicmachines.co
-1
View File
@@ -1 +0,0 @@
{}
-31
View File
@@ -1,31 +0,0 @@
{
"file-explorer": true,
"global-search": true,
"switcher": true,
"graph": true,
"backlink": true,
"canvas": true,
"outgoing-link": true,
"tag-pane": true,
"properties": false,
"page-preview": true,
"daily-notes": true,
"templates": true,
"note-composer": true,
"command-palette": true,
"slash-command": false,
"editor-status": true,
"bookmarks": true,
"markdown-importer": false,
"zk-prefixer": false,
"random-note": false,
"outline": true,
"word-count": true,
"slides": false,
"audio-recorder": false,
"workspaces": false,
"file-recovery": true,
"publish": true,
"sync": true,
"webviewer": false
}
-6
View File
@@ -1,6 +0,0 @@
{
"siteId": "947ee055a8c6f1a57efa4afa09791e62",
"host": "publish-01.obsidian.md",
"included": [],
"excluded": []
}
+7 -5
View File
@@ -4,6 +4,8 @@ type: note
permalink: docs/ai-assistant-guide
---
> Note: This is an optional document that can be copy/pasted into the project knowledge for an LLM to provide a full description of how it can work with Basic Memory. It is provided as a helpful resource. The tools contain extensive usage description prompts with enable the LLM to understand them.
You can [download](https://github.com/basicmachines-co/basic-memory/blob/main/docs/AI%20Assistant%20Guide.md) the contents of this file from GitHub
# AI Assistant Guide for Basic Memory
This guide helps you, the AI assistant, use Basic Memory tools effectively when working with users. It covers reading, writing, and navigating knowledge through the Model Context Protocol (MCP).
@@ -51,7 +53,7 @@ content = await read_note("specs/search-design") # By path
content = await read_note("memory://specs/search") # By memory URL
# Searching for knowledge
results = await search(
results = await search_notes(
query="authentication system", # Text to search for
page=1, # Optional: Pagination
page_size=10 # Optional: Results per page
@@ -152,7 +154,7 @@ Users will interact with Basic Memory in patterns like:
Human: "What were our decisions about auth?"
You: Let me find that information for you.
[Use search() to find relevant notes]
[Use search_notes() to find relevant notes]
[Then build_context() to understand connections]
```
@@ -249,7 +251,7 @@ When creating relations, you can:
# Example workflow for creating notes with effective relations
async def create_note_with_effective_relations():
# Search for existing entities to reference
search_results = await search("travel")
search_results = await search_notes("travel")
existing_entities = [result.title for result in search_results.primary_results]
# Check if specific entities exist
@@ -321,7 +323,7 @@ Common issues to watch for:
content = await read_note("Document")
except:
# Try search instead
results = await search("Document")
results = await search_notes("Document")
if results and results.primary_results:
# Found something similar
content = await read_note(results.primary_results[0].permalink)
@@ -367,7 +369,7 @@ Common issues to watch for:
- **Create deliberate relations**: Connect each note to at least 2-3 related entities
- **Use existing entities**: Before creating a new relation, search for existing entities
- **Verify wikilinks**: When referencing `[[Entity]]`, use exact titles of existing notes
- **Check accuracy**: Use `search()` or `recent_activity()` to confirm entity titles
- **Check accuracy**: Use `search_notes()` or `recent_activity()` to confirm entity titles
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead of "relates_to")
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
+36 -9
View File
@@ -21,14 +21,13 @@ basic-memory sync
# Watch for changes
basic-memory sync --watch
# Sync specific folder
basic-memory sync path/to/folder
# Show detailed sync information
basic-memory sync --verbose
```
Options:
- `--watch`: Continuously monitor for changes
- `--verbose`: Show detailed output
- `PATH`: Optional path to sync (defaults to ~/basic-memory)
### import
@@ -43,13 +42,13 @@ basic-memory import claude projects
# ChatGPT history
basic-memory import chatgpt
# ChatGPT history
basic-memory import memory-json /path/to/memory.json
```
Options:
- `--folder PATH`: Target folder for imported content
- `--overwrite`: Replace existing files
- `--skip-existing`: Keep existing files
> **Note**: After importing, run `basic-memory sync` to index the new files.
### status
Shows system status information:
@@ -242,10 +241,12 @@ You can redirect input from a file:
basic-memory tool write-note --title "Meeting Notes" --folder "meetings" < meeting_notes.md
```
#### Integration with Claude Code
## Integration with Claude Code
This feature works well with Claude Code in the terminal:
### cli
In a Claude Code session, let Claude know he can use the basic-memory tools, then he can execute them via the cli:
```
@@ -258,6 +259,32 @@ In a Claude Code session, let Claude know he can use the basic-memory tools, the
```
### MCP
Claude code can also now use mcp tools, so it can use any of the basic-memory tool natively. To install basic-memory in Claude Code:
Run
```
claude mcp add basic-memory basic-memory mcp
```
For example:
```
➜ ~ claude mcp add basic-memory basic-memory mcp
Added stdio MCP server basic-memory with command: basic-memory mcp to project config
➜ ~ claude mcp list
basic-memory: basic-memory mcp
```
You can then use the `/mcp` command in the REPL:
```
/mcp
⎿ MCP Server Status
• basic-memory: connected
```
## Troubleshooting Common Issues
+66 -22
View File
@@ -9,15 +9,20 @@ permalink: docs/getting-started
This guide will help you install Basic Memory, configure it with Claude Desktop, and create your first knowledge notes
through conversations.
Basic Memory uses the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) to connect with LLMs. It can be used with any service that supports the MCP, but Claude Desktop works especially well.
Basic Memory uses the [Model Context Protocol](https://modelcontextprotocol.io/introduction) (MCP) to connect with LLMs.
It can be used with any service that supports the MCP, but Claude Desktop works especially well.
## Installation
### Prerequisites
The easiest way to install basic memory is via `uv`. See the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
### 1. Install Basic Memory
```bash
# Install with uv (recommended)
uv install basic-memory
# Install with uv (recommended).
uv tool install basic-memory
# Or with pip
pip install basic-memory
@@ -25,21 +30,44 @@ pip install basic-memory
> **Important**: You need to install Basic Memory using one of the commands above to use the command line tools.
Using `uv tool install` will install the basic-memory package in a standalone virtual environment. See the [UV docs](https://docs.astral.sh/uv/concepts/tools/) for more info.
### 2. Configure Claude Desktop
Claude Desktop often has trouble finding executables in your user path. Follow these steps for a reliable setup:
Edit your Claude Desktop config, located at `~/Library/Application Support/Claude/claude_desktop_config.json`:
#### Step 1: Find the absolute path to uvx
```json
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": [
"basic-memory",
"mcp"
]
}
}
}
```
**Restart Claude Desktop**. You should see Basic Memory tools available in the "tools" menu in Claude Desktop (the little hammer icon in the bottom-right corner of the chat interface). Click it to view available tools.
#### Fix Path to uv
If you get an error that says `ENOENT` , this most likely means Claude Desktop could not find your `uv` installation. Make sure that you have `uv` installed per the instructions above, then:
**Step 1: Find the absolute path to uvx**
Open Terminal and run:
```bash
which uvx
```
This will show you the full path (e.g., `/Users/yourusername/.cargo/bin/uvx`).
#### Step 2: Edit Claude Desktop Configuration
**Step 2: Edit Claude Desktop Configuration**
Edit the configuration file located at `~/Library/Application Support/Claude/claude_desktop_config.json`:
Edit the Claude Desktop config:
```json
{
@@ -57,14 +85,14 @@ Edit the configuration file located at `~/Library/Application Support/Claude/cla
Replace `/absolute/path/to/uvx` with the actual path you found in Step 1.
> **Note**: Using absolute paths is necessary because Claude Desktop cannot access binaries in your user PATH.
#### Step 3: Restart Claude Desktop
**Step 3: Restart Claude Desktop**
Close and reopen Claude Desktop for the changes to take effect.
### 3. Start the Sync Service
> Note the sync service is optional. You can run it if you want your local change to be available in Claude Desktop
Start the sync service to monitor your files for changes:
```bash
@@ -75,7 +103,7 @@ basic-memory sync
basic-memory sync --watch
```
The `--watch` flag enables automatic detection of file changes, keeping your knowledge base current.
The `--watch` flag enables automatic detection of file changes, updating your knowledge in real time.
### 4. Staying Updated
@@ -91,6 +119,21 @@ pip install --upgrade basic-memory
> **Note**: After updating, you'll need to restart Claude Desktop and your sync process for changes to take effect.
### 5. Change the default project directory
By default, Basic Memory will create a project in the `basic-memory` folder in your home directory. You can change this via the `project` [[CLI Reference#project|cli command]].
```
# Add a new project
basic-memory project add work ~/work-basic-memory
# Set the default project
basic-memory project default work
# List all configured projects
basic-memory project list
```
## Troubleshooting Installation
### Common Issues
@@ -107,35 +150,30 @@ If Claude cannot find Basic Memory tools:
#### Permission Issues
If you encounter permission errors:
1. Check that Basic Memory has access to create files in your home directory
2. Ensure Claude Desktop has permission to execute the uvx command
## Creating Your First Knowledge Note
1. **Start the sync process** in a Terminal window:
```bash
basic-memory sync --watch
```
Keep this running in the background.
1. **Open Claude Desktop** and start a new conversation.
2. **Open Claude Desktop** and start a new conversation.
3. **Have a natural conversation** about any topic:
2. **Have a natural conversation** about any topic:
```
You: "Let's talk about coffee brewing methods I've been experimenting with."
Claude: "I'd be happy to discuss coffee brewing methods..."
You: "I've found that pour over gives more flavor clarity than French press..."
```
4. **Ask Claude to create a note**:
3. **Ask Claude to create a note**:
```
You: "Could you create a note summarizing what we've discussed about coffee brewing?"
```
5. **Confirm note creation**:
4. **Confirm note creation**:
Claude will confirm when the note has been created and where it's stored.
6. **View the created file** in your `~/basic-memory` directory using any text editor or Obsidian.
5. **View the created file** in your `~/basic-memory` directory using any text editor or Obsidian.
The file structure will look similar to:
```markdown
---
@@ -153,6 +191,12 @@ If you encounter permission errors:
- relates_to [[Other Coffee Topics]]
```
5. **Start the sync process** in a Terminal window (optional):
```bash
basic-memory sync --watch
```
Keep this running in the background.
## Using Special Prompts
Basic Memory includes special prompts that help you start conversations with context from your knowledge base:
+1 -1
View File
@@ -196,7 +196,7 @@ flowchart TD
end
BMCP <-->|"write_note() read_note()"| KnowledgeFiles
BMCP <-->|"search() build_context()"| KnowledgeIndex
BMCP <-->|"search_notes() build_context()"| KnowledgeIndex
KnowledgeFiles <-.->|Sync Process| KnowledgeIndex
KnowledgeFiles <-->|Direct Editing| Editors((Text Editors & Git))
+7 -1
View File
@@ -24,7 +24,13 @@ Basic Memory connects you and AI assistants through shared knowledge:
Both you and AI assistants like Claude can read from and write to the same knowledge base, creating a continuous
learning environment where each conversation builds upon previous ones.
![[Obsidian-CoffeeKnowledgeBase-examples-overlays.gif]]
## Pick up your conversation right where you left off
- AI assistants can load context from local files in a new conversation
- Notes are saved locally as Markdown files in real time
- No project knowledge or special prompting required
![[Claude-Obsidian-Demo.mp4]]
Basic Memory uses:
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.
+128
View File
@@ -0,0 +1,128 @@
# Basic Memory Installation Guide for LLMs
This guide is specifically designed to help AI assistants like Cline install and configure Basic Memory. Follow these
steps in order.
## Installation Steps
### 1. Install Basic Memory Package
Use one of the following package managers to install:
```bash
# Install with uv (recommended)
uv tool install basic-memory
# Or with pip
pip install basic-memory
```
### 2. Configure MCP Server
Add the following to your config:
```json
{
"mcpServers": {
"basic-memory": {
"command": "uvx",
"args": [
"basic-memory",
"mcp"
]
}
}
}
```
For Claude Desktop, this file is located at:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
### 3. Start Synchronization (optional)
To synchronize files in real-time, run:
```bash
basic-memory sync --watch
```
Or for a one-time sync:
```bash
basic-memory sync
```
## Configuration Options
### Custom Directory
To use a directory other than the default `~/basic-memory`:
```bash
basic-memory project add custom-project /path/to/your/directory
basic-memory project default custom-project
```
### Multiple Projects
To manage multiple knowledge bases:
```bash
# List all projects
basic-memory project list
# Add a new project
basic-memory project add work ~/work-basic-memory
# Set default project
basic-memory project default work
```
## Importing Existing Data
### From Claude.ai
```bash
basic-memory import claude conversations path/to/conversations.json
basic-memory import claude projects path/to/projects.json
```
### From ChatGPT
```bash
basic-memory import chatgpt path/to/conversations.json
```
### From MCP Memory Server
```bash
basic-memory import memory-json path/to/memory.json
```
## Troubleshooting
If you encounter issues:
1. Check that Basic Memory is properly installed:
```bash
basic-memory --version
```
2. Verify the sync process is running:
```bash
ps aux | grep basic-memory
```
3. Check sync output for errors:
```bash
basic-memory sync --verbose
```
4. Check log output:
```bash
cat ~/.basic-memory/basic-memory.log
```
For more detailed information, refer to the [full documentation](https://memory.basicmachines.co/).
+2 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "basic-memory"
version = "0.9.0"
version = "0.11.0"
description = "Local-first knowledge management combining Zettelkasten with knowledge graphs"
readme = "README.md"
requires-python = ">=3.12.1"
@@ -40,6 +40,7 @@ Documentation = "https://github.com/basicmachines-co/basic-memory#readme"
[project.scripts]
basic-memory = "basic_memory.cli.main:app"
bm = "basic_memory.cli.main:app"
[build-system]
requires = ["hatchling"]
+2 -4
View File
@@ -7,11 +7,9 @@ startCommand:
type: object
properties: {}
description: No configuration required. This MCP server runs using the default command.
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
commandFunction: |-
(config) => ({
command: 'basic-memory',
args: ['mcp']
})
exampleConfig: {}
exampleConfig: {}
+1 -1
View File
@@ -1,3 +1,3 @@
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
__version__ = "0.9.0"
__version__ = "0.11.0"
@@ -3,9 +3,6 @@
import json
from datetime import datetime
from fastapi import APIRouter
from sqlalchemy import text
from basic_memory.config import config, config_manager
from basic_memory.deps import (
ProjectInfoRepositoryDep,
@@ -18,6 +15,8 @@ from basic_memory.schemas import (
SystemStatus,
)
from basic_memory.sync.watch_service import WATCH_STATUS_JSON
from fastapi import APIRouter
from sqlalchemy import text
router = APIRouter(prefix="/stats", tags=["statistics"])
@@ -262,7 +261,7 @@ async def get_system_status() -> SystemStatus:
watch_status_path = config.home / ".basic-memory" / WATCH_STATUS_JSON
if watch_status_path.exists():
try:
watch_status = json.loads(watch_status_path.read_text())
watch_status = json.loads(watch_status_path.read_text(encoding="utf-8"))
except Exception: # pragma: no cover
pass
+1 -2
View File
@@ -1,7 +1,7 @@
"""CLI commands for basic-memory."""
from . import status, sync, db, import_memory_json, mcp, import_claude_conversations
from . import import_claude_projects, import_chatgpt, tool, project, project_info
from . import import_claude_projects, import_chatgpt, tool, project
__all__ = [
"status",
@@ -14,5 +14,4 @@ __all__ = [
"import_chatgpt",
"tool",
"project",
"project_info",
]
+15 -2
View File
@@ -1,10 +1,13 @@
"""Database management commands."""
import asyncio
import typer
from loguru import logger
from basic_memory.alembic import migrations
from basic_memory import db
from basic_memory.cli.app import app
from basic_memory.config import config
@app.command()
@@ -14,7 +17,17 @@ def reset(
"""Reset database (drop all tables and recreate)."""
if typer.confirm("This will delete all data in your db. Are you sure?"):
logger.info("Resetting database...")
migrations.reset_database()
# Get database path
db_path = config.database_path
# Delete the database file if it exists
if db_path.exists():
db_path.unlink()
logger.info(f"Database file deleted: {db_path}")
# Create a new empty database
asyncio.run(db.run_migrations(config))
logger.info("Database reset complete")
if reindex:
# Import and run sync
@@ -7,15 +7,14 @@ from pathlib import Path
from typing import Dict, Any, List, Annotated, Set, Optional
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 import_app
from basic_memory.config import config
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
from loguru import logger
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
console = Console()
@@ -167,7 +166,7 @@ async def process_chatgpt_json(
read_task = progress.add_task("Reading chat data...", total=None)
# Read conversations
conversations = json.loads(json_path.read_text())
conversations = json.loads(json_path.read_text(encoding="utf-8"))
progress.update(read_task, total=len(conversations))
# Process each conversation
@@ -7,15 +7,14 @@ from pathlib import Path
from typing import Dict, Any, List, Annotated
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 claude_app
from basic_memory.config import config
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
from loguru import logger
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
console = Console()
@@ -124,7 +123,7 @@ async def process_conversations_json(
read_task = progress.add_task("Reading chat data...", total=None)
# Read chat data - handle array of arrays format
data = json.loads(json_path.read_text())
data = json.loads(json_path.read_text(encoding="utf-8"))
conversations = [chat for chat in data]
progress.update(read_task, total=len(conversations))
@@ -6,15 +6,14 @@ from pathlib import Path
from typing import Dict, Any, Annotated, Optional
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 claude_app
from basic_memory.config import config
from basic_memory.markdown import EntityParser, MarkdownProcessor
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter
from loguru import logger
from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
console = Console()
@@ -103,7 +102,7 @@ async def process_projects_json(
read_task = progress.add_task("Reading project data...", total=None)
# Read project data
data = json.loads(json_path.read_text())
data = json.loads(json_path.read_text(encoding="utf-8"))
progress.update(read_task, total=len(data))
# Track import counts
+168 -2
View File
@@ -1,5 +1,6 @@
"""Command module for basic-memory project management."""
import asyncio
import os
from pathlib import Path
@@ -9,6 +10,12 @@ from rich.table import Table
from basic_memory.cli.app import app
from basic_memory.config import ConfigManager, config
from basic_memory.mcp.tools.project_info import project_info
import json
from datetime import datetime
from rich.panel import Panel
from rich.tree import Tree
console = Console()
@@ -92,12 +99,22 @@ def remove_project(
def set_default_project(
name: str = typer.Argument(..., help="Name of the project to set as default"),
) -> None:
"""Set the default project."""
"""Set the default project and activate it for the current session."""
config_manager = ConfigManager()
try:
# Set the default project
config_manager.set_default_project(name)
console.print(f"[green]Project '{name}' set as default[/green]")
# Also activate it for the current session by setting the environment variable
os.environ["BASIC_MEMORY_PROJECT"] = name
# Reload configuration to apply the change
from importlib import reload
from basic_memory import config as config_module
reload(config_module)
console.print(f"[green]Project '{name}' set as default and activated[/green]")
except ValueError as e: # pragma: no cover
console.print(f"[red]Error: {e}[/red]")
raise typer.Exit(1)
@@ -117,3 +134,152 @@ def show_current_project() -> None:
except ValueError: # pragma: no cover
console.print(f"[yellow]Warning: Project '{current}' not found in configuration[/yellow]")
console.print(f"Using default project: [cyan]{config_manager.default_project}[/cyan]")
@project_app.command("info")
def display_project_info(
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Display detailed information and statistics about the current project."""
try:
# Get project info
info = asyncio.run(project_info())
if json_output:
# Convert to JSON and print
print(json.dumps(info.model_dump(), indent=2, default=str))
else:
# Create rich display
console = Console()
# Project configuration section
console.print(
Panel(
f"[bold]Project:[/bold] {info.project_name}\n"
f"[bold]Path:[/bold] {info.project_path}\n"
f"[bold]Default Project:[/bold] {info.default_project}\n",
title="📊 Basic Memory Project Info",
expand=False,
)
)
# Statistics section
stats_table = Table(title="📈 Statistics")
stats_table.add_column("Metric", style="cyan")
stats_table.add_column("Count", style="green")
stats_table.add_row("Entities", str(info.statistics.total_entities))
stats_table.add_row("Observations", str(info.statistics.total_observations))
stats_table.add_row("Relations", str(info.statistics.total_relations))
stats_table.add_row(
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
)
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
console.print(stats_table)
# Entity types
if info.statistics.entity_types:
entity_types_table = Table(title="📑 Entity Types")
entity_types_table.add_column("Type", style="blue")
entity_types_table.add_column("Count", style="green")
for entity_type, count in info.statistics.entity_types.items():
entity_types_table.add_row(entity_type, str(count))
console.print(entity_types_table)
# Most connected entities
if info.statistics.most_connected_entities:
connected_table = Table(title="🔗 Most Connected Entities")
connected_table.add_column("Title", style="blue")
connected_table.add_column("Permalink", style="cyan")
connected_table.add_column("Relations", style="green")
for entity in info.statistics.most_connected_entities:
connected_table.add_row(
entity["title"], entity["permalink"], str(entity["relation_count"])
)
console.print(connected_table)
# Recent activity
if info.activity.recently_updated:
recent_table = Table(title="🕒 Recent Activity")
recent_table.add_column("Title", style="blue")
recent_table.add_column("Type", style="cyan")
recent_table.add_column("Last Updated", style="green")
for entity in info.activity.recently_updated[:5]: # Show top 5
updated_at = (
datetime.fromisoformat(entity["updated_at"])
if isinstance(entity["updated_at"], str)
else entity["updated_at"]
)
recent_table.add_row(
entity["title"],
entity["entity_type"],
updated_at.strftime("%Y-%m-%d %H:%M"),
)
console.print(recent_table)
# System status
system_tree = Tree("🖥️ System Status")
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
system_tree.add(
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
)
# Watch status
if info.system.watch_status: # pragma: no cover
watch_branch = system_tree.add("Watch Service")
running = info.system.watch_status.get("running", False)
status_color = "green" if running else "red"
watch_branch.add(
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
)
if running:
start_time = (
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
if isinstance(info.system.watch_status.get("start_time"), str)
else info.system.watch_status.get("start_time")
)
watch_branch.add(
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
)
watch_branch.add(
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
)
watch_branch.add(
f"Errors: [{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]{info.system.watch_status.get('error_count', 0)}[/{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]"
)
else:
system_tree.add("[yellow]Watch service not running[/yellow]")
console.print(system_tree)
# Available projects
projects_table = Table(title="📁 Available Projects")
projects_table.add_column("Name", style="blue")
projects_table.add_column("Path", style="cyan")
projects_table.add_column("Default", style="green")
for name, path in info.available_projects.items():
is_default = name == info.default_project
projects_table.add_row(name, path, "" if is_default else "")
console.print(projects_table)
# Timestamp
current_time = (
datetime.fromisoformat(str(info.system.timestamp))
if isinstance(info.system.timestamp, str)
else info.system.timestamp
)
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]")
except Exception as e: # pragma: no cover
typer.echo(f"Error getting project info: {e}", err=True)
raise typer.Exit(1)
@@ -1,167 +0,0 @@
"""CLI command for project info status."""
import asyncio
import json
from datetime import datetime
import typer
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.tree import Tree
from basic_memory.cli.app import app
from basic_memory.mcp.tools.project_info import project_info
info_app = typer.Typer()
app.add_typer(info_app, name="info", help="Get information about your Basic Memory project")
@info_app.command("stats")
def display_project_info(
json_output: bool = typer.Option(False, "--json", help="Output in JSON format"),
):
"""Display detailed information and statistics about the current project."""
try:
# Get project info
info = asyncio.run(project_info())
if json_output:
# Convert to JSON and print
print(json.dumps(info.model_dump(), indent=2, default=str))
else:
# Create rich display
console = Console()
# Project configuration section
console.print(
Panel(
f"[bold]Project:[/bold] {info.project_name}\n"
f"[bold]Path:[/bold] {info.project_path}\n"
f"[bold]Default Project:[/bold] {info.default_project}\n",
title="📊 Basic Memory Project Info",
expand=False,
)
)
# Statistics section
stats_table = Table(title="📈 Statistics")
stats_table.add_column("Metric", style="cyan")
stats_table.add_column("Count", style="green")
stats_table.add_row("Entities", str(info.statistics.total_entities))
stats_table.add_row("Observations", str(info.statistics.total_observations))
stats_table.add_row("Relations", str(info.statistics.total_relations))
stats_table.add_row(
"Unresolved Relations", str(info.statistics.total_unresolved_relations)
)
stats_table.add_row("Isolated Entities", str(info.statistics.isolated_entities))
console.print(stats_table)
# Entity types
if info.statistics.entity_types:
entity_types_table = Table(title="📑 Entity Types")
entity_types_table.add_column("Type", style="blue")
entity_types_table.add_column("Count", style="green")
for entity_type, count in info.statistics.entity_types.items():
entity_types_table.add_row(entity_type, str(count))
console.print(entity_types_table)
# Most connected entities
if info.statistics.most_connected_entities:
connected_table = Table(title="🔗 Most Connected Entities")
connected_table.add_column("Title", style="blue")
connected_table.add_column("Permalink", style="cyan")
connected_table.add_column("Relations", style="green")
for entity in info.statistics.most_connected_entities:
connected_table.add_row(
entity["title"], entity["permalink"], str(entity["relation_count"])
)
console.print(connected_table)
# Recent activity
if info.activity.recently_updated:
recent_table = Table(title="🕒 Recent Activity")
recent_table.add_column("Title", style="blue")
recent_table.add_column("Type", style="cyan")
recent_table.add_column("Last Updated", style="green")
for entity in info.activity.recently_updated[:5]: # Show top 5
updated_at = (
datetime.fromisoformat(entity["updated_at"])
if isinstance(entity["updated_at"], str)
else entity["updated_at"]
)
recent_table.add_row(
entity["title"],
entity["entity_type"],
updated_at.strftime("%Y-%m-%d %H:%M"),
)
console.print(recent_table)
# System status
system_tree = Tree("🖥️ System Status")
system_tree.add(f"Basic Memory version: [bold green]{info.system.version}[/bold green]")
system_tree.add(
f"Database: [cyan]{info.system.database_path}[/cyan] ([green]{info.system.database_size}[/green])"
)
# Watch status
if info.system.watch_status: # pragma: no cover
watch_branch = system_tree.add("Watch Service")
running = info.system.watch_status.get("running", False)
status_color = "green" if running else "red"
watch_branch.add(
f"Status: [bold {status_color}]{'Running' if running else 'Stopped'}[/bold {status_color}]"
)
if running:
start_time = (
datetime.fromisoformat(info.system.watch_status.get("start_time", ""))
if isinstance(info.system.watch_status.get("start_time"), str)
else info.system.watch_status.get("start_time")
)
watch_branch.add(
f"Running since: [cyan]{start_time.strftime('%Y-%m-%d %H:%M')}[/cyan]"
)
watch_branch.add(
f"Files synced: [green]{info.system.watch_status.get('synced_files', 0)}[/green]"
)
watch_branch.add(
f"Errors: [{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]{info.system.watch_status.get('error_count', 0)}[/{'red' if info.system.watch_status.get('error_count', 0) > 0 else 'green'}]"
)
else:
system_tree.add("[yellow]Watch service not running[/yellow]")
console.print(system_tree)
# Available projects
projects_table = Table(title="📁 Available Projects")
projects_table.add_column("Name", style="blue")
projects_table.add_column("Path", style="cyan")
projects_table.add_column("Default", style="green")
for name, path in info.available_projects.items():
is_default = name == info.default_project
projects_table.add_row(name, path, "" if is_default else "")
console.print(projects_table)
# Timestamp
current_time = (
datetime.fromisoformat(str(info.system.timestamp))
if isinstance(info.system.timestamp, str)
else info.system.timestamp
)
console.print(f"\nTimestamp: [cyan]{current_time.strftime('%Y-%m-%d %H:%M:%S')}[/cyan]")
except Exception as e: # pragma: no cover
typer.echo(f"Error getting project info: {e}", err=True)
raise typer.Exit(1)
+8 -4
View File
@@ -12,7 +12,7 @@ from basic_memory.cli.app import app
from basic_memory.mcp.tools import build_context as mcp_build_context
from basic_memory.mcp.tools import read_note as mcp_read_note
from basic_memory.mcp.tools import recent_activity as mcp_recent_activity
from basic_memory.mcp.tools import search as mcp_search
from basic_memory.mcp.tools import search_notes as mcp_search
from basic_memory.mcp.tools import write_note as mcp_write_note
# Import prompts
@@ -29,7 +29,7 @@ from basic_memory.schemas.memory import MemoryUrl
from basic_memory.schemas.search import SearchQuery, SearchItemType
tool_app = typer.Typer()
app.add_typer(tool_app, name="tool", help="Direct access to MCP tools via CLI")
app.add_typer(tool_app, name="tool", help="Access to MCP tools via CLI")
@tool_app.command()
@@ -103,6 +103,7 @@ def write_note(
@tool_app.command()
def read_note(identifier: str, page: int = 1, page_size: int = 10):
"""Read a markdown note from the knowledge base."""
try:
note = asyncio.run(mcp_read_note(identifier, page, page_size))
rprint(note)
@@ -122,6 +123,7 @@ def build_context(
page_size: int = 10,
max_related: int = 10,
):
"""Get context needed to continue a discussion."""
try:
context = asyncio.run(
mcp_build_context(
@@ -154,6 +156,7 @@ def recent_activity(
page_size: int = 10,
max_related: int = 10,
):
"""Get recent activity across the knowledge base."""
try:
context = asyncio.run(
mcp_recent_activity(
@@ -177,8 +180,8 @@ def recent_activity(
raise
@tool_app.command()
def search(
@tool_app.command("search-notes")
def search_notes(
query: str,
permalink: Annotated[bool, typer.Option("--permalink", help="Search permalink values")] = False,
title: Annotated[bool, typer.Option("--title", help="Search title values")] = False,
@@ -189,6 +192,7 @@ def search(
page: int = 1,
page_size: int = 10,
):
"""Search across all content in the knowledge base."""
if permalink and title: # pragma: no cover
print("Cannot search both permalink and title")
raise typer.Abort()
+43 -9
View File
@@ -111,7 +111,7 @@ class ConfigManager:
"""Load configuration from file or create default."""
if self.config_file.exists():
try:
data = json.loads(self.config_file.read_text())
data = json.loads(self.config_file.read_text(encoding="utf-8"))
return BasicMemoryConfig(**data)
except Exception as e:
logger.error(f"Failed to load config: {e}")
@@ -214,11 +214,45 @@ user_home = Path.home()
log_dir = user_home / DATA_DIR_NAME
log_dir.mkdir(parents=True, exist_ok=True)
setup_logging(
env=config.env,
home_dir=user_home, # Use user home for logs
log_level=config.log_level,
log_file=f"{DATA_DIR_NAME}/basic-memory.log",
console=False,
)
logger.info(f"Starting Basic Memory {basic_memory.__version__} (Project: {config.project})")
def get_process_name(): # pragma: no cover
"""
get the type of process for logging
"""
import sys
if "sync" in sys.argv:
return "sync"
elif "mcp" in sys.argv:
return "mcp"
else:
return "cli"
process_name = get_process_name()
# Global flag to track if logging has been set up
_LOGGING_SETUP = False
def setup_basic_memory_logging(): # pragma: no cover
"""Set up logging for basic-memory, ensuring it only happens once."""
global _LOGGING_SETUP
if _LOGGING_SETUP:
# We can't log before logging is set up
# print("Skipping duplicate logging setup")
return
setup_logging(
env=config.env,
home_dir=user_home, # Use user home for logs
log_level=config.log_level,
log_file=f"{DATA_DIR_NAME}/basic-memory-{process_name}.log",
console=False,
)
logger.info(f"Starting Basic Memory {basic_memory.__version__} (Project: {config.project})")
_LOGGING_SETUP = True
# Set up logging
setup_basic_memory_logging()
+3 -3
View File
@@ -85,7 +85,7 @@ async def write_file_atomic(path: FilePath, content: str) -> None:
temp_path = path_obj.with_suffix(".tmp")
try:
temp_path.write_text(content)
temp_path.write_text(content, encoding="utf-8")
temp_path.replace(path_obj)
logger.debug("Wrote file atomically", path=str(path_obj), content_length=len(content))
except Exception as e: # pragma: no cover
@@ -203,7 +203,7 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
path_obj = Path(path) if isinstance(path, str) else path
# Read current content
content = path_obj.read_text()
content = path_obj.read_text(encoding="utf-8")
# Parse current frontmatter
current_fm = {}
@@ -215,7 +215,7 @@ async def update_frontmatter(path: FilePath, updates: Dict[str, Any]) -> str:
new_fm = {**current_fm, **updates}
# Write new file with updated frontmatter
yaml_fm = yaml.dump(new_fm, sort_keys=False)
yaml_fm = yaml.dump(new_fm, sort_keys=False, allow_unicode=True)
final_content = f"---\n{yaml_fm}---\n\n{content.strip()}"
logger.debug("Updating frontmatter", path=str(path_obj), update_keys=list(updates.keys()))
+13 -10
View File
@@ -4,21 +4,22 @@ Uses markdown-it with plugins to parse structured data from markdown content.
"""
from dataclasses import dataclass, field
from pathlib import Path
from datetime import datetime
from pathlib import Path
from typing import Any, Optional
import dateparser
from markdown_it import MarkdownIt
import dateparser
import frontmatter
from markdown_it import MarkdownIt
from basic_memory.markdown.plugins import observation_plugin, relation_plugin
from basic_memory.markdown.schemas import (
EntityMarkdown,
EntityFrontmatter,
EntityMarkdown,
Observation,
Relation,
)
from basic_memory.utils import parse_tags
md = MarkdownIt().use(observation_plugin).use(relation_plugin)
@@ -56,11 +57,11 @@ def parse(content: str) -> EntityContent:
)
def parse_tags(tags: Any) -> list[str]:
"""Parse tags into list of strings."""
if isinstance(tags, (list, tuple)):
return [str(t).strip() for t in tags if str(t).strip()]
return [t.strip() for t in tags.split(",") if t.strip()]
# def parse_tags(tags: Any) -> list[str]:
# """Parse tags into list of strings."""
# if isinstance(tags, (list, tuple)):
# return [str(t).strip() for t in tags if str(t).strip()]
# return [t.strip() for t in tags.split(",") if t.strip()]
class EntityParser:
@@ -101,7 +102,9 @@ class EntityParser:
metadata = post.metadata
metadata["title"] = post.metadata.get("title", absolute_path.name)
metadata["type"] = post.metadata.get("type", "note")
metadata["tags"] = parse_tags(post.metadata.get("tags", []))
tags = parse_tags(post.metadata.get("tags", [])) # pyright: ignore
if tags:
metadata["tags"] = tags
# frontmatter
entity_frontmatter = EntityFrontmatter(
@@ -83,7 +83,7 @@ class MarkdownProcessor:
"""
# Dirty check if needed
if expected_checksum is not None:
current_content = path.read_text()
current_content = path.read_text(encoding="utf-8")
current_checksum = await file_utils.compute_checksum(current_content)
if current_checksum != expected_checksum:
raise DirtyFileError(f"File {path} has been modified")
+1 -1
View File
@@ -42,7 +42,7 @@ class EntityFrontmatter(BaseModel):
@property
def tags(self) -> List[str]:
return self.metadata.get("tags") if self.metadata else [] # pyright: ignore
return self.metadata.get("tags") if self.metadata else None # pyright: ignore
@property
def title(self) -> str:
@@ -1,8 +1,7 @@
from pathlib import Path
from loguru import logger
from basic_memory.mcp.server import mcp
from loguru import logger
@mcp.resource(
@@ -20,7 +19,7 @@ def ai_assistant_guide() -> str:
A focused guide on Basic Memory usage.
"""
logger.info("Loading AI assistant guide resource")
guide_doc = Path(__file__).parent.parent.parent.parent.parent / "static" / "ai_assistant_guide.md"
content = guide_doc.read_text()
guide_doc = Path(__file__).parent.parent / "resources" / "ai_assistant_guide.md"
content = guide_doc.read_text(encoding="utf-8")
logger.info(f"Loaded AI assistant guide ({len(content)} chars)")
return content
@@ -14,7 +14,7 @@ from basic_memory.mcp.prompts.utils import format_prompt_context, PromptContext,
from basic_memory.mcp.server import mcp
from basic_memory.mcp.tools.build_context import build_context
from basic_memory.mcp.tools.recent_activity import recent_activity
from basic_memory.mcp.tools.search import search
from basic_memory.mcp.tools.search import search_notes
from basic_memory.schemas.base import TimeFrame
from basic_memory.schemas.memory import GraphContext
from basic_memory.schemas.search import SearchQuery, SearchItemType
@@ -47,7 +47,7 @@ async def continue_conversation(
# If topic provided, search for it
if topic:
search_results = await search(
search_results = await search_notes(
SearchQuery(text=topic, after_date=timeframe, types=[SearchItemType.ENTITY])
)
@@ -93,7 +93,7 @@ async def continue_conversation(
## Next Steps
You can:
- Explore more with: `search({{"text": "{topic}"}})`
- Explore more with: `search_notes({{"text": "{topic}"}})`
- See what's changed: `recent_activity(timeframe="{timeframe or "7d"}")`
- **Record new learnings or decisions from this conversation:** `write_note(title="[Create a meaningful title]", content="[Content with observations and relations]")`
+4 -4
View File
@@ -10,7 +10,7 @@ from loguru import logger
from pydantic import Field
from basic_memory.mcp.server import mcp
from basic_memory.mcp.tools.search import search as search_tool
from basic_memory.mcp.tools.search import search_notes as search_tool
from basic_memory.schemas.base import TimeFrame
from basic_memory.schemas.search import SearchQuery, SearchResponse
@@ -144,9 +144,9 @@ def format_search_results(
## Next Steps
You can:
- Refine your search: `search("{query} AND additional_term")`
- Exclude terms: `search("{query} NOT exclude_term")`
- View more results: `search("{query}", after_date=None)`
- Refine your search: `search_notes("{query} AND additional_term")`
- Exclude terms: `search_notes("{query} NOT exclude_term")`
- View more results: `search_notes("{query}", after_date=None)`
- Check recent activity: `recent_activity()`
## Synthesize and Capture Knowledge
@@ -0,0 +1,413 @@
# AI Assistant Guide for Basic Memory
This guide helps AIs use Basic Memory tools effectively when working with users. It covers reading, writing, and
navigating knowledge through the Model Context Protocol (MCP).
## Overview
Basic Memory allows you and users to record context in local Markdown files, building a rich knowledge base through
natural conversations. The system automatically creates a semantic knowledge graph from simple text patterns.
- **Local-First**: All data is stored in plain text files on the user's computer
- **Real-Time**: Users see content updates immediately
- **Bi-Directional**: Both you and users can read and edit notes
- **Semantic**: Simple patterns create a structured knowledge graph
- **Persistent**: Knowledge persists across sessions and conversations
## The Importance of the Knowledge Graph
**Basic Memory's value comes from connections between notes, not just the notes themselves.**
When writing notes, your primary goal should be creating a rich, interconnected knowledge graph:
1. **Increase Semantic Density**: Add multiple observations and relations to each note
2. **Use Accurate References**: Aim to reference existing entities by their exact titles
3. **Create Forward References**: Feel free to reference entities that don't exist yet - Basic Memory will resolve these
when they're created later
4. **Create Bidirectional Links**: When appropriate, connect entities from both directions
5. **Use Meaningful Categories**: Add semantic context with appropriate observation categories
6. **Choose Precise Relations**: Use specific relation types that convey meaning
Remember: A knowledge graph with 10 heavily connected notes is more valuable than 20 isolated notes. Your job is to help
build these connections!
## Core Tools Reference
```python
# Writing knowledge - THE MOST IMPORTANT TOOL!
response = await write_note(
title="Search Design", # Required: Note title
content="# Search Design\n...", # Required: Note content
folder="specs", # Optional: Folder to save in
tags=["search", "design"], # Optional: Tags for categorization
verbose=True # Optional: Get parsing details
)
# Reading knowledge
content = await read_note("Search Design") # By title
content = await read_note("specs/search-design") # By path
content = await read_note("memory://specs/search") # By memory URL
# Searching for knowledge
results = await search_notes(
query="authentication system", # Text to search for
page=1, # Optional: Pagination
page_size=10 # Optional: Results per page
)
# Building context from the knowledge graph
context = await build_context(
url="memory://specs/search", # Starting point
depth=2, # Optional: How many hops to follow
timeframe="1 month" # Optional: Recent timeframe
)
# Checking recent changes
activity = await recent_activity(
type="all", # Optional: Entity types to include
depth=1, # Optional: Related items to include
timeframe="1 week" # Optional: Time window
)
# Creating a knowledge visualization
canvas_result = await canvas(
nodes=[{"id": "note1", "label": "Search Design"}], # Nodes to display
edges=[{"from": "note1", "to": "note2"}], # Connections
title="Project Overview", # Canvas title
folder="diagrams" # Storage location
)
```
## memory:// URLs Explained
Basic Memory uses a special URL format to reference entities in the knowledge graph:
- `memory://title` - Reference by title
- `memory://folder/title` - Reference by folder and title
- `memory://permalink` - Reference by permalink
- `memory://path/relation_type/*` - Follow all relations of a specific type
- `memory://path/*/target` - Find all entities with relations to target
## Semantic Markdown Format
Knowledge is encoded in standard markdown using simple patterns:
**Observations** - Facts about an entity:
```markdown
- [category] This is an observation #tag1 #tag2 (optional context)
```
**Relations** - Links between entities:
```markdown
- relation_type [[Target Entity]] (optional context)
```
**Common Categories & Relation Types:**
- Categories: `[idea]`, `[decision]`, `[question]`, `[fact]`, `[requirement]`, `[technique]`, `[recipe]`, `[preference]`
- Relations: `relates_to`, `implements`, `requires`, `extends`, `part_of`, `pairs_with`, `inspired_by`,
`originated_from`
## When to Record Context
**Always consider recording context when**:
1. Users make decisions or reach conclusions
2. Important information emerges during conversation
3. Multiple related topics are discussed
4. The conversation contains information that might be useful later
5. Plans, tasks, or action items are mentioned
**Protocol for recording context**:
1. Identify valuable information in the conversation
2. Ask the user: "Would you like me to record our discussion about [topic] in Basic Memory?"
3. If they agree, use `write_note` to capture the information
4. If they decline, continue without recording
5. Let the user know when information has been recorded: "I've saved our discussion about [topic] to Basic Memory."
## Understanding User Interactions
Users will interact with Basic Memory in patterns like:
1. **Creating knowledge**:
```
Human: "Let's write up what we discussed about search."
You: I'll create a note capturing our discussion about the search functionality.
[Use write_note() to record the conversation details]
```
2. **Referencing existing knowledge**:
```
Human: "Take a look at memory://specs/search"
You: I'll examine that information.
[Use build_context() to gather related information]
[Then read_note() to access specific content]
```
3. **Finding information**:
```
Human: "What were our decisions about auth?"
You: Let me find that information for you.
[Use search_notes() to find relevant notes]
[Then build_context() to understand connections]
```
## Key Things to Remember
1. **Files are Truth**
- All knowledge lives in local files on the user's computer
- Users can edit files outside your interaction
- Changes need to be synced by the user (usually automatic)
- Always verify information is current with `recent_activity()`
2. **Building Context Effectively**
- Start with specific entities
- Follow meaningful relations
- Check recent changes
- Build context incrementally
- Combine related information
3. **Writing Knowledge Wisely**
- Using the same title+folder will overwrite existing notes
- Structure content with clear headings and sections
- Use semantic markup for observations and relations
- Keep files organized in logical folders
## Common Knowledge Patterns
### Capturing Decisions
```markdown
# Coffee Brewing Methods
## Context
I've experimented with various brewing methods including French press, pour over, and espresso.
## Decision
Pour over is my preferred method for light to medium roasts because it highlights subtle flavors and offers more control
over the extraction.
## Observations
- [technique] Blooming the coffee grounds for 30 seconds improves extraction #brewing
- [preference] Water temperature between 195-205°F works best #temperature
- [equipment] Gooseneck kettle provides better control of water flow #tools
## Relations
- pairs_with [[Light Roast Beans]]
- contrasts_with [[French Press Method]]
- requires [[Proper Grinding Technique]]
```
### Recording Project Structure
```markdown
# Garden Planning
## Overview
This document outlines the garden layout and planting strategy for this season.
## Observations
- [structure] Raised beds in south corner for sun exposure #layout
- [structure] Drip irrigation system installed for efficiency #watering
- [pattern] Companion planting used to deter pests naturally #technique
## Relations
- contains [[Vegetable Section]]
- contains [[Herb Garden]]
- implements [[Organic Gardening Principles]]
```
### Technical Discussions
```markdown
# Recipe Improvement Discussion
## Key Points
Discussed strategies for improving the chocolate chip cookie recipe.
## Observations
- [issue] Cookies spread too thin when baked at 350°F #texture
- [solution] Chilling dough for 24 hours improves flavor and reduces spreading #technique
- [decision] Will use brown butter instead of regular butter #flavor
## Relations
- improves [[Basic Cookie Recipe]]
- inspired_by [[Bakery-Style Cookies]]
- pairs_with [[Homemade Ice Cream]]
```
### Creating Effective Relations
When creating relations, you can:
1. Reference existing entities by their exact title
2. Create forward references to entities that don't exist yet
```python
# Example workflow for creating notes with effective relations
async def create_note_with_effective_relations():
# Search for existing entities to reference
search_results = await search_notes("travel")
existing_entities = [result.title for result in search_results.primary_results]
# Check if specific entities exist
packing_tips_exists = "Packing Tips" in existing_entities
japan_travel_exists = "Japan Travel Guide" in existing_entities
# Prepare relations section - include both existing and forward references
relations_section = "## Relations\n"
# Existing reference - exact match to known entity
if packing_tips_exists:
relations_section += "- references [[Packing Tips]]\n"
else:
# Forward reference - will be linked when that entity is created later
relations_section += "- references [[Packing Tips]]\n"
# Another possible reference
if japan_travel_exists:
relations_section += "- part_of [[Japan Travel Guide]]\n"
# You can also check recently modified notes to reference them
recent = await recent_activity(timeframe="1 week")
recent_titles = [item.title for item in recent.primary_results]
if "Transportation Options" in recent_titles:
relations_section += "- relates_to [[Transportation Options]]\n"
# Always include meaningful forward references, even if they don't exist yet
relations_section += "- located_in [[Tokyo]]\n"
relations_section += "- visited_during [[Spring 2023 Trip]]\n"
# Now create the note with both verified and forward relations
content = f"""# Tokyo Neighborhood Guide
## Overview
Details about different Tokyo neighborhoods and their unique characteristics.
## Observations
- [area] Shibuya is a busy shopping district #shopping
- [transportation] Yamanote Line connects major neighborhoods #transit
- [recommendation] Visit Shimokitazawa for vintage shopping #unique
- [tip] Get a Suica card for easy train travel #convenience
{relations_section}
"""
result = await write_note(
title="Tokyo Neighborhood Guide",
content=content,
verbose=True
)
# You can check which relations were resolved and which are forward references
if result and 'relations' in result:
resolved = [r['to_name'] for r in result['relations'] if r.get('target_id')]
forward_refs = [r['to_name'] for r in result['relations'] if not r.get('target_id')]
print(f"Resolved relations: {resolved}")
print(f"Forward references that will be resolved later: {forward_refs}")
```
## Error Handling
Common issues to watch for:
1. **Missing Content**
```python
try:
content = await read_note("Document")
except:
# Try search instead
results = await search_notes("Document")
if results and results.primary_results:
# Found something similar
content = await read_note(results.primary_results[0].permalink)
```
2. **Forward References (Unresolved Relations)**
```python
response = await write_note(..., verbose=True)
# Check for forward references (unresolved relations)
forward_refs = []
for relation in response.get('relations', []):
if not relation.get('target_id'):
forward_refs.append(relation.get('to_name'))
if forward_refs:
# This is a feature, not an error! Inform the user about forward references
print(f"Note created with forward references to: {forward_refs}")
print("These will be automatically linked when those notes are created.")
# Optionally suggest creating those entities now
print("Would you like me to create any of these notes now to complete the connections?")
```
3. **Sync Issues**
```python
# If information seems outdated
activity = await recent_activity(timeframe="1 hour")
if not activity or not activity.primary_results:
print("It seems there haven't been recent updates. You might need to run 'basic-memory sync'.")
```
## Best Practices
1. **Proactively Record Context**
- Offer to capture important discussions
- Record decisions, rationales, and conclusions
- Link to related topics
- Ask for permission first: "Would you like me to save our discussion about [topic]?"
- Confirm when complete: "I've saved our discussion to Basic Memory"
2. **Create a Rich Semantic Graph**
- **Add meaningful observations**: Include at least 3-5 categorized observations in each note
- **Create deliberate relations**: Connect each note to at least 2-3 related entities
- **Use existing entities**: Before creating a new relation, search for existing entities
- **Verify wikilinks**: When referencing `[[Entity]]`, use exact titles of existing notes
- **Check accuracy**: Use `search_notes()` or `recent_activity()` to confirm entity titles
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead
of "relates_to")
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
3. **Structure Content Thoughtfully**
- Use clear, descriptive titles
- Organize with logical sections (Context, Decision, Implementation, etc.)
- Include relevant context and background
- Add semantic observations with appropriate categories
- Use a consistent format for similar types of notes
- Balance detail with conciseness
4. **Navigate Knowledge Effectively**
- Start with specific searches
- Follow relation paths
- Combine information from multiple sources
- Verify information is current
- Build a complete picture before responding
5. **Help Users Maintain Their Knowledge**
- Suggest organizing related topics
- Identify potential duplicates
- Recommend adding relations between topics
- Offer to create summaries of scattered information
- Suggest potential missing relations: "I notice this might relate to [topic], would you like me to add that
connection?"
Built with ♥️ b
y Basic Machines
+2 -2
View File
@@ -12,7 +12,7 @@ from basic_memory.mcp.tools.build_context import build_context
from basic_memory.mcp.tools.recent_activity import recent_activity
from basic_memory.mcp.tools.read_note import read_note
from basic_memory.mcp.tools.write_note import write_note
from basic_memory.mcp.tools.search import search
from basic_memory.mcp.tools.search import search_notes
from basic_memory.mcp.tools.canvas import canvas
__all__ = [
@@ -22,6 +22,6 @@ __all__ = [
"read_content",
"read_note",
"recent_activity",
"search",
"search_notes",
"write_note",
]
+3 -3
View File
@@ -6,7 +6,7 @@ from loguru import logger
from basic_memory.mcp.async_client import client
from basic_memory.mcp.server import mcp
from basic_memory.mcp.tools.search import search
from basic_memory.mcp.tools.search import search_notes
from basic_memory.mcp.tools.utils import call_get
from basic_memory.schemas.memory import memory_url_path
from basic_memory.schemas.search import SearchQuery
@@ -63,7 +63,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
# Fallback 1: Try title search via API
logger.info(f"Search title for: {identifier}")
title_results = await search(SearchQuery(title=identifier))
title_results = await search_notes(SearchQuery(title=identifier))
if title_results and title_results.results:
result = title_results.results[0] # Get the first/best match
@@ -87,7 +87,7 @@ async def read_note(identifier: str, page: int = 1, page_size: int = 10) -> str:
# Fallback 2: Text search as a last resort
logger.info(f"Title search failed, trying text search for: {identifier}")
text_results = await search(SearchQuery(text=identifier))
text_results = await search_notes(SearchQuery(text=identifier))
# We didn't find a direct match, construct a helpful error message
if not text_results or not text_results.results:
+11 -11
View File
@@ -9,10 +9,10 @@ from basic_memory.mcp.async_client import client
@mcp.tool(
description="Search across all content in basic-memory, including documents and entities",
description="Search across all content in the knowledge base.",
)
async def search(query: SearchQuery, page: int = 1, page_size: int = 10) -> SearchResponse:
"""Search across all content in basic-memory.
async def search_notes(query: SearchQuery, page: int = 1, page_size: int = 10) -> SearchResponse:
"""Search across all content in the knowledge base.
This tool searches the knowledge base using full-text search, pattern matching,
or exact permalink lookup. It supports filtering by content type, entity type,
@@ -36,34 +36,34 @@ async def search(query: SearchQuery, page: int = 1, page_size: int = 10) -> Sear
Examples:
# Basic text search
results = await search(SearchQuery(text="project planning"))
results = await search_notes(SearchQuery(text="project planning"))
# Boolean AND search (both terms must be present)
results = await search(SearchQuery(text="project AND planning"))
results = await search_notes(SearchQuery(text="project AND planning"))
# Boolean OR search (either term can be present)
results = await search(SearchQuery(text="project OR meeting"))
results = await search_notes(SearchQuery(text="project OR meeting"))
# Boolean NOT search (exclude terms)
results = await search(SearchQuery(text="project NOT meeting"))
results = await search_notes(SearchQuery(text="project NOT meeting"))
# Boolean search with grouping
results = await search(SearchQuery(text="(project OR planning) AND notes"))
results = await search_notes(SearchQuery(text="(project OR planning) AND notes"))
# Search with type filter
results = await search(SearchQuery(
results = await search_notes(SearchQuery(
text="meeting notes",
types=["entity"],
))
# Search for recent content
results = await search(SearchQuery(
results = await search_notes(SearchQuery(
text="bug report",
after_date="1 week"
))
# Pattern matching on permalinks
results = await search(SearchQuery(
results = await search_notes(SearchQuery(
permalink_match="docs/meeting-*"
))
"""
+17 -7
View File
@@ -1,6 +1,6 @@
"""Write note tool for Basic Memory MCP server."""
from typing import Optional, List
from typing import List, Union
from loguru import logger
@@ -9,6 +9,13 @@ from basic_memory.mcp.server import mcp
from basic_memory.mcp.tools.utils import call_put
from basic_memory.schemas import EntityResponse
from basic_memory.schemas.base import Entity
from basic_memory.utils import parse_tags
# Define TagType as a Union that can accept either a string or a list of strings or None
TagType = Union[List[str], str, None]
# Define TagType as a Union that can accept either a string or a list of strings or None
TagType = Union[List[str], str, None]
@mcp.tool(
@@ -18,7 +25,7 @@ async def write_note(
title: str,
content: str,
folder: str,
tags: Optional[List[str]] = None,
tags=None, # Remove type hint completely to avoid schema issues
) -> str:
"""Write a markdown note to the knowledge base.
@@ -40,13 +47,14 @@ async def write_note(
Examples:
`- depends_on [[Content Parser]] (Need for semantic extraction)`
`- implements [[Search Spec]] (Initial implementation)`
`- This feature extends [[Base Design]] and uses [[Core Utils]]`
`- This feature extends [[Base Design]] andst uses [[Core Utils]]`
Args:
title: The title of the note
content: Markdown content for the note, can include observations and relations
folder: the folder where the file should be saved
tags: Optional list of tags to categorize the note
tags: Tags to categorize the note. Can be a list of strings, a comma-separated string, or None.
Note: If passing from external MCP clients, use a string format (e.g. "tag1,tag2,tag3")
Returns:
A markdown formatted summary of the semantic content, including:
@@ -58,8 +66,10 @@ async def write_note(
"""
logger.info("MCP tool call", tool="write_note", folder=folder, title=title, tags=tags)
# Process tags using the helper function
tag_list = parse_tags(tags)
# Create the entity request
metadata = {"tags": [f"#{tag}" for tag in tags]} if tags else None
metadata = {"tags": [f"#{tag}" for tag in tag_list]} if tag_list else None
entity = Entity(
title=title,
folder=folder,
@@ -105,8 +115,8 @@ async def write_note(
summary.append(f"- Unresolved: {unresolved}")
summary.append("\nUnresolved relations will be retried on next sync.")
if tags:
summary.append(f"\n## Tags\n- {', '.join(tags)}")
if tag_list:
summary.append(f"\n## Tags\n- {', '.join(tag_list)}")
# Log the response with structured data
logger.info(
+11 -1
View File
@@ -141,10 +141,20 @@ class EntityService(BaseService[EntityModel]):
# Convert file path string to Path
file_path = Path(entity.file_path)
# Read existing frontmatter from the file if it exists
existing_markdown = await self.entity_parser.parse_file(file_path)
# Create post with new content from schema
post = await schema_to_markdown(schema)
# Merge new metadata with existing metadata
existing_markdown.frontmatter.metadata.update(post.metadata)
# Create a new post with merged metadata
merged_post = frontmatter.Post(post.content, **existing_markdown.frontmatter.metadata)
# write file
final_content = frontmatter.dumps(post, sort_keys=False)
final_content = frontmatter.dumps(merged_post, sort_keys=False)
checksum = await self.file_service.write_file(file_path, final_content)
# parse entity from file
+3 -5
View File
@@ -5,8 +5,6 @@ from os import stat_result
from pathlib import Path
from typing import Any, Dict, Tuple, Union
from loguru import logger
from basic_memory import file_utils
from basic_memory.file_utils import FileError
from basic_memory.markdown.markdown_processor import MarkdownProcessor
@@ -14,6 +12,7 @@ from basic_memory.models import Entity as EntityModel
from basic_memory.schemas import Entity as EntitySchema
from basic_memory.services.exceptions import FileOperationError
from basic_memory.utils import FilePath
from loguru import logger
class FileService:
@@ -171,8 +170,7 @@ class FileService:
try:
logger.debug("Reading file", operation="read_file", path=str(full_path))
content = full_path.read_text()
content = full_path.read_text(encoding="utf-8")
checksum = await file_utils.compute_checksum(content)
logger.debug(
@@ -236,7 +234,7 @@ class FileService:
try:
if self.is_markdown(path):
# read str
content = full_path.read_text()
content = full_path.read_text(encoding="utf-8")
else:
# read bytes
content = full_path.read_bytes()
+22 -12
View File
@@ -5,16 +5,15 @@ from datetime import datetime
from pathlib import Path
from typing import List, Optional, Set
from basic_memory.config import ProjectConfig
from basic_memory.services.file_service import FileService
from basic_memory.sync.sync_service import SyncService
from loguru import logger
from pydantic import BaseModel
from rich.console import Console
from watchfiles import awatch
from watchfiles.main import FileChange, Change
from basic_memory.config import ProjectConfig
from basic_memory.services.file_service import FileService
from basic_memory.sync.sync_service import SyncService
WATCH_STATUS_JSON = "watch-status.json"
@@ -138,6 +137,10 @@ class WatchService:
if part.startswith("."):
return False
# Skip temp files used in atomic operations
if path.endswith(".tmp"):
return False
return True
async def write_status(self):
@@ -161,6 +164,11 @@ class WatchService:
for change, path in changes:
# convert to relative path
relative_path = str(Path(path).relative_to(directory))
# Skip .tmp files - they're temporary and shouldn't be synced
if relative_path.endswith(".tmp"):
continue
if change == Change.added:
adds.append(relative_path)
elif change == Change.deleted:
@@ -184,8 +192,8 @@ class WatchService:
# We don't need to process directories, only the files inside them
# This prevents errors when trying to compute checksums or read directories as files
added_full_path = directory / added_path
if added_full_path.is_dir():
logger.debug("Skipping directory for move detection", path=added_path)
if not added_full_path.exists() or added_full_path.is_dir():
logger.debug("Skipping non-existent or directory path", path=added_path)
processed.add(added_path)
continue
@@ -247,10 +255,12 @@ class WatchService:
if path not in processed:
# Skip directories - only process files
full_path = directory / path
if full_path.is_dir(): # pragma: no cover
logger.debug("Skipping directory", path=path)
processed.add(path)
continue
if not full_path.exists() or full_path.is_dir():
logger.debug(
"Skipping non-existent or directory path", path=path
) # pragma: no cover
processed.add(path) # pragma: no cover
continue # pragma: no cover
logger.debug("Processing new file", path=path)
entity, checksum = await self.sync_service.sync_file(path, new=True)
@@ -281,8 +291,8 @@ class WatchService:
if path not in processed:
# Skip directories - only process files
full_path = directory / path
if full_path.is_dir():
logger.debug("Skipping directory", path=path)
if not full_path.exists() or full_path.is_dir():
logger.debug("Skipping non-existent or directory path", path=path)
processed.add(path)
continue
+27 -1
View File
@@ -6,7 +6,7 @@ import logging
import re
import sys
from pathlib import Path
from typing import Optional, Protocol, Union, runtime_checkable
from typing import Optional, Protocol, Union, runtime_checkable, List
from loguru import logger
from unidecode import unidecode
@@ -128,3 +128,29 @@ def setup_logging(
# Set log levels for noisy loggers
for logger_name, level in noisy_loggers.items():
logging.getLogger(logger_name).setLevel(level)
def parse_tags(tags: Union[List[str], str, None]) -> List[str]:
"""Parse tags from various input formats into a consistent list.
Args:
tags: Can be a list of strings, a comma-separated string, or None
Returns:
A list of tag strings, or an empty list if no tags
"""
if tags is None:
return []
if isinstance(tags, list):
return tags
if isinstance(tags, str):
return [tag.strip() for tag in tags.split(",") if tag.strip()]
# For any other type, try to convert to string and parse
try: # pragma: no cover
return parse_tags(str(tags))
except (ValueError, TypeError): # pragma: no cover
logger.warning(f"Couldn't parse tags from input of type {type(tags)}: {tags}")
return []
+5 -5
View File
@@ -49,7 +49,7 @@ content = await read_note("specs/search-design") # By path
content = await read_note("memory://specs/search") # By memory URL
# Searching for knowledge
results = await search(
results = await search_notes(
query="authentication system", # Text to search for
page=1, # Optional: Pagination
page_size=10 # Optional: Results per page
@@ -154,7 +154,7 @@ Users will interact with Basic Memory in patterns like:
Human: "What were our decisions about auth?"
You: Let me find that information for you.
[Use search() to find relevant notes]
[Use search_notes() to find relevant notes]
[Then build_context() to understand connections]
```
@@ -263,7 +263,7 @@ When creating relations, you can:
# Example workflow for creating notes with effective relations
async def create_note_with_effective_relations():
# Search for existing entities to reference
search_results = await search("travel")
search_results = await search_notes("travel")
existing_entities = [result.title for result in search_results.primary_results]
# Check if specific entities exist
@@ -335,7 +335,7 @@ Common issues to watch for:
content = await read_note("Document")
except:
# Try search instead
results = await search("Document")
results = await search_notes("Document")
if results and results.primary_results:
# Found something similar
content = await read_note(results.primary_results[0].permalink)
@@ -381,7 +381,7 @@ Common issues to watch for:
- **Create deliberate relations**: Connect each note to at least 2-3 related entities
- **Use existing entities**: Before creating a new relation, search for existing entities
- **Verify wikilinks**: When referencing `[[Entity]]`, use exact titles of existing notes
- **Check accuracy**: Use `search()` or `recent_activity()` to confirm entity titles
- **Check accuracy**: Use `search_notes()` or `recent_activity()` to confirm entity titles
- **Use precise relation types**: Choose specific relation types that convey meaning (e.g., "implements" instead
of "relates_to")
- **Consider bidirectional relations**: When appropriate, create inverse relations in both entities
+2 -2
View File
@@ -211,7 +211,7 @@ def test_search_basic(cli_env, setup_test_note):
"""Test basic search command."""
result = runner.invoke(
tool_app,
["search", "test observation"],
["search-notes", "test observation"],
)
assert result.exit_code == 0
@@ -235,7 +235,7 @@ def test_search_permalink(cli_env, setup_test_note):
result = runner.invoke(
tool_app,
["search", permalink, "--permalink"],
["search-notes", permalink, "--permalink"],
)
assert result.exit_code == 0
+52 -2
View File
@@ -15,6 +15,13 @@ from basic_memory.config import ConfigManager, DATA_DIR_NAME, CONFIG_FILE_NAME
@pytest.fixture
def temp_home(monkeypatch):
"""Create a temporary directory for testing."""
# Save the original environment variable if it exists
original_env = os.environ.get("BASIC_MEMORY_PROJECT")
# Clear environment variable for clean test
if "BASIC_MEMORY_PROJECT" in os.environ:
del os.environ["BASIC_MEMORY_PROJECT"]
with TemporaryDirectory() as tempdir:
temp_home = Path(tempdir)
monkeypatch.setattr(Path, "home", lambda: temp_home)
@@ -25,6 +32,12 @@ def temp_home(monkeypatch):
yield temp_home
# Cleanup: restore original environment variable if it existed
if original_env is not None:
os.environ["BASIC_MEMORY_PROJECT"] = original_env
elif "BASIC_MEMORY_PROJECT" in os.environ:
del os.environ["BASIC_MEMORY_PROJECT"]
@pytest.fixture
def cli_runner():
@@ -123,17 +136,33 @@ def test_project_default(cli_runner, temp_home):
# Set as default
result = cli_runner.invoke(app, ["project", "default", "test"])
assert result.exit_code == 0
assert "Project 'test' set as default" in result.stdout
assert "Project 'test' set as default and activated" in result.stdout
# Verify default was set
config_manager = ConfigManager()
assert config_manager.default_project == "test"
# Extra verification: check if the environment variable was set
assert os.environ.get("BASIC_MEMORY_PROJECT") == "test"
def test_project_current(cli_runner, temp_home):
"""Test showing the current project."""
# Create a bare-bones config.json with main as the default project
config_file = temp_home / DATA_DIR_NAME / CONFIG_FILE_NAME
config_data = {
"projects": {
"main": str(temp_home / "basic-memory"),
},
"default_project": "main",
}
config_file.write_text(json.dumps(config_data))
# Set as default
# Create the main project directory
main_dir = temp_home / "basic-memory"
main_dir.mkdir(parents=True, exist_ok=True)
# Now check the current project
result = cli_runner.invoke(app, ["project", "current"])
assert result.exit_code == 0
assert "Current project: main" in result.stdout
@@ -156,3 +185,24 @@ def test_project_option(cli_runner, temp_home, monkeypatch):
# Verify environment variable was set
assert env_vars.get("BASIC_MEMORY_PROJECT") == "test"
def test_project_default_activates_project(cli_runner, temp_home, monkeypatch):
"""Test that setting the default project also activates it in the current session."""
# Create a test environment
env = {}
monkeypatch.setattr(os, "environ", env)
# Create two test projects
config_manager = ConfigManager()
config_manager.add_project("project1", str(temp_home / "project1"))
# Set project1 as default using the CLI command
result = cli_runner.invoke(app, ["project", "default", "project1"])
assert result.exit_code == 0
assert "Project 'project1' set as default and activated" in result.stdout
# Verify the environment variable was set
# This is the core of our fix - the set_default_project command now also sets
# the BASIC_MEMORY_PROJECT environment variable to activate the project
assert env.get("BASIC_MEMORY_PROJECT") == "project1"
+2 -2
View File
@@ -12,7 +12,7 @@ def test_info_stats_command(cli_env, test_graph):
runner = CliRunner()
# Run the command
result = runner.invoke(cli_app, ["info", "stats"])
result = runner.invoke(cli_app, ["project", "info"])
# Verify exit code
assert result.exit_code == 0
@@ -26,7 +26,7 @@ def test_info_stats_json(cli_env, test_graph):
runner = CliRunner()
# Run the command with --json flag
result = runner.invoke(cli_app, ["info", "stats", "--json"])
result = runner.invoke(cli_app, ["project", "info", "--json"])
# Verify exit code
assert result.exit_code == 0
+92 -1
View File
@@ -1,7 +1,6 @@
"""Tests for note tools that exercise the full stack with SQLite."""
from textwrap import dedent
import pytest
from basic_memory.mcp.tools import write_note, read_note, delete_note
@@ -187,6 +186,30 @@ async def test_delete_note_doesnt_exist(app):
assert deleted is False
@pytest.mark.asyncio
async def test_write_note_with_tag_array_from_bug_report(app):
"""Test creating a note with a tag array as reported in issue #38.
This reproduces the exact payload from the bug report where Cursor
was passing an array of tags and getting a type mismatch error.
"""
# This is the exact payload from the bug report
bug_payload = {
"title": "Title",
"folder": "folder",
"content": "CONTENT",
"tags": ["hipporag", "search", "fallback", "symfony", "error-handling"],
}
# Try to call the function with this data directly
result = await write_note(**bug_payload)
assert result
assert "permalink: folder/title" in result
assert "Tags" in result
assert "hipporag" in result
@pytest.mark.asyncio
async def test_write_note_verbose(app):
"""Test creating a new note.
@@ -229,3 +252,71 @@ async def test_write_note_verbose(app):
""").strip()
in result
)
@pytest.mark.asyncio
async def test_write_note_preserves_custom_metadata(app, test_config):
"""Test that updating a note preserves custom metadata fields.
Reproduces issue #36 where custom frontmatter fields like Status
were being lost when updating notes with the write_note tool.
Should:
- Create a note with custom frontmatter
- Update the note with new content
- Verify custom frontmatter is preserved
"""
# First, create a note with custom metadata using write_note
await write_note(
title="Custom Metadata Note",
folder="test",
content="# Initial content",
tags=["test"],
)
# Read the note to get its permalink
content = await read_note("test/custom-metadata-note")
# Now directly update the file with custom frontmatter
# We need to use a direct file update to add custom frontmatter
import frontmatter
file_path = test_config.home / "test" / "Custom Metadata Note.md"
post = frontmatter.load(file_path)
# Add custom frontmatter
post["Status"] = "In Progress"
post["Priority"] = "High"
post["Version"] = "1.0"
# Write the file back
with open(file_path, "w") as f:
f.write(frontmatter.dumps(post))
# Now update the note using write_note
result = await write_note(
title="Custom Metadata Note",
folder="test",
content="# Updated content",
tags=["test", "updated"],
)
# Verify the update was successful
assert "Updated test/Custom Metadata Note.md" in result
# Read the note back and check if custom frontmatter is preserved
content = await read_note("test/custom-metadata-note")
# Custom frontmatter should be preserved
assert "Status: In Progress" in content
assert "Priority: High" in content
# Version might be quoted as '1.0' due to YAML serialization
assert "Version:" in content # Just check that the field exists
assert "1.0" in content # And that the value exists somewhere
# And new content should be there
assert "# Updated content" in content
# And tags should be updated
assert "'#test'" in content
assert "'#updated'" in content
+1 -1
View File
@@ -26,7 +26,7 @@ async def mock_call_get():
@pytest_asyncio.fixture
async def mock_search():
"""Mock for search tool."""
with patch("basic_memory.mcp.tools.read_note.search") as mock:
with patch("basic_memory.mcp.tools.read_note.search_notes") as mock:
# Default to empty results
mock.return_value = SearchResponse(results=[], current_page=1, page_size=1)
yield mock
+5 -5
View File
@@ -4,7 +4,7 @@ import pytest
from datetime import datetime, timedelta
from basic_memory.mcp.tools import write_note
from basic_memory.mcp.tools.search import search
from basic_memory.mcp.tools.search import search_notes
from basic_memory.schemas.search import SearchQuery, SearchItemType
@@ -22,7 +22,7 @@ async def test_search_basic(client):
# Search for it
query = SearchQuery(text="searchable")
response = await search(query)
response = await search_notes(query)
# Verify results
assert len(response.results) > 0
@@ -43,7 +43,7 @@ async def test_search_pagination(client):
# Search for it
query = SearchQuery(text="searchable")
response = await search(query, page=1, page_size=1)
response = await search_notes(query, page=1, page_size=1)
# Verify results
assert len(response.results) == 1
@@ -62,7 +62,7 @@ async def test_search_with_type_filter(client):
# Search with type filter
query = SearchQuery(text="type", types=[SearchItemType.ENTITY])
response = await search(query)
response = await search_notes(query)
# Verify all results are entities
assert all(r.type == "entity" for r in response.results)
@@ -81,7 +81,7 @@ async def test_search_with_date_filter(client):
# Search with date filter
one_hour_ago = datetime.now() - timedelta(hours=1)
query = SearchQuery(text="recent", after_date=one_hour_ago)
response = await search(query)
response = await search_notes(query)
# Verify we get results within timeframe
assert len(response.results) > 0
+1 -1
View File
@@ -521,7 +521,7 @@ async def test_update_with_content(entity_service: EntityService, file_service:
"""
).strip()
# Create test entity
# update entity
entity, created = await entity_service.create_or_update_entity(
EntitySchema(
title="Git Workflow Guide",
+184
View File
@@ -0,0 +1,184 @@
"""Test proper handling of .tmp files during sync."""
import asyncio
from pathlib import Path
import pytest
from watchfiles import Change
async def create_test_file(path: Path, content: str = "test content") -> None:
"""Create a test file with given content."""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
@pytest.mark.asyncio
async def test_temp_file_filter(watch_service):
"""Test that .tmp files are correctly filtered out."""
# Test filter_changes method directly
tmp_path = str(watch_service.config.home / "test.tmp")
assert not watch_service.filter_changes(Change.added, tmp_path)
# Test with valid file
valid_path = str(watch_service.config.home / "test.md")
assert watch_service.filter_changes(Change.added, valid_path)
@pytest.mark.asyncio
async def test_handle_tmp_files(watch_service, test_config, monkeypatch):
"""Test handling of .tmp files during sync process."""
project_dir = test_config.home
# Create a .tmp file - this simulates a file being written with write_file_atomic
tmp_file = project_dir / "test.tmp"
await create_test_file(tmp_file, "This is a temporary file")
# Create the target final file
final_file = project_dir / "test.md"
await create_test_file(final_file, "This is the final file")
# Setup changes that include both the .tmp and final file
changes = {
(Change.added, str(tmp_file)),
(Change.added, str(final_file)),
}
# Track sync_file calls
sync_calls = []
# Mock sync_file to track calls
original_sync_file = watch_service.sync_service.sync_file
async def mock_sync_file(path, new=True):
sync_calls.append(path)
return await original_sync_file(path, new)
monkeypatch.setattr(watch_service.sync_service, "sync_file", mock_sync_file)
# Handle changes
await watch_service.handle_changes(project_dir, changes)
# Verify .tmp file was not processed
assert "test.tmp" not in sync_calls
assert "test.md" in sync_calls
# Verify only the final file got an entity
tmp_entity = await watch_service.sync_service.entity_repository.get_by_file_path("test.tmp")
final_entity = await watch_service.sync_service.entity_repository.get_by_file_path("test.md")
assert tmp_entity is None, "Temp file should not have an entity"
assert final_entity is not None, "Final file should have an entity"
@pytest.mark.asyncio
async def test_atomic_write_tmp_file_handling(watch_service, test_config, monkeypatch):
"""Test handling of file changes during atomic write operations."""
project_dir = test_config.home
# This test simulates the full atomic write process:
# 1. First a .tmp file is created
# 2. Then the .tmp file is renamed to the final file
# 3. Both events are processed by the watch service
# Setup file paths
tmp_path = project_dir / "document.tmp"
final_path = project_dir / "document.md"
# Create mockup of the atomic write process
await create_test_file(tmp_path, "Content for document")
# First batch of changes - .tmp file created
changes1 = {(Change.added, str(tmp_path))}
# Process first batch
await watch_service.handle_changes(project_dir, changes1)
# Now "replace" the temp file with the final file
tmp_path.rename(final_path)
# Second batch of changes - .tmp file deleted, final file added
changes2 = {(Change.deleted, str(tmp_path)), (Change.added, str(final_path))}
# Process second batch
await watch_service.handle_changes(project_dir, changes2)
# Verify only the final file is in the database
tmp_entity = await watch_service.sync_service.entity_repository.get_by_file_path("document.tmp")
final_entity = await watch_service.sync_service.entity_repository.get_by_file_path(
"document.md"
)
assert tmp_entity is None, "Temp file should not have an entity"
assert final_entity is not None, "Final file should have an entity"
# Check events
new_events = [e for e in watch_service.state.recent_events if e.action == "new"]
assert len(new_events) == 1
assert new_events[0].path == "document.md"
@pytest.mark.asyncio
async def test_rapid_atomic_writes(watch_service, test_config):
"""Test handling of rapid atomic writes to the same destination."""
project_dir = test_config.home
# This test simulates multiple rapid atomic writes to the same file:
# 1. Several .tmp files are created one after another
# 2. Each is then renamed to the same final file
# 3. Events are batched and processed together
# Setup file paths
tmp1_path = project_dir / "document.1.tmp"
tmp2_path = project_dir / "document.2.tmp"
final_path = project_dir / "document.md"
# Create multiple temp files that will be used in sequence
await create_test_file(tmp1_path, "First version")
await create_test_file(tmp2_path, "Second version")
# Simulate the first atomic write
tmp1_path.rename(final_path)
# Brief pause to ensure file system registers the change
await asyncio.sleep(0.1)
# Read content to verify
content1 = final_path.read_text()
assert content1 == "First version"
# Simulate the second atomic write
tmp2_path.rename(final_path)
# Verify content was updated
content2 = final_path.read_text()
assert content2 == "Second version"
# Create a batch of changes that might arrive in mixed order
changes = {
(Change.added, str(tmp1_path)),
(Change.deleted, str(tmp1_path)),
(Change.added, str(tmp2_path)),
(Change.deleted, str(tmp2_path)),
(Change.added, str(final_path)),
(Change.modified, str(final_path)),
}
# Process all changes
await watch_service.handle_changes(project_dir, changes)
# Verify only the final file is in the database
final_entity = await watch_service.sync_service.entity_repository.get_by_file_path(
"document.md"
)
assert final_entity is not None
# Also verify no tmp entities were created
tmp1_entity = await watch_service.sync_service.entity_repository.get_by_file_path(
"document.1.tmp"
)
tmp2_entity = await watch_service.sync_service.entity_repository.get_by_file_path(
"document.2.tmp"
)
assert tmp1_entity is None
assert tmp2_entity is None
+195
View File
@@ -0,0 +1,195 @@
"""Test UTF-8 character handling in file operations."""
import pytest
from basic_memory import file_utils
from basic_memory.file_utils import write_file_atomic, compute_checksum
@pytest.mark.asyncio
async def test_write_utf8_characters(tmp_path):
"""Test writing files with UTF-8 characters."""
# Create a test file with various UTF-8 characters
test_path = tmp_path / "utf8_test.md"
# Include characters from various scripts
utf8_content = """# UTF-8 Test Document
## Cyrillic
Привет мир! (Hello world in Russian)
## Greek
Γειά σου Κόσμε! (Hello world in Greek)
## Chinese
你好世界! (Hello world in Chinese)
## Japanese
こんにちは世界! (Hello world in Japanese)
## Arabic
مرحبا بالعالم! (Hello world in Arabic)
## Emoji
🌍 🌎 🌏 🌐 🗺 🧭
"""
# Test the atomic write function with UTF-8 content
await write_file_atomic(test_path, utf8_content)
# Verify the file was written correctly
assert test_path.exists()
# Read the file back and verify content
content = test_path.read_text(encoding="utf-8")
assert "Привет мир!" in content
assert "Γειά σου Κόσμε!" in content
assert "你好世界" in content
assert "こんにちは世界" in content
assert "مرحبا بالعالم" in content
assert "🌍 🌎 🌏" in content
# Verify checksums match
checksum1 = await compute_checksum(utf8_content)
checksum2 = await compute_checksum(content)
assert checksum1 == checksum2
@pytest.mark.asyncio
async def test_frontmatter_with_utf8(tmp_path):
"""Test handling of frontmatter with UTF-8 characters."""
# Create a test file with frontmatter containing UTF-8
test_path = tmp_path / "frontmatter_utf8.md"
# Create content with UTF-8 in frontmatter
content = """---
title: UTF-8 测试文件 (Test File)
author: José García
keywords:
- тестирование
- 테스트
- 测试
---
# UTF-8 Support Test
This file has UTF-8 characters in the frontmatter.
"""
# Write the file
await write_file_atomic(test_path, content)
# Update frontmatter with more UTF-8
await file_utils.update_frontmatter(
test_path,
{
"description": "这是一个测试文件 (This is a test file)",
"tags": ["国际化", "юникод", "🔤"],
},
)
# Read back and check
updated_content = test_path.read_text(encoding="utf-8")
# Original values should be preserved
assert "title: UTF-8 测试文件" in updated_content
assert "author: José García" in updated_content
# New values should be added
assert "description: 这是一个测试文件" in updated_content
assert "国际化" in updated_content
assert "юникод" in updated_content
assert "🔤" in updated_content
@pytest.mark.asyncio
async def test_utf8_in_entity_sync(sync_service, test_config):
"""Test syncing an entity with UTF-8 content."""
project_dir = test_config.home
# Create a test file with UTF-8 characters
test_file = project_dir / "utf8_entity.md"
utf8_content = """---
type: knowledge
permalink: i18n/utf8-document
---
# UTF-8 测试文档
## Observations
- [language] العربية (Arabic) is written right-to-left
- [language] 日本語 (Japanese) uses three writing systems
- [encoding] UTF-8 可以编码所有 Unicode 字符
## Relations
- relates_to [[i18n/Ελληνικά]]
- relates_to [[i18n/Русский]]
"""
# Write the file
test_file.parent.mkdir(parents=True, exist_ok=True)
test_file.write_text(utf8_content, encoding="utf-8")
# Sync the file
await sync_service.sync(test_config.home, show_progress=False)
# Verify entity was created
entity = await sync_service.entity_service.get_by_permalink("i18n/utf8-document")
assert entity is not None
# Verify observations were created with UTF-8 content
assert len(entity.observations) == 3
# Check observation content
obs_categories = [o.category for o in entity.observations]
obs_content = [o.content for o in entity.observations]
assert "language" in obs_categories
assert "encoding" in obs_categories
assert any("العربية" in c for c in obs_content)
assert any("日本語" in c for c in obs_content)
assert any("Unicode" in c for c in obs_content)
# Check relations
assert len(entity.relations) == 2
relation_names = [r.to_name for r in entity.relations]
assert "i18n/Ελληνικά" in relation_names
assert "i18n/Русский" in relation_names
@pytest.mark.asyncio
async def test_write_file_service_utf8(sync_service, test_config):
"""Test FileService handling of UTF-8 content."""
file_service = sync_service.file_service
# Test writing UTF-8 content through the file service
test_path = "utf8_service_test.md"
utf8_content = """---
title: FileService UTF-8 Test
---
# UTF-8 通过 FileService 测试
Testing FileService with UTF-8:
* Português: Olá mundo!
* Thai: สวสดชาวโลก
* Korean: 안녕하세요 세계
* Hindi: नमस ि
"""
# Use the file service to write the file
checksum = await file_service.write_file(test_path, utf8_content)
# Verify file exists
full_path = file_service.base_path / test_path
assert full_path.exists()
# Read back content and verify
content, read_checksum = await file_service.read_file(test_path)
assert "通过" in content
assert "Português" in content
assert "สวัสดีชาวโลก" in content
assert "안녕하세요" in content
assert "नमस्ते" in content
# Checksums should match
assert checksum == read_checksum
Generated
+202 -172
View File
@@ -25,16 +25,16 @@ wheels = [
[[package]]
name = "alembic"
version = "1.14.1"
version = "1.15.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 }
sdist = { url = "https://files.pythonhosted.org/packages/4a/ed/901044acb892caa5604bf818d2da9ab0df94ef606c6059fdf367894ebf60/alembic-1.15.1.tar.gz", hash = "sha256:e1a1c738577bca1f27e68728c910cd389b9a92152ff91d902da649c192e30c49", size = 1924789 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/7e/ac0991d1745f7d755fc1cd381b3990a45b404b4d008fc75e2a983516fbfe/alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5", size = 233565 },
{ url = "https://files.pythonhosted.org/packages/99/f7/d398fae160568472ddce0b3fde9c4581afc593019a6adc91006a66406991/alembic-1.15.1-py3-none-any.whl", hash = "sha256:197de710da4b3e91cf66a826a5b31b5d59a127ab41bd0fc42863e2902ce2bbbe", size = 231753 },
]
[[package]]
@@ -48,16 +48,16 @@ wheels = [
[[package]]
name = "anyio"
version = "4.8.0"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
[[package]]
@@ -71,7 +71,7 @@ wheels = [
[[package]]
name = "basic-memory"
version = "0.9.0"
version = "0.10.1"
source = { editable = "." }
dependencies = [
{ name = "aiosqlite" },
@@ -148,6 +148,15 @@ dev = [
{ name = "ruff", specifier = ">=0.1.6" },
]
[[package]]
name = "cabarchive"
version = "0.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/28/d3/a544aed878edc269ce4427bc937310b73624e1d595de7f4e5bcab413a639/cabarchive-0.2.4.tar.gz", hash = "sha256:04f60089473114cf26eab2b7e1d09611c5bfaf8edd3202dacef66bb5c71e48cf", size = 21064 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/fb/713421f46c68f4bf9cd26f05bda0c233446108997b6b4d83d7ef07f20009/cabarchive-0.2.4-py3-none-any.whl", hash = "sha256:4afabd224eb2e40af8e907379fb8eec6b0adfb71c2aef4457ec3a4d77383c059", size = 25729 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
@@ -195,66 +204,78 @@ wheels = [
[[package]]
name = "coverage"
version = "7.6.12"
version = "7.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 }
sdist = { url = "https://files.pythonhosted.org/packages/6b/bf/3effb7453498de9c14a81ca21e1f92e6723ce7ebdc5402ae30e4dcc490ac/coverage-7.7.1.tar.gz", hash = "sha256:199a1272e642266b90c9f40dec7fd3d307b51bf639fa0d15980dc0b3246c1393", size = 810332 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 },
{ url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 },
{ url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 },
{ url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 },
{ url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 },
{ url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 },
{ url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 },
{ url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 },
{ url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 },
{ url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 },
{ url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 },
{ url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 },
{ url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 },
{ url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 },
{ url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 },
{ url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 },
{ url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 },
{ url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 },
{ url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 },
{ url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 },
{ url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 },
{ url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 },
{ url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 },
{ url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 },
{ url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 },
{ url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 },
{ url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 },
{ url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 },
{ url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 },
{ url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 },
{ url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 },
{ url = "https://files.pythonhosted.org/packages/cf/b0/4eaba302a86ec3528231d7cfc954ae1929ec5d42b032eb6f5b5f5a9155d2/coverage-7.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:eff187177d8016ff6addf789dcc421c3db0d014e4946c1cc3fbf697f7852459d", size = 211253 },
{ url = "https://files.pythonhosted.org/packages/fd/68/21b973e6780a3f2457e31ede1aca6c2f84bda4359457b40da3ae805dcf30/coverage-7.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2444fbe1ba1889e0b29eb4d11931afa88f92dc507b7248f45be372775b3cef4f", size = 211504 },
{ url = "https://files.pythonhosted.org/packages/d1/b4/c19e9c565407664390254252496292f1e3076c31c5c01701ffacc060e745/coverage-7.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:177d837339883c541f8524683e227adcaea581eca6bb33823a2a1fdae4c988e1", size = 245566 },
{ url = "https://files.pythonhosted.org/packages/7b/0e/f9829cdd25e5083638559c8c267ff0577c6bab19dacb1a4fcfc1e70e41c0/coverage-7.7.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15d54ecef1582b1d3ec6049b20d3c1a07d5e7f85335d8a3b617c9960b4f807e0", size = 242455 },
{ url = "https://files.pythonhosted.org/packages/29/57/a3ada2e50a665bf6d9851b5eb3a9a07d7e38f970bdd4d39895f311331d56/coverage-7.7.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75c82b27c56478d5e1391f2e7b2e7f588d093157fa40d53fd9453a471b1191f2", size = 244713 },
{ url = "https://files.pythonhosted.org/packages/0f/d3/f15c7d45682a73eca0611427896016bad4c8f635b0fc13aae13a01f8ed9d/coverage-7.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:315ff74b585110ac3b7ab631e89e769d294f303c6d21302a816b3554ed4c81af", size = 244476 },
{ url = "https://files.pythonhosted.org/packages/19/3b/64540074e256082b220e8810fd72543eff03286c59dc91976281dc0a559c/coverage-7.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4dd532dac197d68c478480edde74fd4476c6823355987fd31d01ad9aa1e5fb59", size = 242695 },
{ url = "https://files.pythonhosted.org/packages/8a/c1/9cad25372ead7f9395a91bb42d8ae63e6cefe7408eb79fd38797e2b763eb/coverage-7.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:385618003e3d608001676bb35dc67ae3ad44c75c0395d8de5780af7bb35be6b2", size = 243888 },
{ url = "https://files.pythonhosted.org/packages/66/c6/c3e6c895bc5b95ccfe4cb5838669dbe5226ee4ad10604c46b778c304d6f9/coverage-7.7.1-cp312-cp312-win32.whl", hash = "sha256:63306486fcb5a827449464f6211d2991f01dfa2965976018c9bab9d5e45a35c8", size = 213744 },
{ url = "https://files.pythonhosted.org/packages/cc/8a/6df2fcb4c3e38ec6cd7e211ca8391405ada4e3b1295695d00aa07c6ee736/coverage-7.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:37351dc8123c154fa05b7579fdb126b9f8b1cf42fd6f79ddf19121b7bdd4aa04", size = 214546 },
{ url = "https://files.pythonhosted.org/packages/ec/2a/1a254eaadb01c163b29d6ce742aa380fc5cfe74a82138ce6eb944c42effa/coverage-7.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eebd927b86761a7068a06d3699fd6c20129becf15bb44282db085921ea0f1585", size = 211277 },
{ url = "https://files.pythonhosted.org/packages/cf/00/9636028365efd4eb6db71cdd01d99e59f25cf0d47a59943dbee32dd1573b/coverage-7.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a79c4a09765d18311c35975ad2eb1ac613c0401afdd9cb1ca4110aeb5dd3c4c", size = 211551 },
{ url = "https://files.pythonhosted.org/packages/6f/c8/14aed97f80363f055b6cd91e62986492d9fe3b55e06b4b5c82627ae18744/coverage-7.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b1c65a739447c5ddce5b96c0a388fd82e4bbdff7251396a70182b1d83631019", size = 245068 },
{ url = "https://files.pythonhosted.org/packages/d6/76/9c5fe3f900e01d7995b0cda08fc8bf9773b4b1be58bdd626f319c7d4ec11/coverage-7.7.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392cc8fd2b1b010ca36840735e2a526fcbd76795a5d44006065e79868cc76ccf", size = 242109 },
{ url = "https://files.pythonhosted.org/packages/c0/81/760993bb536fb674d3a059f718145dcd409ed6d00ae4e3cbf380019fdfd0/coverage-7.7.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bb47cc9f07a59a451361a850cb06d20633e77a9118d05fd0f77b1864439461b", size = 244129 },
{ url = "https://files.pythonhosted.org/packages/00/be/1114a19f93eae0b6cd955dabb5bee80397bd420d846e63cd0ebffc134e3d/coverage-7.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b4c144c129343416a49378e05c9451c34aae5ccf00221e4fa4f487db0816ee2f", size = 244201 },
{ url = "https://files.pythonhosted.org/packages/06/8d/9128fd283c660474c7dc2b1ea5c66761bc776b970c1724989ed70e9d6eee/coverage-7.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bc96441c9d9ca12a790b5ae17d2fa6654da4b3962ea15e0eabb1b1caed094777", size = 242282 },
{ url = "https://files.pythonhosted.org/packages/d4/2a/6d7dbfe9c1f82e2cdc28d48f4a0c93190cf58f057fa91ba2391b92437fe6/coverage-7.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d03287eb03186256999539d98818c425c33546ab4901028c8fa933b62c35c3a", size = 243570 },
{ url = "https://files.pythonhosted.org/packages/cf/3e/29f1e4ce3bb951bcf74b2037a82d94c5064b3334304a3809a95805628838/coverage-7.7.1-cp313-cp313-win32.whl", hash = "sha256:8fed429c26b99641dc1f3a79179860122b22745dd9af36f29b141e178925070a", size = 213772 },
{ url = "https://files.pythonhosted.org/packages/bc/3a/cf029bf34aefd22ad34f0e808eba8d5830f297a1acb483a2124f097ff769/coverage-7.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:092b134129a8bb940c08b2d9ceb4459af5fb3faea77888af63182e17d89e1cf1", size = 214575 },
{ url = "https://files.pythonhosted.org/packages/92/4c/fb8b35f186a2519126209dce91ab8644c9a901cf04f8dfa65576ca2dd9e8/coverage-7.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3154b369141c3169b8133973ac00f63fcf8d6dbcc297d788d36afbb7811e511", size = 212113 },
{ url = "https://files.pythonhosted.org/packages/59/90/e834ffc86fd811c5b570a64ee1895b20404a247ec18a896b9ba543b12097/coverage-7.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:264ff2bcce27a7f455b64ac0dfe097680b65d9a1a293ef902675fa8158d20b24", size = 212333 },
{ url = "https://files.pythonhosted.org/packages/a5/a1/27f0ad39569b3b02410b881c42e58ab403df13fcd465b475db514b83d3d3/coverage-7.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba8480ebe401c2f094d10a8c4209b800a9b77215b6c796d16b6ecdf665048950", size = 256566 },
{ url = "https://files.pythonhosted.org/packages/9f/3b/21fa66a1db1b90a0633e771a32754f7c02d60236a251afb1b86d7e15d83a/coverage-7.7.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520af84febb6bb54453e7fbb730afa58c7178fd018c398a8fcd8e269a79bf96d", size = 252276 },
{ url = "https://files.pythonhosted.org/packages/d6/e5/4ab83a59b0f8ac4f0029018559fc4c7d042e1b4552a722e2bfb04f652296/coverage-7.7.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88d96127ae01ff571d465d4b0be25c123789cef88ba0879194d673fdea52f54e", size = 254616 },
{ url = "https://files.pythonhosted.org/packages/db/7a/4224417c0ccdb16a5ba4d8d1fcfaa18439be1624c29435bb9bc88ccabdfb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0ce92c5a9d7007d838456f4b77ea159cb628187a137e1895331e530973dcf862", size = 255707 },
{ url = "https://files.pythonhosted.org/packages/51/20/ff18a329ccaa3d035e2134ecf3a2e92a52d3be6704c76e74ca5589ece260/coverage-7.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0dab4ef76d7b14f432057fdb7a0477e8bffca0ad39ace308be6e74864e632271", size = 253876 },
{ url = "https://files.pythonhosted.org/packages/e4/e8/1d6f1a6651672c64f45ffad05306dad9c4c189bec694270822508049b2cb/coverage-7.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7e688010581dbac9cab72800e9076e16f7cccd0d89af5785b70daa11174e94de", size = 254687 },
{ url = "https://files.pythonhosted.org/packages/6b/ea/1b9a14cf3e2bc3fd9de23a336a8082091711c5f480b500782d59e84a8fe5/coverage-7.7.1-cp313-cp313t-win32.whl", hash = "sha256:e52eb31ae3afacdacfe50705a15b75ded67935770c460d88c215a9c0c40d0e9c", size = 214486 },
{ url = "https://files.pythonhosted.org/packages/cc/bb/faa6bcf769cb7b3b660532a30d77c440289b40636c7f80e498b961295d07/coverage-7.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a6b6b3bd121ee2ec4bd35039319f3423d0be282b9752a5ae9f18724bc93ebe7c", size = 215647 },
{ url = "https://files.pythonhosted.org/packages/52/26/9f53293ff4cc1d47d98367ce045ca2e62746d6be74a5c6851a474eabf59b/coverage-7.7.1-py3-none-any.whl", hash = "sha256:822fa99dd1ac686061e1219b67868e25d9757989cf2259f735a4802497d6da31", size = 203006 },
]
[[package]]
name = "cx-freeze"
version = "7.2.10"
version = "8.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cabarchive", marker = "sys_platform == 'win32'" },
{ name = "cx-logging", marker = "sys_platform == 'win32'" },
{ name = "dmgbuild", marker = "sys_platform == 'darwin'" },
{ name = "filelock", marker = "sys_platform == 'linux'" },
{ name = "filelock" },
{ name = "lief", marker = "sys_platform == 'win32'" },
{ name = "packaging" },
{ name = "patchelf", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'armv7l' and sys_platform == 'linux') or (platform_machine == 'i686' and sys_platform == 'linux') or (platform_machine == 'ppc64le' and sys_platform == 'linux') or (platform_machine == 's390x' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" },
{ name = "setuptools" },
{ name = "striprtf", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d4/c3/9f05155f7e0f55041add55cedc8724e9d36418c11c2e29131056fbce8e5e/cx_freeze-7.2.10.tar.gz", hash = "sha256:16a6eb37db7d158d0eb4bba8b93a135b6c85ea4bf2c4176c437bc34f01067e61", size = 3146042 }
sdist = { url = "https://files.pythonhosted.org/packages/9d/9f/e007ffc5f457c5451b7ade774529f64d0cdb61d91dae428568775b4ec9e3/cx_freeze-8.0.0.tar.gz", hash = "sha256:80e1f87bb152ed0f97f7c6435e0237d44aade99979927c4627771322a25d550d", size = 3177126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e2/9a/0f4084aa728a67974574eac2f1db3a032671536f68a935f340caa31b8667/cx_Freeze-7.2.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4a908dcad86888b741aa2051a65693d8662999a8abf9b7cf3285ef4ccfddad43", size = 21397503 },
{ url = "https://files.pythonhosted.org/packages/f6/ba/1a66ed666e128152ee9d43b6aa952db823de9918fc7c864373c1000816a4/cx_Freeze-7.2.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:416460e7f3312b1e2010ce5cb35e0449ff5a6e75b3536adddf83fd9b10d13a88", size = 12649925 },
{ url = "https://files.pythonhosted.org/packages/bf/29/e0b3fa58394bfc6bc3ce1776e77788e6c8a856fd850974924791b4d5e661/cx_Freeze-7.2.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:851a0d919a732d17e1db145c72d69b5560ab1578387e18915a9e9f0be1bc50fd", size = 13949452 },
{ url = "https://files.pythonhosted.org/packages/3c/bd/c237c1b0c48cf5fe2103dedad42a3d36c0ef6c44ab86265f8480d13b3c6f/cx_Freeze-7.2.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e251c74b2551668ae890d4c7a2fbb73a6c9b692de4096ddd9867322873e11b2b", size = 12787091 },
{ url = "https://files.pythonhosted.org/packages/8f/66/fae7d0992e72290a08494fa7c5826f00b5130e63747a434993a140746e07/cx_Freeze-7.2.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7f5d65830cb3b0d24590e4526995baf01beeb2517c41838d597c5a2e4ca76d7", size = 13766813 },
{ url = "https://files.pythonhosted.org/packages/f1/78/c123ec526b57531851d9d9c3338aa2df41b396fe3d23b7a2737bf1ef1a63/cx_Freeze-7.2.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d00914208a2953125398d192d1c332eea2bfb4f210a1507efe5188fec18fb8e3", size = 13315368 },
{ url = "https://files.pythonhosted.org/packages/ac/6c/4a3ef0c1991805505b2ea69fd09e1fd694138b011d2c62808ab5d4548b16/cx_Freeze-7.2.10-cp312-cp312-win32.whl", hash = "sha256:c0af82ba512323a1036a42c2e863ea56e12764c8c9f0764303df1fa3e29b9d4a", size = 2136880 },
{ url = "https://files.pythonhosted.org/packages/06/4c/d1c81cb9cf6b0a68837d7ee44a98a3269bafbc259cb8b3b96c22ac1e235e/cx_Freeze-7.2.10-cp312-cp312-win_amd64.whl", hash = "sha256:15b7dafc5eb2b5f6c118eaeed25b0fb0f3a9022e779958e5f9cd1ebf9f77c737", size = 2139867 },
{ url = "https://files.pythonhosted.org/packages/2e/26/23311406e69f96b2382b00b661e85931c70069f6702306edfee034ce473b/cx_Freeze-8.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:6da256133c981960408443558031ce818254b9bc8483039d507ebd5b914f5f6d", size = 21439091 },
{ url = "https://files.pythonhosted.org/packages/14/95/8fa6126a9f407d342f25186c6933de06da4a0705c3ab41452ac0a324887c/cx_Freeze-8.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1af9ab25e90924c26e45ee27a4743604538ab51095b4eb2c9052d548ac04b5e7", size = 14278341 },
{ url = "https://files.pythonhosted.org/packages/c3/b4/8d299d9bcbae20261c465e3dce45888c63c96b82cb43f909564106698af3/cx_Freeze-8.0.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0cc36dd9c1f0ee156b1288d3a566b55281ae83b09f56c7480d00f5e32236b53", size = 15783842 },
{ url = "https://files.pythonhosted.org/packages/cc/fe/801de38e532ac5099c1a35a0b8e3c7ef045f3657bdb0113bc0ea9c1b880f/cx_Freeze-8.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5101f2f7074c1373ac122e50c102ffc1805218d0bd994f3f07fe8ffefe46c7cb", size = 14418893 },
{ url = "https://files.pythonhosted.org/packages/4b/f1/280af574f3a52290f808be06a6c017299f0970a29ecaa6975f46a53bf5c5/cx_Freeze-8.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b5afdddd6a68c69823308b03dd697ec84adca898ff4dcf507325fc56f08ba8f6", size = 15442970 },
{ url = "https://files.pythonhosted.org/packages/8a/34/0b20b94e60740fe755d07429089b10cc400aef4ce1af6b1fea9b1e8b16a4/cx_Freeze-8.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:592ee11c4e6e61b57bfd3cd1a4655cfe1e7dbb5e2d1bbac8fa34614d54b16294", size = 15223472 },
{ url = "https://files.pythonhosted.org/packages/c0/fd/b440b436bca420406d17a5727447a386e54974bf6197255b0cba83bcc92b/cx_Freeze-8.0.0-cp312-cp312-win32.whl", hash = "sha256:a560abd4f29438ed1c5be9b995a6a28db476a0210b354c28b8b7789bf4e8ae00", size = 2184568 },
{ url = "https://files.pythonhosted.org/packages/3b/1b/9ddc5e42b65257eb1805184fdcc9cb5cf1538b1837d4502ef563c3e737b5/cx_Freeze-8.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:90b7cad33fbe9be5defb7bbd24270f3589037e0127e3e5b2078255fe1ca64037", size = 2189398 },
{ url = "https://files.pythonhosted.org/packages/ff/02/ab8a1226166494689b3adf29036509055a680e0435cebbb0e6681f333341/cx_Freeze-8.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25fbbd1f06c162b0056f9aa1982ee7c87198445511ec850f07d2e2cf208460f1", size = 20907868 },
{ url = "https://files.pythonhosted.org/packages/3c/79/19d1cc296f8945b772f2c726acadb9817e3fd158412d039d96b774975fb5/cx_Freeze-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3741719ad85f348f088fe20d7acbe6c176e941aa9c8888633eeeb6734d835c", size = 13995569 },
{ url = "https://files.pythonhosted.org/packages/d2/b1/618549522e176dc4128d75b7d915c4b8ec79171d361a8160a3a434db44b4/cx_Freeze-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee138b0b000b4014abfcba8f3d6ec45524c50d02e985b987027185c07aa5bdde", size = 14086121 },
{ url = "https://files.pythonhosted.org/packages/ef/63/404c17a650fd1f7d24a7916831cb17ca3ecd42e8aa611c60ab5445656d0e/cx_Freeze-8.0.0-cp313-cp313-win32.whl", hash = "sha256:403bd682238e4e8e0eac89dc0240925fc6bb3b638f28a2f1b7404be62cdcd42c", size = 2188054 },
{ url = "https://files.pythonhosted.org/packages/ea/3d/1b0da41e70077e2dece038eec2b666b07a804cbaaa15c8ea377a7c9e96f5/cx_Freeze-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:bdba9196bf577d04700a4f17bf6ce9ec3e56d4dd97a7feae237dc8dce07287e7", size = 2192875 },
{ url = "https://files.pythonhosted.org/packages/90/23/a7584176f749bbb73494a8c2decfb00af8fdc579e189177321e2ccb0c0bc/cx_Freeze-8.0.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:93a2ca2ee6e374dd3a6796e67367d24396467df2f9b8434f12daec9d9a88e126", size = 20867921 },
{ url = "https://files.pythonhosted.org/packages/c6/4b/b6296f5cbe74a04bb1746d6da3b591bbe9f327dc509f1c18770323fc5d18/cx_Freeze-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf47b56fa2cb5982232d30a475a739d7c10db498a8d45024c93b963a8736d478", size = 12500579 },
{ url = "https://files.pythonhosted.org/packages/a3/25/b55c7a0658a1bae3b9ec0a2eab46836ba7a4c2d3049610aabbe2667d2bda/cx_Freeze-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd36251ae2af50ec78fd574411a0fbb7868d8ec0ebaaa9a0dc264a46600102fc", size = 12579341 },
{ url = "https://files.pythonhosted.org/packages/c0/ae/0cb1e6aee865adc9f3e6a9ed29e6d16732612ff0e67cd214c6bd1ccf1ac3/cx_Freeze-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:51857ce2863b9c33fbdfa96a04365c64641cb8f567f4c7cadace6f63ebcf9a25", size = 13484222 },
{ url = "https://files.pythonhosted.org/packages/aa/a4/bae422c7256ca2c3474d65823e5012fc40e465e29cc4fdfa56f6733d8ba3/cx_Freeze-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:eae16598241754e6184c07a959055f824d4b5b9d7eaf2b1a31d3bb118c448374", size = 13162872 },
]
[[package]]
@@ -286,15 +307,15 @@ wheels = [
[[package]]
name = "dmgbuild"
version = "1.6.4"
version = "1.6.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ds-store", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or sys_platform != 'linux'" },
{ name = "mac-alias", marker = "(platform_machine != 'aarch64' and platform_machine != 'armv7l' and platform_machine != 'i686' and platform_machine != 'ppc64le' and platform_machine != 's390x' and platform_machine != 'x86_64') or sys_platform != 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/17/89c089f4263b6c7f40a69355dd74f2cf5b5a58c4f7db79f76ed3d9e1ca5b/dmgbuild-1.6.4.tar.gz", hash = "sha256:adacada75ee517398d014e3fb250b004a2225f01e54832df52d38b9cd1530b74", size = 36824 }
sdist = { url = "https://files.pythonhosted.org/packages/16/93/b9702c68d5dedfd6b91c76268a89091ff681b8e3b9a026e7919b6ab730a4/dmgbuild-1.6.5.tar.gz", hash = "sha256:c5cbeec574bad84a324348aa7c36d4aada04568c99fb104dec18d22ba3259f45", size = 36848 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/c5/71ecb2f6a95082aa6a7227a92054024e29caba133a7a86094ce32fd03c1f/dmgbuild-1.6.4-py3-none-any.whl", hash = "sha256:53a2f0a9e65111314fce7ecfb6637d08bc3186fa99e719abc9a5af2b484a7d65", size = 34900 },
{ url = "https://files.pythonhosted.org/packages/48/4a/b16f1081f69592c6dba92baa4d3ca7a5685091a0f840f4b5e01be41aaf84/dmgbuild-1.6.5-py3-none-any.whl", hash = "sha256:e19ab8c5e8238e6455d9ccb9175817be7fd62b9cdd1eef20f63dd88e0ec469ab", size = 34906 },
]
[[package]]
@@ -342,16 +363,16 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.115.9"
version = "0.115.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/dd/d854f85e70f7341b29e3fda754f2833aec197bd355f805238758e3bcd8ed/fastapi-0.115.9.tar.gz", hash = "sha256:9d7da3b196c5eed049bc769f9475cd55509a112fbe031c0ef2f53768ae68d13f", size = 293774 }
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/b6/7517af5234378518f27ad35a7b24af9591bc500b8c1780929c1295999eb6/fastapi-0.115.9-py3-none-any.whl", hash = "sha256:4a439d7923e4de796bcc88b64e9754340fcd1574673cbd865ba8a99fe0d28c56", size = 94919 },
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 },
]
[package.optional-dependencies]
@@ -385,11 +406,11 @@ standard = [
[[package]]
name = "filelock"
version = "3.17.0"
version = "3.18.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 }
sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 },
{ url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215 },
]
[[package]]
@@ -549,34 +570,34 @@ wheels = [
[[package]]
name = "iniconfig"
version = "2.0.0"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
]
[[package]]
name = "jinja2"
version = "3.1.5"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 }
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 },
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
]
[[package]]
name = "lief"
version = "0.16.3"
version = "0.16.4"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/c6/2b8eab0b1c061d2a0a29642cbef91b9b36148e86f3fce99200ede5e8e788/lief-0.16.3-cp312-cp312-win32.whl", hash = "sha256:e7ba797829584c5cc1c8a736b2f1587f09b1f3030239c968c7664649fb79ae15", size = 3042391 },
{ url = "https://files.pythonhosted.org/packages/76/4e/7bb9efe6bbe804449c150295fa597c7e10fd7264c5a5205c38f9b3bd9942/lief-0.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:70c1f5f66bd4eeead2853a7a80d941b4dd03e3791c68c5351b0d39c78cfa9afe", size = 3167314 },
{ url = "https://files.pythonhosted.org/packages/57/9a/be854d274352a2ce406691fb6c80c82b4a6adf0fdb6978734e4c082326b7/lief-0.16.3-cp313-cp313-win32.whl", hash = "sha256:aefbe78b06d9e89387ab8fc069d1cb34252f5916cf35eaa21088b21b74b99d08", size = 3042393 },
{ url = "https://files.pythonhosted.org/packages/51/be/34b8864d0976258f29a5e3d7bedb2785fd56409cf866813458bfd32d2a6b/lief-0.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:52dc05445d8019b61a9ab8c6eb9d6238c4346ac692dcecca76d5f329a999216e", size = 3167188 },
{ url = "https://files.pythonhosted.org/packages/00/9c/1051c681702740d92bc125c74a5b74e20d873ee04686d9d6a44e028fe6bb/lief-0.16.4-cp312-cp312-win32.whl", hash = "sha256:06cd2432def66454785add6b8f14f2cf9f7ef4168cf2eb24367192953fb33206", size = 3053189 },
{ url = "https://files.pythonhosted.org/packages/39/6e/f4a2d1c0e47939d407d8fe894c78964249bd8efd172be75878054051b688/lief-0.16.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6363bf971135a4c65e2fa151e456995bb6ecb38c00f1ed9f8d4c6a625111556", size = 3182188 },
{ url = "https://files.pythonhosted.org/packages/3c/15/b1518db49c4d69232631133cd2b3b34a5691015c5e7394c5f38c0eba15c1/lief-0.16.4-cp313-cp313-win32.whl", hash = "sha256:9e103cc2bee8ff8d960ead8a094fc6aa9878b9ebeae0c1785d1e5bc2428ec4a4", size = 3053184 },
{ url = "https://files.pythonhosted.org/packages/ad/da/2b01fafc36aeb9a0c0949f5ffaba0a4562c6e77276fbd6b6cbf87387087c/lief-0.16.4-cp313-cp313-win_amd64.whl", hash = "sha256:775661661d0c4d33429f99e6cfd7f0e0c3781191be81c79eaa4a9f2c68929ebd", size = 3182245 },
]
[[package]]
@@ -665,7 +686,7 @@ wheels = [
[[package]]
name = "mcp"
version = "1.3.0"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -677,9 +698,9 @@ dependencies = [
{ name = "starlette" },
{ name = "uvicorn" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/81e5f2490290351fc97bf46c24ff935128cb7d34d68e3987b522f26f7ada/mcp-1.3.0.tar.gz", hash = "sha256:f409ae4482ce9d53e7ac03f3f7808bcab735bdfc0fba937453782efb43882d45", size = 150235 }
sdist = { url = "https://files.pythonhosted.org/packages/6d/c9/c55764824e893fdebe777ac7223200986a275c3191dba9169f8eb6d7c978/mcp-1.5.0.tar.gz", hash = "sha256:5b2766c05e68e01a2034875e250139839498c61792163a7b221fc170c12f5aa9", size = 159128 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d0/d2/a9e87b506b2094f5aa9becc1af5178842701b27217fa43877353da2577e3/mcp-1.3.0-py3-none-any.whl", hash = "sha256:2829d67ce339a249f803f22eba5e90385eafcac45c94b00cab6cef7e8f217211", size = 70672 },
{ url = "https://files.pythonhosted.org/packages/c1/d1/3ff566ecf322077d861f1a68a1ff025cad337417bd66ad22a7c6f7dfcfaf/mcp-1.5.0-py3-none-any.whl", hash = "sha256:51c3f35ce93cb702f7513c12406bbea9665ef75a08db909200b07da9db641527", size = 73734 },
]
[[package]]
@@ -711,16 +732,16 @@ wheels = [
[[package]]
name = "patchelf"
version = "0.17.2.1"
version = "0.17.2.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/83/ec/ac383eb82792e092d8037649b382cf78a7b79c2ce4e5b861f61519b9b14e/patchelf-0.17.2.1.tar.gz", hash = "sha256:a6eb0dd452ce4127d0d5e1eb26515e39186fa609364274bc1b0b77539cfa7031", size = 167484 }
sdist = { url = "https://files.pythonhosted.org/packages/0d/41/dc3ee5838db2d90be935adb53ae7745135d9c719d070b1989b246f983c7f/patchelf-0.17.2.2.tar.gz", hash = "sha256:080b2ac3074fd4ab257700088e82470425e56609aa0dd07abe548f04b7b3b007", size = 149517 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/09/b7ec52d01ad2936c967e929fd076b0a2e4f94ce950c9440e38f98c67675e/patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:fc329da0e8f628bd836dfb8eaf523547e342351fa8f739bf2b3fe4a6db5a297c", size = 421930 },
{ url = "https://files.pythonhosted.org/packages/69/47/e02357d1075cdf4b56be39a6c218a5a2b0bd3896011120ae3765190ab527/patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ccb266a94edf016efe80151172c26cff8c2ec120a57a1665d257b0442784195d", size = 381147 },
{ url = "https://files.pythonhosted.org/packages/92/78/3f4e19d6ba7acf1e01db6d82d23e0435a76466187644ce0dbcf6bd621317/patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:f47b5bdd6885cfb20abdd14c707d26eb6f499a7f52e911865548d4aa43385502", size = 488526 },
{ url = "https://files.pythonhosted.org/packages/08/d3/adedfbcff489605a0e907f390006ef7d59a3ac56a9e2dbe721ddbbb1e58e/patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:a9e6ebb0874a11f7ed56d2380bfaa95f00612b23b15f896583da30c2059fcfa8", size = 470060 },
{ url = "https://files.pythonhosted.org/packages/c6/9b/33e54d5916863904609e40b53206e5a49cf649b71f7f787eddcbc27a9f5c/patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:3c8d58f0e4c1929b1c7c45ba8da5a84a8f1aa6a82a46e1cfb2e44a4d40f350e5", size = 499565 },
{ url = "https://files.pythonhosted.org/packages/c6/73/c3105c973dd2afcdc5d946ee211d5c4ecdf9d27bb54ae835b144e706e86d/patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d1a9bc0d4fd80c038523ebdc451a1cce75237cfcc52dbd1aca224578001d5927", size = 425709 },
{ url = "https://files.pythonhosted.org/packages/8d/15/25b5d10d971f509fe6bc8951b855f0f05be4c24e0dd1616c14a6e1a9116a/patchelf-0.17.2.2-py3-none-manylinux1_i686.manylinux_2_5_i686.musllinux_1_1_i686.whl", hash = "sha256:3b8a4d7cccac04d8231dec321245611bf147b199cbf4da305d1a364ff689fb58", size = 524182 },
{ url = "https://files.pythonhosted.org/packages/f2/f9/e070956e350ccdfdf059251836f757ad91ac0c01b0ba3e033ea7188d8d42/patchelf-0.17.2.2-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:e334ebb1c5aa9fc740fd95ebe449271899fe1e45a3eb0941300b304f7e3d1299", size = 466519 },
{ url = "https://files.pythonhosted.org/packages/56/0d/dc3ac6c6e9e9d0d3e40bee1abe95a07034f83627319e60a7dc9abdbfafee/patchelf-0.17.2.2-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:3d32cd69442a229724f7f071b61cef1f87ccd80cf755af0b1ecefd553fa9ae3f", size = 462123 },
{ url = "https://files.pythonhosted.org/packages/1e/f6/b842b19c2b72df1c524ab3793c3ec9cf3926c7c841e0b64b34f95d7fb806/patchelf-0.17.2.2-py3-none-manylinux2014_armv7l.manylinux_2_17_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:05f6bbdbe484439cb025e20c60abd37e432e6798dfa3f39a072e6b7499072a8c", size = 412347 },
{ url = "https://files.pythonhosted.org/packages/a8/0b/33eb3087703d903dd01cf6b0d64e067bf3718a5e8b1239bc6fc2c4b1fdb2/patchelf-0.17.2.2-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:b54e79ceb444ec6a536a5dc2e8fc9c771ec6a1fa7d5f4dbb3dc0e5b8e5ff82e1", size = 522827 },
{ url = "https://files.pythonhosted.org/packages/4f/25/6379dc26714b5a40f51b3c7927d668b00a51517e857da7f3cb09d1d0bcb6/patchelf-0.17.2.2-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.musllinux_1_1_s390x.whl", hash = "sha256:24374cdbd9a072230339024fb6922577cb3231396640610b069f678bc483f21e", size = 565961 },
]
[[package]]
@@ -912,20 +933,20 @@ wheels = [
[[package]]
name = "pyright"
version = "1.1.395"
version = "1.1.397"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fb/47/a2e1dfd70f9f0db34f70d5b108c82be57bf24185af69c95acff57f9239fa/pyright-1.1.395.tar.gz", hash = "sha256:53703169068c160bfb41e1b44ba3e2512492869c26cfad927e1268cb3fbb1b1c", size = 3813566 }
sdist = { url = "https://files.pythonhosted.org/packages/92/23/cefa10c9cb198e0858ed0b9233371d62bca880337f628e58f50dfdfb12f0/pyright-1.1.397.tar.gz", hash = "sha256:07530fd65a449e4b0b28dceef14be0d8e0995a7a5b1bb2f3f897c3e548451ce3", size = 3818998 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/a1/531897f8caa6c6cc99862cd1c908ddd8a366a51d968e83ab4523ded98b30/pyright-1.1.395-py3-none-any.whl", hash = "sha256:f9bc726870e740c6c77c94657734d90563a3e9765bb523b39f5860198ed75eef", size = 5688787 },
{ url = "https://files.pythonhosted.org/packages/01/b5/98ec41e1e0ad5576ecd42c90ec363560f7b389a441722ea3c7207682dec7/pyright-1.1.397-py3-none-any.whl", hash = "sha256:2e93fba776e714a82b085d68f8345b01f91ba43e1ab9d513e79b70fc85906257", size = 5693631 },
]
[[package]]
name = "pytest"
version = "8.3.4"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -933,21 +954,21 @@ dependencies = [
{ name = "packaging" },
{ name = "pluggy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 }
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 },
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
]
[[package]]
name = "pytest-asyncio"
version = "0.25.3"
version = "0.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 }
sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 },
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 },
]
[[package]]
@@ -989,11 +1010,11 @@ wheels = [
[[package]]
name = "python-dotenv"
version = "1.0.1"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 },
]
[[package]]
@@ -1019,11 +1040,11 @@ wheels = [
[[package]]
name = "pytz"
version = "2025.1"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 }
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 },
]
[[package]]
@@ -1128,36 +1149,36 @@ wheels = [
[[package]]
name = "ruff"
version = "0.9.8"
version = "0.11.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e9/59/ac745a2492986a4c900c73a7a3a10eb4d7a3853e43443519bceecae5eefc/ruff-0.9.8.tar.gz", hash = "sha256:12d455f2be6fe98accbea2487bbb8eaec716c760bf60b45e7e13f76f913f56e9", size = 3715230 }
sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/1c/9de3a463279e9a203104fe80881d7dcfd8377eb52b3d5608770ea6ff3dc6/ruff-0.9.8-py3-none-linux_armv6l.whl", hash = "sha256:d236f0ce0190bbc6fa9b4c4b85e916fb4c50fd087e6558af1bf5a45eb20e374d", size = 10036520 },
{ url = "https://files.pythonhosted.org/packages/35/10/a4eda083ad0b60a4c16bc9a68c6eda59de69a3a58913a0b62541f5c551cd/ruff-0.9.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:59fac6922b336d0c38df199761ade561563e1b7636e3a2b767b9ee5a68aa9cbf", size = 10827099 },
{ url = "https://files.pythonhosted.org/packages/57/34/cf7e18f2315926ee2c98f931717e1302f8c3face189f5b99352eb48c5373/ruff-0.9.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a82082ec72bde2166ec138055307396c4d4e543fd97266dc2bfa24284cb30af6", size = 10161605 },
{ url = "https://files.pythonhosted.org/packages/f3/08/5e7e8fc08d193e3520b9227249a00bc9b8da9e0a20bf97bef03a9a9f0d38/ruff-0.9.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e06635d12321605d1d11226c7d3c6b1245a0df498099868d14b4e353b3f0ac22", size = 10338840 },
{ url = "https://files.pythonhosted.org/packages/54/c0/df2187618b87334867ea7942f6d2d79ea3e5cb3ed709cfa5c8df115d3715/ruff-0.9.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:65961815bb35d427e957940d13b2a1d0a67d8b245d3a7e0b5a4a2058536d3532", size = 9891009 },
{ url = "https://files.pythonhosted.org/packages/fb/39/8fc50b87203e71e6f3281111813ab0f3d6095cb1129efc2cf4c33e977657/ruff-0.9.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c18356beaef174797ad83f11debc5569e96afa73a549b2d073912565cfc4cfd1", size = 11413420 },
{ url = "https://files.pythonhosted.org/packages/6a/7b/53cd91b99a1cef31126859fb98fdc347c47e0047a9ec51391ea28f08284d/ruff-0.9.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a1dfc443bee0288ea926a4d9ecfd858bf94ddf0a03a256c63e81b2b6dccdfc7d", size = 12138017 },
{ url = "https://files.pythonhosted.org/packages/1a/d4/949a328934202a2d2641dcd759761d8ed806e672cbbad0a88e20a46c43ba/ruff-0.9.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc86d5a85cd5ab1d5aff1650f038aa34681d0692cc2467aa9ddef37bd56ea3f9", size = 11592548 },
{ url = "https://files.pythonhosted.org/packages/c6/8e/8520a4d97eefedb8472811fd5144fcb1fcbb29f83bb9bb4356a468e7eeac/ruff-0.9.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:66662aa19535d58fe6d04e5b59a39e495b102f2f5a2a1b9698e240eb78f429ef", size = 13787277 },
{ url = "https://files.pythonhosted.org/packages/24/68/f1629e00dbc5c9adcd31f12f9438b68c50ab0eefca8b07e11b6c94f11b09/ruff-0.9.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:733647b2fe9367e1aa049c0eba296363746f3bc0dbfd454b0bc4b7b46cdf0146", size = 11275421 },
{ url = "https://files.pythonhosted.org/packages/28/65/c133462f179b925e49910532c7d7b5a244df5995c155cd2ab9452545926f/ruff-0.9.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:100031be9777f67af7f61b4d4eea2a0531ed6788940aca4360f6b9aae317c53b", size = 10220273 },
{ url = "https://files.pythonhosted.org/packages/d8/1e/9339aef1896470380838385dbdc91f62998c37d406009f05ff3b810265f3/ruff-0.9.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f090758d58b4667d9022eee1085a854db93d800279e5a177ebda5adc1faf639", size = 9860266 },
{ url = "https://files.pythonhosted.org/packages/ca/33/2a2934860df6bd3665776ec686fc33910e7a1b793bdd2f000aea3e8f0b65/ruff-0.9.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f774998b9c9a062510533aba9b53085de6be6d41e13a7a0bd086af8a40e838c3", size = 10831947 },
{ url = "https://files.pythonhosted.org/packages/74/66/0a7677b1cda4b2367a654f9af57f1dbe58f38c6704da88aee9bbf3941197/ruff-0.9.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6ef7cc80626264ab8ab4d68b359ba867b8a52b0830a9643cd31289146dd40892", size = 11306767 },
{ url = "https://files.pythonhosted.org/packages/c4/90/6c98f94e036c8acdf19bd8f3f84d246e43cbcc950e24dc7ff85d2f2735ba/ruff-0.9.8-py3-none-win32.whl", hash = "sha256:54b57b623a683e696a1ede99db95500763c1badafe105b6ad8d8e9d96e385ae2", size = 10234107 },
{ url = "https://files.pythonhosted.org/packages/f5/e7/35877491b4b64daa35cbd7dc06aa5969e7bb1cd6f69e5594e4376dfbc16d/ruff-0.9.8-py3-none-win_amd64.whl", hash = "sha256:b0878103b2fb8af55ad701308a69ce713108ad346c3a3a143ebcd1e13829c9a7", size = 11357825 },
{ url = "https://files.pythonhosted.org/packages/6e/98/de77a972b2e9ded804dea5d4e6fbfa093d99e81092602567787ea87979af/ruff-0.9.8-py3-none-win_arm64.whl", hash = "sha256:e459a4fc4150fcc60da26c59a6a4b70878c60a99df865a71cf6f958dc68c419a", size = 10435420 },
{ url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 },
{ url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 },
{ url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 },
{ url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 },
{ url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 },
{ url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 },
{ url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 },
{ url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 },
{ url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 },
{ url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 },
{ url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 },
{ url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 },
{ url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 },
{ url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 },
{ url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 },
{ url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 },
{ url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 },
]
[[package]]
name = "setuptools"
version = "75.8.2"
version = "75.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d1/53/43d99d7687e8cdef5ab5f9ec5eaf2c0423c2b35133a2b7e7bc276fc32b21/setuptools-75.8.2.tar.gz", hash = "sha256:4880473a969e5f23f2a2be3646b2dfd84af9028716d398e46192f84bc36900d2", size = 1344083 }
sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/38/7d7362e031bd6dc121e5081d8cb6aa6f6fedf2b67bf889962134c6da4705/setuptools-75.8.2-py3-none-any.whl", hash = "sha256:558e47c15f1811c1fa7adbd0096669bf76c1d3f433f58324df69f3f5ecac4e8f", size = 1229385 },
{ url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 },
]
[[package]]
@@ -1189,31 +1210,31 @@ wheels = [
[[package]]
name = "sqlalchemy"
version = "2.0.38"
version = "2.0.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e4/08/9a90962ea72acd532bda71249a626344d855c4032603924b1b547694b837/sqlalchemy-2.0.38.tar.gz", hash = "sha256:e5a4d82bdb4bf1ac1285a68eab02d253ab73355d9f0fe725a97e1e0fa689decb", size = 9634782 }
sdist = { url = "https://files.pythonhosted.org/packages/00/8e/e77fcaa67f8b9f504b4764570191e291524575ddbfe78a90fc656d671fdc/sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22", size = 9644602 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/f8/6d0424af1442c989b655a7b5f608bc2ae5e4f94cdf6df9f6054f629dc587/SQLAlchemy-2.0.38-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12d5b06a1f3aeccf295a5843c86835033797fea292c60e72b07bcb5d820e6dd3", size = 2104927 },
{ url = "https://files.pythonhosted.org/packages/25/80/fc06e65fca0a19533e2bfab633a5633ed8b6ee0b9c8d580acf84609ce4da/SQLAlchemy-2.0.38-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e036549ad14f2b414c725349cce0772ea34a7ab008e9cd67f9084e4f371d1f32", size = 2095317 },
{ url = "https://files.pythonhosted.org/packages/98/2d/5d66605f76b8e344813237dc160a01f03b987201e974b46056a7fb94a874/SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3bee874cb1fadee2ff2b79fc9fc808aa638670f28b2145074538d4a6a5028e", size = 3244735 },
{ url = "https://files.pythonhosted.org/packages/73/8d/b0539e8dce90861efc38fea3eefb15a5d0cfeacf818614762e77a9f192f9/SQLAlchemy-2.0.38-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185ea07a99ce8b8edfc788c586c538c4b1351007e614ceb708fd01b095ef33e", size = 3255581 },
{ url = "https://files.pythonhosted.org/packages/ac/a5/94e1e44bf5bdffd1782807fcc072542b110b950f0be53f49e68b5f5eca1b/SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b79ee64d01d05a5476d5cceb3c27b5535e6bb84ee0f872ba60d9a8cd4d0e6579", size = 3190877 },
{ url = "https://files.pythonhosted.org/packages/91/13/f08b09996dce945aec029c64f61c13b4788541ac588d9288e31e0d3d8850/SQLAlchemy-2.0.38-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:afd776cf1ebfc7f9aa42a09cf19feadb40a26366802d86c1fba080d8e5e74bdd", size = 3217485 },
{ url = "https://files.pythonhosted.org/packages/13/8f/8cfe2ba5ba6d8090f4de0e658330c53be6b7bf430a8df1b141c2b180dcdf/SQLAlchemy-2.0.38-cp312-cp312-win32.whl", hash = "sha256:a5645cd45f56895cfe3ca3459aed9ff2d3f9aaa29ff7edf557fa7a23515a3725", size = 2075254 },
{ url = "https://files.pythonhosted.org/packages/c2/5c/e3c77fae41862be1da966ca98eec7fbc07cdd0b00f8b3e1ef2a13eaa6cca/SQLAlchemy-2.0.38-cp312-cp312-win_amd64.whl", hash = "sha256:1052723e6cd95312f6a6eff9a279fd41bbae67633415373fdac3c430eca3425d", size = 2100865 },
{ url = "https://files.pythonhosted.org/packages/21/77/caa875a1f5a8a8980b564cc0e6fee1bc992d62d29101252561d0a5e9719c/SQLAlchemy-2.0.38-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ecef029b69843b82048c5b347d8e6049356aa24ed644006c9a9d7098c3bd3bfd", size = 2100201 },
{ url = "https://files.pythonhosted.org/packages/f4/ec/94bb036ec78bf9a20f8010c807105da9152dd84f72e8c51681ad2f30b3fd/SQLAlchemy-2.0.38-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c8bcad7fc12f0cc5896d8e10fdf703c45bd487294a986903fe032c72201596b", size = 2090678 },
{ url = "https://files.pythonhosted.org/packages/7b/61/63ff1893f146e34d3934c0860209fdd3925c25ee064330e6c2152bacc335/SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a0ef3f98175d77180ffdc623d38e9f1736e8d86b6ba70bff182a7e68bed7727", size = 3177107 },
{ url = "https://files.pythonhosted.org/packages/a9/4f/b933bea41a602b5f274065cc824fae25780ed38664d735575192490a021b/SQLAlchemy-2.0.38-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b0ac78898c50e2574e9f938d2e5caa8fe187d7a5b69b65faa1ea4648925b096", size = 3190435 },
{ url = "https://files.pythonhosted.org/packages/f5/23/9e654b4059e385988de08c5d3b38a369ea042f4c4d7c8902376fd737096a/SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9eb4fa13c8c7a2404b6a8e3772c17a55b1ba18bc711e25e4d6c0c9f5f541b02a", size = 3123648 },
{ url = "https://files.pythonhosted.org/packages/83/59/94c6d804e76ebc6412a08d2b086a8cb3e5a056cd61508e18ddaf3ec70100/SQLAlchemy-2.0.38-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dba1cdb8f319084f5b00d41207b2079822aa8d6a4667c0f369fce85e34b0c86", size = 3151789 },
{ url = "https://files.pythonhosted.org/packages/b2/27/17f143013aabbe1256dce19061eafdce0b0142465ce32168cdb9a18c04b1/SQLAlchemy-2.0.38-cp313-cp313-win32.whl", hash = "sha256:eae27ad7580529a427cfdd52c87abb2dfb15ce2b7a3e0fc29fbb63e2ed6f8120", size = 2073023 },
{ url = "https://files.pythonhosted.org/packages/e2/3e/259404b03c3ed2e7eee4c179e001a07d9b61070334be91124cf4ad32eec7/SQLAlchemy-2.0.38-cp313-cp313-win_amd64.whl", hash = "sha256:b335a7c958bc945e10c522c069cd6e5804f4ff20f9a744dd38e748eb602cbbda", size = 2096908 },
{ url = "https://files.pythonhosted.org/packages/aa/e4/592120713a314621c692211eba034d09becaf6bc8848fabc1dc2a54d8c16/SQLAlchemy-2.0.38-py3-none-any.whl", hash = "sha256:63178c675d4c80def39f1febd625a6333f44c0ba269edd8a468b156394b27753", size = 1896347 },
{ url = "https://files.pythonhosted.org/packages/98/86/b2cb432aeb00a1eda7ed33ce86d943c2452dc1642f3ec51bfe9eaae9604b/sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b", size = 2107210 },
{ url = "https://files.pythonhosted.org/packages/bf/b0/b2479edb3419ca763ba1b587161c292d181351a33642985506a530f9162b/sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c", size = 2097599 },
{ url = "https://files.pythonhosted.org/packages/58/5e/c5b792a4abcc71e68d44cb531c4845ac539d558975cc61db1afbc8a73c96/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47", size = 3247012 },
{ url = "https://files.pythonhosted.org/packages/e0/a8/055fa8a7c5f85e6123b7e40ec2e9e87d63c566011d599b4a5ab75e033017/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd", size = 3257851 },
{ url = "https://files.pythonhosted.org/packages/f6/40/aec16681e91a22ddf03dbaeb3c659bce96107c5f47d2a7c665eb7f24a014/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a", size = 3193155 },
{ url = "https://files.pythonhosted.org/packages/21/9d/cef697b137b9eb0b66ab8e9cf193a7c7c048da3b4bb667e5fcea4d90c7a2/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06", size = 3219770 },
{ url = "https://files.pythonhosted.org/packages/57/05/e109ca7dde837d8f2f1b235357e4e607f8af81ad8bc29c230fed8245687d/sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109", size = 2077567 },
{ url = "https://files.pythonhosted.org/packages/97/c6/25ca068e38c29ed6be0fde2521888f19da923dbd58f5ff16af1b73ec9b58/sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338", size = 2103136 },
{ url = "https://files.pythonhosted.org/packages/32/47/55778362642344324a900b6b2b1b26f7f02225b374eb93adc4a363a2d8ae/sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87", size = 2102484 },
{ url = "https://files.pythonhosted.org/packages/1b/e1/f5f26f67d095f408138f0fb2c37f827f3d458f2ae51881546045e7e55566/sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716", size = 2092955 },
{ url = "https://files.pythonhosted.org/packages/c5/c2/0db0022fc729a54fc7aef90a3457bf20144a681baef82f7357832b44c566/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4", size = 3179367 },
{ url = "https://files.pythonhosted.org/packages/33/b7/f33743d87d0b4e7a1f12e1631a4b9a29a8d0d7c0ff9b8c896d0bf897fb60/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d", size = 3192705 },
{ url = "https://files.pythonhosted.org/packages/c9/74/6814f31719109c973ddccc87bdfc2c2a9bc013bec64a375599dc5269a310/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e", size = 3125927 },
{ url = "https://files.pythonhosted.org/packages/e8/6b/18f476f4baaa9a0e2fbc6808d8f958a5268b637c8eccff497bf96908d528/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723", size = 3154055 },
{ url = "https://files.pythonhosted.org/packages/b4/60/76714cecb528da46bc53a0dd36d1ccef2f74ef25448b630a0a760ad07bdb/sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7", size = 2075315 },
{ url = "https://files.pythonhosted.org/packages/5b/7c/76828886d913700548bac5851eefa5b2c0251ebc37921fe476b93ce81b50/sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0", size = 2099175 },
{ url = "https://files.pythonhosted.org/packages/7b/0f/d69904cb7d17e65c65713303a244ec91fd3c96677baf1d6331457fd47e16/sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f", size = 1898621 },
]
[[package]]
@@ -1231,14 +1252,23 @@ wheels = [
[[package]]
name = "starlette"
version = "0.45.3"
version = "0.46.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 }
sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 },
{ url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
]
[[package]]
name = "striprtf"
version = "0.0.28"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/3d/b3806c11f90f795284ab19b9561d547779ba8c3acc22a3907ac6afe9ec61/striprtf-0.0.28.tar.gz", hash = "sha256:902806a2e0821faf412130450bdbb84f15e996a729061a51fe7286c620b6fee3", size = 7196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/63/18dc0365c0d3edc7e81a3c1cef2015079fdf58c8b681fa79ad952be7925b/striprtf-0.0.28-py3-none-any.whl", hash = "sha256:d441f32aeb730c347ccbbbbb06a057b8df1a4e46df0bdb9fca548c601988929c", size = 7667 },
]
[[package]]
@@ -1267,23 +1297,23 @@ wheels = [
[[package]]
name = "tzdata"
version = "2025.1"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 },
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
]
[[package]]
name = "tzlocal"
version = "5.3"
version = "5.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/33/cc/11360404b20a6340b9b4ed39a3338c4af47bc63f87f6cea94dbcbde07029/tzlocal-5.3.tar.gz", hash = "sha256:2fafbfc07e9d8b49ade18f898d6bcd37ae88ce3ad6486842a2e4f03af68323d2", size = 30480 }
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/9f/1c0b69d3abf4c65acac051ad696b8aea55afbb746dea8017baab53febb5e/tzlocal-5.3-py3-none-any.whl", hash = "sha256:3814135a1bb29763c6e4f08fd6e41dbb435c7a60bfbb03270211bcc537187d8c", size = 17920 },
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026 },
]
[[package]]
@@ -1377,33 +1407,33 @@ wheels = [
[[package]]
name = "websockets"
version = "15.0"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/7a/8bc4d15af7ff30f7ba34f9a172063bfcee9f5001d7cef04bee800a658f33/websockets-15.0.tar.gz", hash = "sha256:ca36151289a15b39d8d683fd8b7abbe26fc50be311066c5f8dcf3cb8cee107ab", size = 175574 }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/1e/92c4547d7b2a93f848aedaf37e9054111bc00dc11bff4385ca3f80dbb412/websockets-15.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cccc18077acd34c8072578394ec79563664b1c205f7a86a62e94fafc7b59001f", size = 174709 },
{ url = "https://files.pythonhosted.org/packages/9f/37/eae4830a28061ba552516d84478686b637cd9e57d6a90b45ad69e89cb0af/websockets-15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4c22992e24f12de340ca5f824121a5b3e1a37ad4360b4e1aaf15e9d1c42582d", size = 172372 },
{ url = "https://files.pythonhosted.org/packages/46/2f/b409f8b8aa9328d5a47f7a301a43319d540d70cf036d1e6443675978a988/websockets-15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1206432cc6c644f6fc03374b264c5ff805d980311563202ed7fef91a38906276", size = 172607 },
{ url = "https://files.pythonhosted.org/packages/d6/81/d7e2e4542d4b4df849b0110df1b1f94f2647b71ab4b65d672090931ad2bb/websockets-15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d3cc75ef3e17490042c47e0523aee1bcc4eacd2482796107fd59dd1100a44bc", size = 182422 },
{ url = "https://files.pythonhosted.org/packages/b6/91/3b303160938d123eea97f58be363f7dbec76e8c59d587e07b5bc257dd584/websockets-15.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b89504227a5311610e4be16071465885a0a3d6b0e82e305ef46d9b064ce5fb72", size = 181362 },
{ url = "https://files.pythonhosted.org/packages/f2/8b/df6807f1ca339c567aba9a7ab03bfdb9a833f625e8d2b4fc7529e4c701de/websockets-15.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e3efe356416bc67a8e093607315951d76910f03d2b3ad49c4ade9207bf710d", size = 181787 },
{ url = "https://files.pythonhosted.org/packages/21/37/e6d3d5ebb0ebcaf98ae84904205c9dcaf3e0fe93e65000b9f08631ed7309/websockets-15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0f2205cdb444a42a7919690238fb5979a05439b9dbb73dd47c863d39640d85ab", size = 182058 },
{ url = "https://files.pythonhosted.org/packages/c9/df/6aca296f2be4c638ad20908bb3d7c94ce7afc8d9b4b2b0780d1fc59b359c/websockets-15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:aea01f40995fa0945c020228ab919b8dfc93fc8a9f2d3d705ab5b793f32d9e99", size = 181434 },
{ url = "https://files.pythonhosted.org/packages/88/f1/75717a982bab39bbe63c83f9df0e7753e5c98bab907eb4fb5d97fe5c8c11/websockets-15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9f8e33747b1332db11cf7fcf4a9512bef9748cb5eb4d3f7fbc8c30d75dc6ffc", size = 181431 },
{ url = "https://files.pythonhosted.org/packages/e7/15/cee9e63ed9ac5bfc1a3ae8fc6c02c41745023c21eed622eef142d8fdd749/websockets-15.0-cp312-cp312-win32.whl", hash = "sha256:32e02a2d83f4954aa8c17e03fe8ec6962432c39aca4be7e8ee346b05a3476904", size = 175678 },
{ url = "https://files.pythonhosted.org/packages/4e/00/993974c60f40faabb725d4dbae8b072ef73b4c4454bd261d3b1d34ace41f/websockets-15.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc02b159b65c05f2ed9ec176b715b66918a674bd4daed48a9a7a590dd4be1aa", size = 176119 },
{ url = "https://files.pythonhosted.org/packages/12/23/be28dc1023707ac51768f848d28a946443041a348ee3a54abdf9f6283372/websockets-15.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d2244d8ab24374bed366f9ff206e2619345f9cd7fe79aad5225f53faac28b6b1", size = 174714 },
{ url = "https://files.pythonhosted.org/packages/8f/ff/02b5e9fbb078e7666bf3d25c18c69b499747a12f3e7f2776063ef3fb7061/websockets-15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3a302241fbe825a3e4fe07666a2ab513edfdc6d43ce24b79691b45115273b5e7", size = 172374 },
{ url = "https://files.pythonhosted.org/packages/8e/61/901c8d4698e0477eff4c3c664d53f898b601fa83af4ce81946650ec2a4cb/websockets-15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:10552fed076757a70ba2c18edcbc601c7637b30cdfe8c24b65171e824c7d6081", size = 172605 },
{ url = "https://files.pythonhosted.org/packages/d2/4b/dc47601a80dff317aecf8da7b4ab278d11d3494b2c373b493e4887561f90/websockets-15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c53f97032b87a406044a1c33d1e9290cc38b117a8062e8a8b285175d7e2f99c9", size = 182380 },
{ url = "https://files.pythonhosted.org/packages/83/f7/b155d2b38f05ed47a0b8de1c9ea245fcd7fc625d89f35a37eccba34b42de/websockets-15.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1caf951110ca757b8ad9c4974f5cac7b8413004d2f29707e4d03a65d54cedf2b", size = 181325 },
{ url = "https://files.pythonhosted.org/packages/d3/ff/040a20c01c294695cac0e361caf86f33347acc38f164f6d2be1d3e007d9f/websockets-15.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf1ab71f9f23b0a1d52ec1682a3907e0c208c12fef9c3e99d2b80166b17905f", size = 181763 },
{ url = "https://files.pythonhosted.org/packages/cb/6a/af23e93678fda8341ac8775e85123425e45c608389d3514863c702896ea5/websockets-15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bfcd3acc1a81f106abac6afd42327d2cf1e77ec905ae11dc1d9142a006a496b6", size = 182097 },
{ url = "https://files.pythonhosted.org/packages/7e/3e/1069e159c30129dc03c01513b5830237e576f47cedb888777dd885cae583/websockets-15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8c5c8e1bac05ef3c23722e591ef4f688f528235e2480f157a9cfe0a19081375", size = 181485 },
{ url = "https://files.pythonhosted.org/packages/9a/a7/c91c47103f1cd941b576bbc452601e9e01f67d5c9be3e0a9abe726491ab5/websockets-15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:86bfb52a9cfbcc09aba2b71388b0a20ea5c52b6517c0b2e316222435a8cdab72", size = 181466 },
{ url = "https://files.pythonhosted.org/packages/16/32/a4ca6e3d56c24aac46b0cf5c03b841379f6409d07fc2044b244f90f54105/websockets-15.0-cp313-cp313-win32.whl", hash = "sha256:26ba70fed190708551c19a360f9d7eca8e8c0f615d19a574292b7229e0ae324c", size = 175673 },
{ url = "https://files.pythonhosted.org/packages/c0/31/25a417a23e985b61ffa5544f9facfe4a118cb64d664c886f1244a8baeca5/websockets-15.0-cp313-cp313-win_amd64.whl", hash = "sha256:ae721bcc8e69846af00b7a77a220614d9b2ec57d25017a6bbde3a99473e41ce8", size = 176115 },
{ url = "https://files.pythonhosted.org/packages/e8/b2/31eec524b53f01cd8343f10a8e429730c52c1849941d1f530f8253b6d934/websockets-15.0-py3-none-any.whl", hash = "sha256:51ffd53c53c4442415b613497a34ba0aa7b99ac07f1e4a62db5dcd640ae6c3c3", size = 169023 },
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
]
[[package]]