diff --git a/.cursor/rules/nextjs/nextjs.mdc b/.cursor/rules/nextjs/nextjs.mdc new file mode 100644 index 0000000..68ca801 --- /dev/null +++ b/.cursor/rules/nextjs/nextjs.mdc @@ -0,0 +1,293 @@ +--- +description: +globs: *.ts,*.tsx +alwaysApply: false +--- +--- +description: This rule provides comprehensive guidance for Next.js development, covering code organization, performance, security, testing, and common pitfalls. It helps developers build robust, scalable, and maintainable Next.js applications by adhering to community-accepted best practices and coding standards. +globs: *.js,*.jsx,*.ts,*.tsx +--- +# Next.js Best Practices + +This document outlines best practices for developing Next.js applications, focusing on code organization, performance optimization, security, testing strategies, and common pitfalls to avoid. Adhering to these guidelines will help you build robust, scalable, and maintainable applications. + +## 1. Code Organization and Structure + +### Directory Structure + +* **`app/`**: (Recommended - Next.js 13+) Contains route handlers, server components, and client components. + * `page.tsx`: Represents the UI for a route. + * `layout.tsx`: Defines the layout for a route and its children. + * `loading.tsx`: Displays a loading UI while a route segment is loading. + * `error.tsx`: Handles errors within a route segment. + * `head.tsx`: Manages the `` metadata for a route. + * `route.ts`: Defines server-side route handlers (API routes). + * `[dynamic-segment]`: Dynamic route segments, using brackets. + * `@folder-name`: Route Groups to organize routes without affecting URL structure. +* **`pages/`**: (Legacy - Before Next.js 13) Contains page components. + * `api/`: Serverless functions (API routes). + * `_app.js/tsx`: Custom App component (wraps all pages). + * `_document.js/tsx`: Custom Document component (control the entire HTML document). +* **`components/`**: Reusable UI components. +* **`lib/`**: Utility functions, helper functions, and third-party integrations. +* **`hooks/`**: Custom React hooks. +* **`styles/`**: Global styles and CSS modules. +* **`public/`**: Static assets (images, fonts, etc.). +* **`types/`**: TypeScript type definitions and interfaces. +* **`utils/`**: Contains utilities and helper functions, along with any API-related logic. + +**Recommendation:** Prefer the `app/` directory structure for new projects as it aligns with the latest Next.js features and best practices. When using `pages/`, keep it simple and migrate to `app/` when feasible. + +### File Naming Conventions + +* **Components:** `ComponentName.jsx` or `ComponentName.tsx` +* **Pages:** `page.js`, `page.jsx`, `page.ts`, `page.tsx` (within the `app` or `pages` directory) +* **Layouts:** `layout.js`, `layout.jsx`, `layout.ts`, `layout.tsx` (within the `app` directory) +* **API Routes:** `route.js`, `route.ts` (within the `app/api` directory or `pages/api` directory) +* **Hooks:** `useHookName.js` or `useHookName.ts` +* **Styles:** `ComponentName.module.css` or `ComponentName.module.scss` +* **Types:** `types.ts` or `interfaces.ts` + +### Module Organization + +* **Co-location:** Keep related components, styles, and tests in the same directory. +* **Feature-based modules:** Group files by feature rather than type (e.g., `components/user-profile/`, not `components/button`, `components/form`). +* **Avoid deeply nested directories:** Keep the directory structure relatively flat to improve navigation. + +### Component Architecture + +* **Presentational vs. Container Components:** Separate components that handle data fetching and state management (container components) from those that only render UI (presentational components). +* **Atomic Design:** Organize components into atoms, molecules, organisms, templates, and pages for better reusability and maintainability. +* **Composition over inheritance:** Favor composition to create flexible and reusable components. +* **Server Components (app directory):** Use server components by default for improved performance. Only use client components when interactivity (event handlers, useState, useEffect) is required. + +### Code Splitting + +* **Dynamic imports:** Use `next/dynamic` to load components only when they are needed, improving initial load time. Example: `dynamic(() => import('../components/MyComponent'))`. +* **Route-level code splitting:** Next.js automatically splits code based on routes, so each page only loads the necessary JavaScript. +* **Granular code splitting:** Break down large components into smaller chunks that can be loaded independently. + +## 2. Common Patterns and Anti-patterns + +### Design Patterns + +* **Higher-Order Components (HOCs):** Reusable component logic. +* **Render Props:** Sharing code between React components using a prop whose value is a function. +* **Hooks:** Extracting stateful logic into reusable functions. +* **Context API:** Managing global state. +* **Compound Components:** Combining multiple components that work together implicitly. + +### Recommended Approaches + +* **Data fetching:** Use `getServerSideProps` or `getStaticProps` or server components for fetching data on the server-side. Use `SWR` or `React Query` for client-side data fetching and caching. +* **Styling:** Use CSS Modules, Styled Components, or Tailwind CSS for component-level styling. Prefer Tailwind CSS for rapid development. +* **State Management:** Use React Context, Zustand, Jotai, or Recoil for managing global state. Redux is an option, but often overkill for smaller Next.js projects. +* **Form Handling:** Use `react-hook-form` for managing forms and validation. +* **API Routes:** Use Next.js API routes for serverless functions. + +### Anti-patterns and Code Smells + +* **Over-fetching data:** Only fetch the data that is needed by the component. +* **Blocking the main thread:** Avoid long-running synchronous operations in the main thread. +* **Mutating state directly:** Always use `setState` or hooks to update state. +* **Not memoizing components:** Use `React.memo` to prevent unnecessary re-renders. +* **Using `useEffect` without a dependency array:** Ensure the dependency array is complete to prevent unexpected behavior. +* **Writing server side code in client components:** Can expose secrets or cause unexpected behavior. + +### State Management + +* **Local State:** Use `useState` for component-specific state. +* **Context API:** Use `useContext` for application-wide state that doesn't change often. +* **Third-party libraries:** Use `Zustand`, `Jotai`, or `Recoil` for more complex state management needs. These are simpler and more performant alternatives to Redux for many Next.js use cases. + +### Error Handling + +* **`try...catch`:** Use `try...catch` blocks for handling errors in asynchronous operations. +* **Error Boundary Components:** Create reusable error boundary components to catch errors in child components. Implement `getDerivedStateFromError` or `componentDidCatch` lifecycle methods. +* **Centralized error logging:** Log errors to a central service like Sentry or Bugsnag. +* **Custom Error Pages:** Use `_error.js` or `_error.tsx` to create custom error pages. +* **Route-level error handling (app directory):** Use `error.tsx` within route segments to handle errors specific to that route. + +## 3. Performance Considerations + +### Optimization Techniques + +* **Image optimization:** Use `next/image` component for automatic image optimization, including lazy loading and responsive images. +* **Font optimization:** Use `next/font` to optimize font loading and prevent layout shift. +* **Code splitting:** Use dynamic imports and route-level code splitting to reduce initial load time. +* **Caching:** Use caching strategies (e.g., `Cache-Control` headers, `SWR`, `React Query`) to reduce data fetching overhead. +* **Memoization:** Use `React.memo` to prevent unnecessary re-renders of components. +* **Prefetching:** Use the `` tag to prefetch pages that are likely to be visited. +* **SSR/SSG:** Use Static Site Generation (SSG) for content that doesn't change often and Server-Side Rendering (SSR) for dynamic content. +* **Incremental Static Regeneration (ISR):** Use ISR to update statically generated pages on a regular interval. + +### Memory Management + +* **Avoid memory leaks:** Clean up event listeners and timers in `useEffect` hooks. +* **Minimize re-renders:** Only update state when necessary to reduce the number of re-renders. +* **Use immutable data structures:** Avoid mutating data directly to prevent unexpected side effects. + +### Rendering Optimization + +* **Server Components (app directory):** Render as much as possible on the server to reduce client-side JavaScript. +* **Client Components (app directory):** Only use client components when interactivity is required. Defer rendering of non-critical client components using `React.lazy`. + +### Bundle Size Optimization + +* **Analyze bundle size:** Use tools like `webpack-bundle-analyzer` to identify large dependencies. +* **Remove unused code:** Use tree shaking to remove unused code from your bundles. +* **Use smaller dependencies:** Replace large dependencies with smaller, more lightweight alternatives. +* **Compression:** Enable Gzip or Brotli compression on your server to reduce the size of the transferred files. + +### Lazy Loading + +* **Images:** Use `next/image` for automatic lazy loading of images. +* **Components:** Use `next/dynamic` for lazy loading of components. +* **Intersection Observer:** Use the Intersection Observer API for manual lazy loading of content. + +## 4. Security Best Practices + +### Common Vulnerabilities + +* **Cross-Site Scripting (XSS):** Sanitize user input to prevent XSS attacks. Be especially careful when rendering HTML directly from user input. +* **Cross-Site Request Forgery (CSRF):** Use CSRF tokens to protect against CSRF attacks. +* **SQL Injection:** Use parameterized queries or an ORM to prevent SQL injection attacks. +* **Authentication and Authorization vulnerabilities:** Implement secure authentication and authorization mechanisms. Avoid storing secrets in client-side code. +* **Exposing sensitive data:** Protect API keys and other sensitive data by storing them in environment variables and accessing them on the server-side. + +### Input Validation + +* **Server-side validation:** Always validate user input on the server-side. +* **Client-side validation:** Use client-side validation for immediate feedback, but don't rely on it for security. +* **Sanitize input:** Sanitize user input to remove potentially malicious code. +* **Use a validation library:** Use a library like `zod` or `yup` for validating user input. + +### Authentication and Authorization + +* **Use a secure authentication provider:** Use a service like Auth0, NextAuth.js, or Firebase Authentication for secure authentication. +* **Store tokens securely:** Store tokens in HTTP-only cookies or local storage. +* **Implement role-based access control:** Use role-based access control to restrict access to sensitive resources. +* **Protect API endpoints:** Use authentication middleware to protect API endpoints. + +### Data Protection + +* **Encrypt sensitive data:** Encrypt sensitive data at rest and in transit. +* **Use HTTPS:** Use HTTPS to encrypt communication between the client and the server. +* **Regularly update dependencies:** Keep your dependencies up to date to patch security vulnerabilities. +* **Secure environment variables:** Never commit environment variables to your repository. Use a secrets management tool if necessary. + +### Secure API Communication + +* **Use HTTPS:** Use HTTPS for all API communication. +* **Authenticate API requests:** Use API keys or tokens to authenticate API requests. +* **Rate limiting:** Implement rate limiting to prevent abuse of your API. +* **Input validation:** Validate all API request parameters. +* **Output encoding:** Properly encode API responses to prevent injection attacks. + +## 5. Testing Approaches + +### Unit Testing + +* **Test individual components:** Write unit tests for individual components to ensure they are working correctly. +* **Use a testing framework:** Use a testing framework like Jest or Mocha. +* **Mock dependencies:** Mock external dependencies to isolate components during testing. +* **Test edge cases:** Test edge cases and error conditions to ensure the component is robust. +* **Use React Testing Library:** Prefer React Testing Library for component testing as it encourages testing from a user perspective, promoting better accessibility and more robust tests. + +### Integration Testing + +* **Test interactions between components:** Write integration tests to ensure that components are working together correctly. +* **Test API calls:** Test API calls to ensure that data is being fetched and saved correctly. +* **Use a testing framework:** Use a testing framework like Jest or Mocha with libraries like `msw` (Mock Service Worker) to intercept and mock API calls. + +### End-to-End Testing + +* **Test the entire application:** Write end-to-end tests to ensure that the entire application is working correctly. +* **Use a testing framework:** Use a testing framework like Cypress or Playwright. +* **Test user flows:** Test common user flows to ensure that the application is providing a good user experience. +* **Focus on critical paths:** Prioritize end-to-end tests for critical user flows to ensure application stability. + +### Test Organization + +* **Co-locate tests with components:** Keep tests in the same directory as the components they are testing. +* **Use a consistent naming convention:** Use a consistent naming convention for test files (e.g., `ComponentName.test.js`). +* **Organize tests by feature:** Organize tests by feature to improve maintainability. + +### Mocking and Stubbing + +* **Mock external dependencies:** Mock external dependencies to isolate components during testing. +* **Stub API calls:** Stub API calls to prevent network requests during testing. +* **Use a mocking library:** Use a mocking library like Jest's built-in mocking capabilities or `msw`. + +## 6. Common Pitfalls and Gotchas + +### Frequent Mistakes + +* **Not understanding server-side rendering:** Failing to utilize SSR effectively can impact SEO and initial load performance. +* **Over-complicating state management:** Using Redux for simple state management needs can add unnecessary complexity. +* **Not optimizing images:** Not using `next/image` can result in large image sizes and slow loading times. +* **Ignoring security best practices:** Neglecting security can lead to vulnerabilities. +* **Not testing the application thoroughly:** Insufficient testing can result in bugs and regressions. +* **Accidentally exposing API keys or secrets in client-side code.** + +### Edge Cases + +* **Handling errors gracefully:** Implement proper error handling to prevent the application from crashing. +* **Dealing with different screen sizes:** Ensure the application is responsive and works well on different screen sizes. +* **Supporting different browsers:** Test the application in different browsers to ensure compatibility. +* **Managing complex data structures:** Use appropriate data structures and algorithms to efficiently manage complex data. + +### Version-Specific Issues + +* **Breaking changes:** Be aware of breaking changes when upgrading Next.js versions. +* **Deprecated features:** Avoid using deprecated features. +* **Compatibility with third-party libraries:** Ensure that third-party libraries are compatible with the Next.js version being used. + +### Compatibility Concerns + +* **Browser compatibility:** Ensure that the application is compatible with the target browsers. +* **Third-party library compatibility:** Ensure that third-party libraries are compatible with Next.js. + +### Debugging Strategies + +* **Use the browser developer tools:** Use the browser developer tools to inspect the DOM, debug JavaScript, and analyze network requests. +* **Use console.log statements:** Use `console.log` statements to debug code. +* **Use a debugger:** Use a debugger to step through code and inspect variables. +* **Use error logging:** Log errors to a central service to track and analyze issues. + +## 7. Tooling and Environment + +### Recommended Development Tools + +* **VS Code:** Code editor with excellent support for JavaScript, TypeScript, and React. +* **ESLint:** Linter for identifying and fixing code style issues. +* **Prettier:** Code formatter for automatically formatting code. +* **Chrome Developer Tools:** Browser developer tools for debugging and profiling. +* **React Developer Tools:** Browser extension for inspecting React components. +* **Webpack Bundle Analyzer:** Tool for analyzing the size of the Webpack bundle. + +### Build Configuration + +* **Use environment variables:** Store configuration values in environment variables. +* **Use a build script:** Use a build script to automate the build process. +* **Optimize build settings:** Optimize build settings for production (e.g., enable minification, tree shaking). + +### Linting and Formatting + +* **Use ESLint with recommended rules:** Configure ESLint with a set of recommended rules for JavaScript and React. +* **Use Prettier for automatic formatting:** Configure Prettier to automatically format code on save. +* **Integrate linting and formatting into the build process:** Integrate linting and formatting into the build process to ensure that code is always consistent. +* **Use a shared configuration:** Ensure that all developers are using the same linting and formatting configuration. + +### Deployment + +* **Use Vercel for easy deployment:** Vercel is the recommended platform for deploying Next.js applications. +* **Use a CDN for static assets:** Use a CDN to serve static assets from a location that is geographically close to the user. +* **Configure caching:** Configure caching to improve performance and reduce server load. +* **Monitor application health:** Monitor application health to detect and resolve issues quickly. + +### CI/CD Integration + +* **Use a CI/CD pipeline:** Use a CI/CD pipeline to automate the build, test, and deployment process. +* **Run tests in the CI/CD pipeline:** Run tests in the CI/CD pipeline to ensure that code is working correctly before it is deployed. +* **Automate deployments:** Automate deployments to reduce the risk of human error. \ No newline at end of file diff --git a/.cursor/rules/project/always.mdc b/.cursor/rules/project/always.mdc new file mode 100644 index 0000000..e911b89 --- /dev/null +++ b/.cursor/rules/project/always.mdc @@ -0,0 +1,15 @@ +--- +description: +globs: +alwaysApply: true +--- +Important: Use English to write codes and comments. + +This is a long-term project in the early, internal development stage and not yet online. + +Please use best practices and optimal architecture for long-term maintenance. +When modifying code and documentation, backward compatibility does not need to be considered, and the code version should always be 1.0.0. Do not include comments about deprecation or compatibility with old versions. Immediately remove any unused code instead of retaining it. + +When receiving requirements from the user, first rephrase them using professional technical terminology to ensure mutual understanding and allow for corrections. The user acknowledges they may not use precise technical language, so clarification is encouraged. + +Prioritize professional engineering judgment over literal instruction following. You are authorized to modify or extend requirements based on code quality, maintainability, and best practices. Focus on implementing the user's intent rather than their exact words, making technical decisions that serve the long-term health of the codebase. diff --git a/.cursor/rules/project/clean-code.mdc b/.cursor/rules/project/clean-code.mdc new file mode 100644 index 0000000..cf9827f --- /dev/null +++ b/.cursor/rules/project/clean-code.mdc @@ -0,0 +1,57 @@ +--- +description: +globs: *.py +alwaysApply: false +--- +# Clean Code Guidelines + +## Constants Over Magic Numbers +- Replace hard-coded values with named constants +- Use descriptive constant names that explain the value's purpose +- Keep constants at the top of the file or in a dedicated constants file + +## Meaningful Names +- Variables, functions, and classes should reveal their purpose +- Names should explain why something exists and how it's used +- Avoid abbreviations unless they're universally understood + +## Smart Comments +- Don't comment on what the code does - make the code self-documenting +- Use comments to explain why something is done a certain way +- Document APIs, complex algorithms, and non-obvious side effects +- Write docstrings to make the function's intent, usage, and parameters fully clear to someone who only reads the docstring. Save implementation details for the docstring, and reserve high-level, conceptual explanations for the documentation. + +## Single Responsibility +- Each function should do exactly one thing +- Functions should be small and focused +- If a function needs a comment to explain what it does, it should be split + +## DRY (Don't Repeat Yourself) +- Extract repeated code into reusable functions +- Share common logic through proper abstraction +- Maintain single sources of truth + +## Clean Structure +- Keep related code together +- Organize code in a logical hierarchy +- Use consistent file and folder naming conventions + +## Encapsulation +- Hide implementation details +- Expose clear interfaces +- Move nested conditionals into well-named functions + +## Code Quality Maintenance +- Refactor continuously +- Fix technical debt early +- Leave code cleaner than you found it + +## Testing +- Write tests before fixing bugs +- Keep tests readable and maintainable +- Test edge cases and error conditions + +## Version Control +- Write clear commit messages +- Make small, focused commits +- Use meaningful branch names \ No newline at end of file diff --git a/.cursor/rules/project/docs.mdc b/.cursor/rules/project/docs.mdc new file mode 100644 index 0000000..3a61dea --- /dev/null +++ b/.cursor/rules/project/docs.mdc @@ -0,0 +1,12 @@ +--- +description: +globs: +alwaysApply: true +--- +Maintain all documentation in Chinese (but use English to write code). For overviews, refer to the docs/**/readme.md files. Use tables, code blocks, and mermaid flowcharts to make the documentation clear and visually appealing. + +Avoid including actual code examples whenever possible. For API documentation, use generic configuration examples with comments explaining what should be filled in, rather than specific plugin configurations. + +When providing configuration file examples, always use real examples found in the codebase—do not make them up. However, for API request/response examples, use generic placeholders with descriptive comments to ensure the documentation remains universally applicable across different plugins. + +Focus on explaining the purpose and structure of parameters rather than specific values, emphasizing the distinction between plugin_config (for instance creation and routing) and custom (for runtime parameter overrides). \ No newline at end of file diff --git a/.cursor/rules/python/fastapi.mdc b/.cursor/rules/python/fastapi.mdc new file mode 100644 index 0000000..d4c1d82 --- /dev/null +++ b/.cursor/rules/python/fastapi.mdc @@ -0,0 +1,91 @@ +--- +description: +globs: +alwaysApply: false +--- +--- +description: FastAPI best practices and patterns for building modern Python web APIs +globs: **/*.py, app/**/*.py, api/**/*.py +--- + +# FastAPI Best Practices + +## Project Structure +- Use proper directory structure +- Implement proper module organization +- Use proper dependency injection +- Keep routes organized by domain +- Implement proper middleware +- Use proper configuration management + +## API Design +- Use proper HTTP methods +- Implement proper status codes +- Use proper request/response models +- Implement proper validation +- Use proper error handling +- Document APIs with OpenAPI + +## Models +- Use Pydantic models +- Implement proper validation +- Use proper type hints +- Keep models organized +- Use proper inheritance +- Implement proper serialization + +## Database +- Use proper ORM (SQLAlchemy) +- Implement proper migrations +- Use proper connection pooling +- Implement proper transactions +- Use proper query optimization +- Handle database errors properly + +## Authentication +- Implement proper JWT authentication +- Use proper password hashing +- Implement proper role-based access +- Use proper session management +- Implement proper OAuth2 +- Handle authentication errors properly + +## Security +- Implement proper CORS +- Use proper rate limiting +- Implement proper input validation +- Use proper security headers +- Handle security errors properly +- Implement proper logging + +## Performance +- Use proper caching +- Implement proper async operations +- Use proper background tasks +- Implement proper connection pooling +- Use proper query optimization +- Monitor performance metrics + +## Testing +- Write proper unit tests +- Implement proper integration tests +- Use proper test fixtures +- Implement proper mocking +- Test error scenarios +- Use proper test coverage + +## Deployment +- Use proper Docker configuration +- Implement proper CI/CD +- Use proper environment variables +- Implement proper logging +- Use proper monitoring +- Handle deployment errors properly + +## Documentation +- Use proper docstrings +- Implement proper API documentation +- Use proper type hints +- Keep documentation updated +- Document error scenarios +- Use proper versioning \ No newline at end of file diff --git a/.cursor/rules/python/pydantic/description.mdc b/.cursor/rules/python/pydantic/description.mdc new file mode 100644 index 0000000..e9d0ba1 --- /dev/null +++ b/.cursor/rules/python/pydantic/description.mdc @@ -0,0 +1,21 @@ +--- +description: +globs: **/models/**/*.py +alwaysApply: false +--- +Description: +Guidelines for writing user-friendly field descriptions in Pydantic models: + +- **Tone**: Use clear, present tense language. Avoid overly technical jargon. +- **Format**: Remove unnecessary annotations like "(Required)" or "(Optional)" - this info is already in Field definitions. +- **Voice**: Use active voice and directly state the field's purpose and function. +- **Length**: Keep descriptions concise but complete with all necessary information. +- **Technical Terms**: When technical terms are required, provide simple explanations. +- **Consistency**: Use similar phrasing patterns for fields with similar functions. +- **Punctuation**: End all descriptions with periods for consistency. + +Examples: +- Good: "The unique identifier for this world." +- Bad: "Unique World ID. (Required)" +- Good: "Whether interactions in this world influence the character's memory." +- Bad: "Does this world's interactions influence this specific character's long-term memory? (Required)." \ No newline at end of file diff --git a/.cursor/rules/python/pydantic/pydantic.mdc b/.cursor/rules/python/pydantic/pydantic.mdc new file mode 100644 index 0000000..0178fcf --- /dev/null +++ b/.cursor/rules/python/pydantic/pydantic.mdc @@ -0,0 +1,212 @@ +--- +description: +globs: **/models/**/*.py +alwaysApply: false +--- +--- +description: Comprehensive best practices and coding standards for utilizing Pydantic effectively in Python projects, covering code organization, performance, security, and testing. +globs: *.py +--- +- **Model Definition:** + - Use `BaseModel` to define data schemas with type annotations for clarity and automatic validation. + - Prefer simple models that encapsulate a single concept to maintain readability and manageability. + - Use nested models for complex data structures while ensuring each model has clear validation rules. + - Always define a `Config` class within your model to control model behavior. + +- **Validation and Error Handling:** + - Implement built-in and custom validators to enforce data integrity. + - Utilize `@field_validator` for field-specific rules and `@root_validator` for cross-field validation. + - Ensure that validation errors are user-friendly and logged for debugging purposes. + - Custom error messages should be informative and guide the user on how to correct the data. + - Use `ValidationError` to catch and handle validation errors. + +- **Performance Optimization:** + - Consider using lazy initialization and avoid redundant validation where data is already trusted. + - Use Pydantic's configuration options to control when validation occurs, which can significantly enhance performance in high-throughput applications. + - Use `model_rebuild` to dynamically rebuild models when related schema change. + - Consider using `__slots__` in the `Config` class to reduce memory footprint. + +- **Code Organization and Structure:** + - **Directory Structure:** + - Adopt a modular structure: `src/`, `tests/`, `docs/`. + - Models can reside in `src/models/`. + - Validators in `src/validators/`. + - Example: + + project_root/ + ├── src/ + │ ├── __init__.py + │ ├── models/ + │ │ ├── __init__.py + │ │ ├── user.py + │ │ ├── item.py + │ ├── validators/ + │ │ ├── __init__.py + │ │ ├── user_validators.py + │ ├── main.py # Application entry point + ├── tests/ + │ ├── __init__.py + │ ├── test_user.py + │ ├── test_item.py + ├── docs/ + │ ├── ... + ├── .env + ├── pyproject.toml + ├── README.md + + - **File Naming:** + - Use snake_case for file names (e.g., `user_model.py`). + - Name model files after the primary model they define (e.g., `user.py` for `UserModel`). + - **Module Organization:** + - Group related models and validators into separate modules. + - Utilize `__init__.py` to make modules importable. + - **Component Architecture:** + - Employ a layered architecture (e.g., data access, business logic, presentation). + - Pydantic models are primarily used in the data access layer. + - **Code Splitting:** + - Split large models into smaller, manageable components using composition. + - Leverage inheritance judiciously. + +- **Common Patterns and Anti-patterns:** + - **Design Patterns:** + - **Data Transfer Object (DTO):** Pydantic models serve as DTOs. + - **Factory Pattern:** Create model instances using factory functions for complex initialization. + - **Repository Pattern:** Use repositories to abstract data access and validation logic. + - **Recommended Approaches:** + - Centralize validation logic in dedicated validator functions. + - Utilize nested models for complex data structures. + - Use `BaseSettings` for managing application settings. + - **Anti-patterns:** + - Embedding business logic directly into models. + - Overly complex inheritance hierarchies. + - Ignoring validation errors. + - Performing I/O operations within validator functions. + - **State Management:** + - Use immutable models whenever possible to simplify state management. + - Consider using state management libraries like `attrs` or `dataclasses` in conjunction with Pydantic for complex applications. + - **Error Handling:** + - Raise `ValidationError` exceptions when validation fails. + - Provide informative error messages. + - Log validation errors for debugging. + +- **Performance Considerations:** + - **Optimization Techniques:** + - Use `model_rebuild` to recompile models when their schema changes. + - Leverage `__slots__` in the `Config` class to reduce memory footprint. + - Use the `@cached_property` decorator to cache expensive computations. + - **Memory Management:** + - Be mindful of large lists or dictionaries within models, as they can consume significant memory. + - Use generators or iterators for processing large datasets. + - **Efficient Data Parsing:** + - Utilize `model_validate_json` and `model_validate` for efficient data parsing. + - **Controlling Validation:** + - Use `validate_default` and `validate_assignment` options in the `Config` class to control validation occurrence. + +- **Security Best Practices:** + - **Common Vulnerabilities:** + - Injection attacks (e.g., SQL injection) if model data is used directly in database queries. + - Cross-site scripting (XSS) if model data is displayed in web pages without proper escaping. + - Deserialization vulnerabilities if models are deserialized from untrusted sources. + - **Input Validation:** + - Always validate all incoming data using Pydantic models. + - Use appropriate type annotations and constraints to restrict input values. + - Sanitize input data to remove potentially harmful characters or sequences. + - **Authentication and Authorization:** + - Use authentication and authorization mechanisms to restrict access to sensitive data. + - Implement role-based access control (RBAC) to grant different levels of access to different users. + - **Data Protection:** + - Encrypt sensitive data at rest and in transit. + - Use secure storage mechanisms for storing API keys and other secrets. + - Mask sensitive data in logs and error messages. + - **Secure API Communication:** + - Use HTTPS for all API communication. + - Implement API rate limiting to prevent denial-of-service attacks. + +- **Testing Approaches:** + - **Unit Testing:** + - Test individual models and validators in isolation. + - Use parameterized tests to cover different input values and scenarios. + - Verify that validation errors are raised correctly. + - **Integration Testing:** + - Test the interaction between models and other components of the application. + - Use mock objects to simulate external dependencies. + - **End-to-End Testing:** + - Test the entire application flow from end to end. + - Use automated testing tools to simulate user interactions. + - **Test Organization:** + - Organize tests into separate modules based on the component being tested. + - Use descriptive test names to indicate the purpose of each test. + - **Mocking and Stubbing:** + - Use mock objects to simulate external dependencies such as databases or APIs. + - Use stub objects to provide predefined responses for certain functions or methods. + +- **Common Pitfalls and Gotchas:** + - **Frequent Mistakes:** + - Misusing Union Types: Using `Union` incorrectly can complicate type validation and handling. + - Optional Fields without Default Values: Forgetting to provide a default value for optional fields can lead to `None` values causing errors in your application. + - Incorrect Type Annotations: Assigning incorrect types to fields can cause validation to fail. For example, using `str` for a field that should be an `int`. + - **Edge Cases:** + - Handling complex validation logic with dependencies between fields. + - Dealing with large or deeply nested data structures. + - Handling different input formats (e.g., JSON, CSV). + - **Version-Specific Issues:** + - Be aware of breaking changes between Pydantic versions. + - Consult the Pydantic documentation for migration guides. + - **Compatibility Concerns:** + - Ensure compatibility between Pydantic and other libraries used in your project. + - Be mindful of potential conflicts with other validation libraries. + +- **Tooling and Environment:** + - **Development Tools:** + - Use a code editor or IDE with Pydantic support (e.g., VS Code with the Pylance extension). + - Use a static type checker like MyPy to catch type errors. + - Use a linter like Flake8 or Pylint to enforce code style. + - **Build Configuration:** + - Use a build tool like Poetry or Pipenv to manage dependencies. + - Specify Pydantic as a dependency in your project's configuration file. + - **Linting and Formatting:** + - Configure a linter and formatter to enforce consistent code style. + - Use pre-commit hooks to automatically run linters and formatters before committing code. + - **Deployment:** + - Use a deployment platform that supports Python applications (e.g., Heroku, AWS Elastic Beanstalk, Docker). + - Configure your deployment environment to install Pydantic and its dependencies. + - **CI/CD:** + - Integrate Pydantic tests into your CI/CD pipeline. + - Automatically run tests and linters on every commit. + +- **Getting Started with Pydantic:** + - Install Pydantic with `pip install pydantic` + - Define your data models using `BaseModel` and type hints + - Validate your data by instantiating the data models + - Handle validation errors using `try...except ValidationError` + +- **Example:** + python + from pydantic import BaseModel, ValidationError + from typing import List, Optional + + class Address(BaseModel): + street: str + city: str + zip_code: Optional[str] = None + + class User(BaseModel): + id: int + name: str + email: str + addresses: List[Address] + + try: + user_data = { + "id": 1, + "name": "John Doe", + "email": "invalid-email", + "addresses": [{ + "street": "123 Main St", + "city": "Anytown" + }] + } + user = User(**user_data) + print(user) + except ValidationError as e: + print(e.json()) \ No newline at end of file diff --git a/.cursor/rules/python/uv.mdc b/.cursor/rules/python/uv.mdc new file mode 100644 index 0000000..c688d93 --- /dev/null +++ b/.cursor/rules/python/uv.mdc @@ -0,0 +1,69 @@ +--- +description: +globs: pyproject.toml +alwaysApply: false +--- + +# Package Management with `uv` + +These rules define strict guidelines for managing Python dependencies in this project using the `uv` dependency manager. + +**✅ Use `uv` exclusively** + +- All Python dependencies **must be installed, synchronized, and locked** using `uv`. +- Never use `pip`, `pip-tools`, or `poetry` directly for dependency management. + +**🔁 Managing Dependencies** + +Always use these commands: + +```bash +# Add or upgrade dependencies +uv add + +# Remove dependencies +uv remove + +# Reinstall all dependencies from lock file +uv sync +``` + +**🔁 Scripts** + +```bash +# Run script with proper dependencies +uv run script.py +``` + +You can edit inline-metadata manually: + +```python +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "torch", +# "torchvision", +# "opencv-python", +# "numpy", +# "matplotlib", +# "Pillow", +# "timm", +# ] +# /// + +print("some python code") +``` + +Or using uv cli: + +```bash +# Add or upgrade script dependencies +uv add package-name --script script.py + +# Remove script dependencies +uv remove package-name --script script.py + +# Reinstall all script dependencies from lock file +uv sync --script script.py +``` + \ No newline at end of file diff --git a/.cursorignore b/.cursorignore new file mode 100644 index 0000000..1328631 --- /dev/null +++ b/.cursorignore @@ -0,0 +1,2 @@ +# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv) +!.env \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1a1d645 --- /dev/null +++ b/.env.example @@ -0,0 +1,118 @@ +# OLV Launcher Environment Configuration File +# Copy this file as .env and modify the configuration as needed + +# ============== Server Settings ============== +# Server listening address (Default: localhost) +# Use 0.0.0.0 to allow access from external devices +OLV_LAUNCHER_HOST=localhost + +# Server port number (Default: 7000) +# Range: 1024-65535 +OLV_LAUNCHER_PORT=7000 + +# Development mode auto-reload (Default: false) +# Enable only in development environment +OLV_LAUNCHER_RELOAD=false + +# ============== Plugin Settings ============== +# Plugin directory path (Default: plugins) +# Path relative to the project root directory +OLV_LAUNCHER_PLUGINS_DIR=plugins + +# Default port ranges (Default: ["8001-8020", "9001-9020", "10001-10020"]) +# List of port ranges for plugin port allocation, separated by commas +OLV_LAUNCHER_DEFAULT_PORT_RANGES=8001-8020,9001-9020,10001-10020 + +# ============== Service Settings ============== +# Health check interval (Default: 30 seconds) +# Minimum value: 5 seconds +OLV_LAUNCHER_HEALTH_CHECK_INTERVAL=30 + +# Plugin startup timeout (Default: 600 seconds) +# Minimum value: 30 seconds +OLV_LAUNCHER_PLUGIN_STARTUP_TIMEOUT=600 + +# Plugin health check timeout (Default: 10 seconds) +# Minimum value: 1 second +OLV_LAUNCHER_PLUGIN_HEALTH_TIMEOUT=10 + +# ============== Logging Settings ============== +# Log level (Default: INFO) +# Available values: DEBUG, INFO, WARNING, ERROR, CRITICAL +OLV_LAUNCHER_LOG_LEVEL=INFO + +# Log format (Default: %(asctime)s - %(name)s - %(levelname)s - %(message)s) +# Python logging format string +OLV_LAUNCHER_LOG_FORMAT=%(asctime)s - %(name)s - %(levelname)s - %(message)s + +# ============== CORS Settings ============== +# Allowed CORS origins (Default: ["*"]) +# Separate multiple origins with commas, * means allow all origins +OLV_LAUNCHER_CORS_ORIGINS=* + +# Allow CORS credentials (Default: true) +OLV_LAUNCHER_CORS_CREDENTIALS=true + +# Allowed CORS methods (Default: ["*"]) +# Separate multiple methods with commas, * means allow all methods +OLV_LAUNCHER_CORS_METHODS=* + +# Allowed CORS headers (Default: ["*"]) +# Separate multiple headers with commas, * means allow all headers +OLV_LAUNCHER_CORS_HEADERS=* + +# ============== Environment Sync Settings ============== +# Enable UV environment sync (Default: true) +# Automatically synchronize Python environment dependencies +OLV_LAUNCHER_ENABLE_UV_SYNC=true + +# UV sync timeout (Default: 300 seconds) +# Minimum value: 60 seconds +OLV_LAUNCHER_UV_SYNC_TIMEOUT=300 + +# ============== UI Configuration (olv-launcher-ui) ============== +# Backend API base URL +# Used for the frontend to connect to the OLV Launcher backend +NEXT_PUBLIC_API_BASE_URL=http://localhost:7000 + +# ============== OLV Main Core Settings ============== +# Server host address (Default: localhost) +OLV_CORE_HOST=localhost + +# Server port number (Default: 12393) +# Range: 1024-65535 +OLV_CORE_PORT=12393 + +# URL for Core to connect to the Launcher service. +# The value should be the base URL of the OLV Launcher. +# If your Launcher runs on a different host or port, update this URL accordingly. +OLV_CORE_LAUNCHER_URL=http://127.0.0.1:7000 + +# Enable auto-reload in development (Default: false) +OLV_CORE_RELOAD=false + +# Health check interval in seconds (Default: 30) +# Minimum value: 5 seconds +OLV_CORE_HEALTH_CHECK_INTERVAL=30 + +# Logging level (Default: INFO) +# Available values: DEBUG, INFO, WARNING, ERROR, CRITICAL +OLV_CORE_LOG_LEVEL=INFO + +# Log format string (Default: %(asctime)s - %(name)s - %(levelname)s - %(message)s) +OLV_CORE_LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s - %(message)s" + +# Allowed CORS origins (Default: ["*"]) +# Separate multiple origins with commas, * means allow all origins +OLV_CORE_CORS_ORIGINS=* + +# Allow credentials in CORS (Default: true) +OLV_CORE_CORS_CREDENTIALS=true + +# Allowed CORS methods (Default: ["*"]) +# Separate multiple methods with commas, * means allow all methods +OLV_CORE_CORS_METHODS=* + +# Allowed CORS headers (Default: ["*"]) +# Separate multiple headers with commas, * means allow all headers +OLV_CORE_CORS_HEADERS=* \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore index e43b0f9..7007875 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,38 @@ +# macOS system files .DS_Store + +# Python files +__pycache__/ +*.pyc +lab.py +.idea +*.egg-info + +# Virtual environment +.venv +.conda +conda + +# Sensitive data +.env +.env.local + +# Logs +logs/* + +# Cache and models +cache/* +plugins/**/models/* + +# Others +private + +# Next.js +node_modules/ +.next/ +out/ +build/ + +OLV1/ +workspaces.json +.marketplace_cache/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc47d11 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ + +本项目使用 uv 作为 Python 虚拟环境与依赖管理工具。 + +请先安装 [uv](https://docs.astral.sh/uv/getting-started/installation/) + + +## 如何运行 olv_launcher 后端? + + +接着,直接运行 + +```sh +uv run run_launcher.py +``` + +文档在 `127.0.0.1:7000/doc`,管理前端需要单独启动。 + +## 前端 + +管理界面前端需要单独启动。 + +```sh +npm install +``` + +```sh +npm run dev +``` + + + +## 如何运行 websocket 服务器? + +```sh +uv run run_ws.py +``` +服务器将预设运行在 `http://localhost:12393` 上,websocket 端点在 `ws://localhost:12393/api/v1/ws//` (未来可能会修改,具体请参考代码 `src/olv_main/api/v1/endpoints/ws.py`) + +可以使用类似 yaak,postman 或类似工具连接 websocket 端点进行调试。 + +## 测试 + +写在 `tests` 目录下,pytest 会自己去找测试代码进行测试。 + +```sh +uv run pytest +``` + diff --git a/docs/development/cheetsheet.md b/docs/development/cheetsheet.md new file mode 100644 index 0000000..0d45e3c --- /dev/null +++ b/docs/development/cheetsheet.md @@ -0,0 +1,5 @@ +### Get the Project Structure + +```bash +tree -I "__pycache__|*.pyc|.git|.venv|.ruff_cache|.cursor|node_modules" --dirsfirst -L 3 +``` \ No newline at end of file diff --git a/docs/development/launcher/api-reference.md b/docs/development/launcher/api-reference.md new file mode 100644 index 0000000..9fd5248 --- /dev/null +++ b/docs/development/launcher/api-reference.md @@ -0,0 +1,7 @@ +# API 参考 (API Reference) + +Launcher 和 plugin 均使用 FastAPI 开发,提供交互式文档。 + +在启动 OLV Launcher 后,通过访问 `launcher_url/docs` 可以查看交互式 API 文档。也可以访问 `launcher_url/redoc` 获取 ReDoc 版本的文档。 + +对于启动的 plugin,也可以访问 `plugin_url/docs` 和 `plugin_url/redoc` 查看其 API 文档。 \ No newline at end of file diff --git a/docs/development/launcher/configuration.md b/docs/development/launcher/configuration.md new file mode 100644 index 0000000..2529be1 --- /dev/null +++ b/docs/development/launcher/configuration.md @@ -0,0 +1,156 @@ +# OLV Launcher 配置管理 + +OLV Launcher 使用基于 Pydantic BaseSettings 的配置管理系统,支持环境变量、.env 文件和默认值的多层配置加载机制。 + +## 配置加载优先级 + +配置系统按以下优先级加载设置: + +1. **环境变量** - 最高优先级,以 `OLV_LAUNCHER_` 为前缀 +2. **.env 文件** - 项目根目录下的 .env 文件 +3. **默认值** - 代码中定义的默认配置 + +## 配置文件设置 + +### 创建配置文件 + +1. 复制示例配置文件: +```bash +cp .env.example .env +cp olv-launcher-ui/.env.example olv-launcher-ui/.env +``` + +2. 根据需要修改 `.env` 文件中的配置项 + +## Launcher Server + +控制 OLV Launcher 服务器的基本运行参数。 + +| 配置项 | 环境变量 | 类型 | 默认值 | 描述 | +|--------|----------|------|--------|------| +| `host` | `OLV_LAUNCHER_HOST` | string | `localhost` | 服务器监听地址 | +| `port` | `OLV_LAUNCHER_PORT` | int | `7000` | 服务器端口号 (1024-65535) | +| `reload` | `OLV_LAUNCHER_RELOAD` | bool | `false` | 开发模式自动重载 | + +**示例配置:** +```bash +# 允许外部访问 +OLV_LAUNCHER_HOST=0.0.0.0 +OLV_LAUNCHER_PORT=8080 + +# 开发环境启用自动重载 +OLV_LAUNCHER_RELOAD=true +``` + +### 插件配置 + +管理插件发现、加载和端口分配的相关设置。 + +| 配置项 | 环境变量 | 类型 | 默认值 | 描述 | +|--------|----------|------|--------|------| +| `plugins_dir` | `OLV_LAUNCHER_PLUGINS_DIR` | string | `plugins` | 插件目录路径 | +| `default_port_ranges` | `OLV_LAUNCHER_DEFAULT_PORT_RANGES` | list | `["8001-8020", "9001-9020", "10001-10020"]` | 端口分配范围 | + +**示例配置:** +```bash +# 自定义插件目录 +OLV_LAUNCHER_PLUGINS_DIR=/opt/olv/plugins + +# 扩展端口范围 +OLV_LAUNCHER_DEFAULT_PORT_RANGES=8001-8050,9001-9050,10001-10050 +``` + +### 服务管理配置 + +控制插件生命周期管理和健康检查的时间参数。 + +| 配置项 | 环境变量 | 类型 | 默认值 | 最小值 | 描述 | +|--------|----------|------|--------|--------|------| +| `health_check_interval` | `OLV_LAUNCHER_HEALTH_CHECK_INTERVAL` | int | `30` | `5` | 健康检查间隔(秒) | +| `plugin_startup_timeout` | `OLV_LAUNCHER_PLUGIN_STARTUP_TIMEOUT` | int | `600` | `30` | 插件启动超时(秒) | +| `plugin_health_timeout` | `OLV_LAUNCHER_PLUGIN_HEALTH_TIMEOUT` | int | `10` | `1` | 健康检查超时(秒) | + +**示例配置:** +```bash +# 更频繁的健康检查 +OLV_LAUNCHER_HEALTH_CHECK_INTERVAL=15 + +# 延长启动超时(适用于大型模型) +OLV_LAUNCHER_PLUGIN_STARTUP_TIMEOUT=1200 +``` + +### 日志配置 + +控制系统日志输出的级别和格式。 + +| 配置项 | 环境变量 | 类型 | 默认值 | 可选值 | 描述 | +|--------|----------|------|--------|--------|------| +| `log_level` | `OLV_LAUNCHER_LOG_LEVEL` | string | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` | 日志级别 | +| `log_format` | `OLV_LAUNCHER_LOG_FORMAT` | string | `%(asctime)s - %(name)s - %(levelname)s - %(message)s` | Python logging 格式 | 日志格式字符串 | + +**示例配置:** +```bash +# 开发环境使用详细日志 +OLV_LAUNCHER_LOG_LEVEL=DEBUG + +# 自定义日志格式 +OLV_LAUNCHER_LOG_FORMAT=[%(levelname)s] %(asctime)s - %(name)s: %(message)s +``` + +### CORS 配置 + +配置跨域资源共享(CORS)策略,用于前端应用访问。 + +| 配置项 | 环境变量 | 类型 | 默认值 | 描述 | +|--------|----------|------|--------|------| +| `cors_origins` | `OLV_LAUNCHER_CORS_ORIGINS` | list | `["*"]` | 允许的源地址 | +| `cors_credentials` | `OLV_LAUNCHER_CORS_CREDENTIALS` | bool | `true` | 允许携带凭据 | +| `cors_methods` | `OLV_LAUNCHER_CORS_METHODS` | list | `["*"]` | 允许的HTTP方法 | +| `cors_headers` | `OLV_LAUNCHER_CORS_HEADERS` | list | `["*"]` | 允许的请求头 | + +**示例配置:** +```bash +# 限制特定域名访问 +OLV_LAUNCHER_CORS_ORIGINS=http://localhost:3000,https://myapp.com + +# 限制HTTP方法 +OLV_LAUNCHER_CORS_METHODS=GET,POST,PUT,DELETE + +# 自定义允许的头部 +OLV_LAUNCHER_CORS_HEADERS=Content-Type,Authorization,X-Requested-With +``` + +### 环境同步配置 + +控制 Python 环境依赖的自动同步功能。 + +| 配置项 | 环境变量 | 类型 | 默认值 | 最小值 | 描述 | +|--------|----------|------|--------|--------|------| +| `enable_uv_sync` | `OLV_LAUNCHER_ENABLE_UV_SYNC` | bool | `true` | - | 启用UV环境同步 | +| `uv_sync_timeout` | `OLV_LAUNCHER_UV_SYNC_TIMEOUT` | int | `300` | `60` | UV同步超时(秒) | + +**示例配置:** +```bash +# 禁用自动环境同步 +OLV_LAUNCHER_ENABLE_UV_SYNC=false + +# 延长同步超时 +OLV_LAUNCHER_UV_SYNC_TIMEOUT=600 +``` + +## Launcher UI + +OLV Launcher UI 使用独立的配置系统,通过环境变量进行配置。 + +| 配置项 | 环境变量 | 类型 | 默认值 | 描述 | +|--------|----------|------|--------|------| +| `apiBaseUrl` | `NEXT_PUBLIC_API_BASE_URL` | string | `http://localhost:7000` | 后端API基础地址 | +| `apiTimeout` | `NEXT_PUBLIC_API_TIMEOUT` | number | `600000` | API请求超时时间(毫秒) | +| `port` | `PORT` | number | `3000` | 前端开发服务器端口号 | +| `nodeEnv` | `NODE_ENV` | string | `development` | 运行环境模式 | + +## 相关文档 + +- [部署指南](./deployment.md) - 详细的部署配置 +- [插件管理器](./plugin-manager.md) - 插件配置和管理 +- [API 参考](./api-reference.md) - 配置相关的API端点 \ No newline at end of file diff --git a/docs/development/launcher/deployment.md b/docs/development/launcher/deployment.md new file mode 100644 index 0000000..a060cd9 --- /dev/null +++ b/docs/development/launcher/deployment.md @@ -0,0 +1,39 @@ +# 部署指南 (Deployment) + +本指南介绍如何部署和运行 OLV Launcher 服务。OLV Launcher 使用 `uv` 作为 Python 包管理工具,提供便捷的依赖管理和启动方式。 + +## 环境要求 + +## 安装与设置 + +### 1. 安装依赖 + +使用 `uv` 同步项目依赖: + +```bash +uv sync +``` + +### 2. 环境配置 + +参考 [配置管理](./configuration.md) 修改 `.env` 文件配置 Launcher 设置(可选)。 + +## 启动服务 + +```bash +uv run run_launcher.py +``` + +## 服务验证 +访问交互式 API 文档: + +- **Swagger UI**: http://localhost:7000/docs +- **ReDoc**: http://localhost:7000/redoc + +在 Swagger UI 中,你可以: + +- 查看所有可用的 API 端点 +- 测试插件管理功能 +- 查看插件发现和启动状态 +- 监控端口分配情况 +- 实时查看插件日志 \ No newline at end of file diff --git a/docs/development/launcher/models.md b/docs/development/launcher/models.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/development/launcher/plugin-manager.md b/docs/development/launcher/plugin-manager.md new file mode 100644 index 0000000..3bce8a9 --- /dev/null +++ b/docs/development/launcher/plugin-manager.md @@ -0,0 +1,424 @@ +# 插件管理器 (Plugin Manager) + +OLV Launcher 的插件管理器 (`UnifiedPluginManager`) 负责统一管理所有服务类型(ASR, TTS, LLM)的插件。它处理插件的发现、环境同步、生命周期控制、端口分配和日志记录。 + +## 核心职责 + +`UnifiedPluginManager` 的主要职责包括: + +* **插件发现**: 扫描指定目录(通常由 `settings.plugins_dir` 配置,默认为 `"plugins/"`),根据 `plugin.json` 和目录结构识别合法的插件。 +* **环境同步**: 若启用 (`settings.enable_uv_sync`),自动使用 `uv sync` 命令同步插件的 Python 环境,确保依赖正确安装。同步超时由 `settings.uv_sync_timeout` 控制。 +* **生命周期管理**: 启动和停止插件服务进程(本地插件)或健康检查(远程插件)。 +* **端口管理**: 与端口管理器 (`PortManager`) 协作,为本地插件动态分配和释放运行所需的端口。 +* **日志收集**: 捕获本地插件进程的标准输出 (stdout) 和标准错误 (stderr) 流,并提供查阅接口。 +* **状态查询**: 提供查询插件运行状态、所用端口、已发现插件列表等信息的功能。 +* **Schema 管理**: 从插件配置中获取 JSON Schema 和 UI Schema,支持实例创建的表单配置。 +* **远程插件支持**: 管理远程插件的健康检查和状态监控。 + +## 插件类型 + +OLV Launcher 支持两种插件类型: + +### 本地插件 (Local Plugins) +本地插件在 Launcher 进程内运行,由 Launcher 管理其生命周期。 + +### 远程插件 (Remote Plugins) +远程插件是独立的外部服务,通过 HTTP API 与 Launcher 通信。 + +## 插件配置结构 (plugin.json) + +基于真实代码中的配置结构,`plugin.json` 包含所有插件元数据和配置信息: + +### ASR 插件示例(来自 sherpa_onnx_asr_cpu_plugin) + +```json +{ + "name": "sherpa_onnx_asr_cpu_plugin", + "version": "1.0.0", + "description": "Sherpa-ONNX ASR Plugin supporting multiple model types", + "author": "OLV Team", + "service_type": "asr", + "package_manager": "uv", + "plugin_json_schema": { + "type": "object", + "title": "Sherpa-ONNX ASR Engine Configuration", + "properties": { + "model_type": { + "type": "string", + "title": "Model Type", + "enum": ["transducer", "paraformer", "nemo_ctc", "wenet_ctc", "whisper", "tdnn_ctc", "sense_voice"] + }, + "sense_voice": { + "type": "string", + "title": "SenseVoice Model" + }, + "tokens": { + "type": "string", + "title": "Tokens File" + }, + "provider": { + "type": "string", + "title": "ONNX Provider", + "enum": ["cpu", "cuda"] + }, + "num_threads": { + "type": "integer", + "title": "Number of Threads", + "minimum": 1, + "maximum": 32 + }, + "debug": { + "type": "boolean", + "title": "Debug Mode" + }, + "use_itn": { + "type": "boolean", + "title": "Use ITN" + } + }, + "required": ["model_type", "tokens"], + "default": { + "model_type": "sense_voice", + "sense_voice": "./models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/model.onnx", + "tokens": "./models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17/tokens.txt", + "provider": "cpu", + "num_threads": 4, + "debug": false, + "use_itn": true + } + }, + "plugin_ui_schema": { + "ui:title": "Sherpa-ONNX ASR Engine Configuration", + "ui:description": "Configure the Sherpa-ONNX ASR engine settings for speech recognition", + "model_type": { + "ui:widget": "select", + "ui:title": "Model Type", + "ui:description": "Select the type of ASR model to use.", + "ui:help": "Different model types have different capabilities and performance characteristics." + }, + "sense_voice": { + "ui:widget": "textarea", + "ui:title": "SenseVoice Model Path", + "ui:description": "Path to the SenseVoice model file.", + "ui:placeholder": "e.g., /path/to/sense_voice.onnx" + }, + "tokens": { + "ui:widget": "textarea", + "ui:title": "Tokens File Path", + "ui:description": "Path to the tokens file.", + "ui:placeholder": "e.g., /path/to/tokens.txt" + } + } +} +``` + +### TTS 插件示例(来自 fish_audio_tts_plugin) + +```json +{ + "name": "fish_audio_tts_plugin", + "version": "1.0.0", + "description": "Fish Audio TTS Plugin supporting both HTTP and WebSocket connections", + "author": "OLV Team", + "service_type": "tts", + "package_manager": "uv", + "plugin_json_schema": { + "type": "object", + "title": "Fish Audio TTS Engine Configuration", + "properties": { + "api_key": { + "type": "string", + "title": "API Key", + "description": "Fish Audio API key" + }, + "base_url": { + "type": "string", + "title": "Base URL", + "default": "https://api.fish.audio" + }, + "reference_id": { + "type": "string", + "title": "Reference ID", + "description": "Voice reference ID from Fish Audio" + }, + "model": { + "type": "string", + "title": "TTS Model", + "enum": ["speech-1.5", "speech-1.6"], + "default": "speech-1.6" + }, + "format": { + "type": "string", + "title": "Audio Format", + "enum": ["wav", "pcm", "mp3", "opus"], + "default": "mp3" + }, + "temperature": { + "type": "number", + "title": "Temperature", + "minimum": 0.0, + "maximum": 2.0, + "default": 0.7, + "description": "Controls randomness in speech generation" + }, + "prosody": { + "type": "object", + "title": "Prosody Settings", + "properties": { + "speed": { + "type": "number", + "title": "Speech Speed", + "minimum": 0.5, + "maximum": 2.0, + "default": 1.0 + }, + "volume": { + "type": "number", + "title": "Volume Adjustment (dB)", + "default": 0 + } + } + } + }, + "required": ["api_key"], + "default": { + "api_key": "", + "base_url": "https://api.fish.audio", + "reference_id": "", + "model": "speech-1.6", + "format": "mp3", + "temperature": 0.7, + "prosody": { + "speed": 1.0, + "volume": 0 + } + } + }, + "plugin_ui_schema": { + "ui:title": "Fish Audio TTS Configuration", + "ui:description": "Configure Fish Audio TTS plugin settings", + "api_key": { + "ui:widget": "password", + "ui:placeholder": "Enter your Fish Audio API key" + }, + "reference_id": { + "ui:placeholder": "Enter voice reference ID" + }, + "prosody": { + "ui:title": "Prosody Control", + "speed": { + "ui:widget": "range" + }, + "volume": { + "ui:widget": "updown" + } + } + } +} +``` + +### 远程插件示例 + +远程插件需要在 `plugin.json` 中指定 `service_url`: + +```json +{ + "name": "remote_asr_service", + "version": "1.0.0", + "description": "Remote ASR Service", + "author": "External Provider", + "service_type": "asr", + "service_url": "http://remote-asr.example.com:8080", + "plugin_json_schema": { + "type": "object", + "title": "Remote ASR Configuration", + "properties": { + "model_name": { + "type": "string", + "enum": ["whisper-small", "whisper-large"] + }, + "language": { + "type": "string", + "default": "auto" + } + }, + "required": ["model_name"], + "default": { + "model_name": "whisper-small", + "language": "auto" + } + } +} +``` + +### 关键字段说明 + +| 字段 | 类型 | 必需 | 描述 | +|------|------|------|------| +| `name` | string | 是 | 插件名称,必须与目录名一致 | +| `version` | string | 是 | 插件版本号 | +| `service_type` | string | 是 | 服务类型 (asr, tts, llm) | +| `service_url` | string | 否 | 远程插件的服务地址,本地插件可省略 | +| `package_manager` | string | 否 | 包管理器类型,默认为 "uv" | +| `plugin_json_schema` | object | 否 | JSON Schema 定义,包含配置属性和默认值 | +| `plugin_ui_schema` | object | 否 | UI Schema 定义,控制表单渲染 | + +## 初始化 + +`UnifiedPluginManager` 在应用程序启动时被实例化,配置参数来源于 `LauncherSettings`。管理器通过构造函数接收插件目录路径和端口范围配置。 + +| 参数 | 描述 | 来源 | 默认值 | +|------|------|------|--------| +| `plugins_dir` | 存放插件的根目录路径 | `settings.plugins_dir` | `"plugins"` | +| `port_ranges` | 用于插件端口分配的端口范围列表 | `settings.default_port_ranges` | `["8001-8020", "9001-9020", "10001-10020"]` | + +## 工作流程 + +```mermaid +flowchart TD + A[启动 UnifiedPluginManager] --> B[扫描 plugins_dir] + B --> C[遍历服务类型目录
ASR, TTS, LLM] + C --> D[检查插件目录] + D --> E{包含 plugin.json?} + E -->|否| F[跳过该目录] + E -->|是| G[解析 plugin.json] + G --> H{service_type 匹配目录?} + H -->|否| I[记录警告] + H -->|是| J{是远程插件?} + J -->|是| K[存储远程插件信息] + J -->|否| L{包含 pyproject.toml?} + L -->|否| M[记录警告,跳过] + L -->|是| N{启用 UV 同步?} + N -->|是| O[执行 uv sync] + N -->|否| P[存储本地插件信息] + O --> Q{同步成功?} + Q -->|是| P + Q -->|否| R[记录错误但继续] + R --> P + + F --> S{还有目录?} + I --> S + K --> S + M --> S + P --> S + S -->|是| D + S -->|否| T[插件发现完成] + + U[启动插件请求] --> V{插件存在?} + V -->|否| W[返回错误] + V -->|是| X{是远程插件?} + X -->|是| Y[检查远程服务健康状态] + X -->|否| Z{插件已运行?} + Z -->|是| AA[返回现有端口] + Z -->|否| BB[分配端口] + BB --> CC{端口分配成功?} + CC -->|否| DD[抛出 ServiceError] + CC -->|是| EE[构建启动命令] + EE --> FF[启动进程] + FF --> GG[启动日志捕获线程] + GG --> HH[健康检查] + HH --> II{服务就绪?} + II -->|否| JJ{超时?} + JJ -->|是| KK[清理资源并抛出错误] + JJ -->|否| HH + II -->|是| LL[记录运行状态] + Y --> MM{健康检查通过?} + MM -->|是| LL + MM -->|否| NN[返回错误] +``` + +### 1. 插件发现 + +插件发现过程扫描 `plugins_dir` 下的结构化目录。管理器遍历服务类型目录(如 asr、tts、llm),在每个服务类型目录中查找插件子目录。 + +发现过程包括: +- 验证目录结构(`plugins/service_type/plugin_name/`) +- 解析 `plugin.json` 配置文件 +- 检查 `service_type` 与目录层级匹配 +- 区分本地和远程插件 +- 对本地插件检查 `pyproject.toml` 存在性 + +### 2. 环境同步 + +对本地插件进行环境同步。如果全局配置启用了 UV 同步,管理器会在插件目录下执行 `uv sync` 命令来安装或更新依赖。同步操作有超时限制,失败时会记录错误但不阻止其他操作。 + +### 3. 启动插件服务 + +插件启动流程根据插件类型有所不同: + +#### 本地插件启动 + +本地插件启动过程包括: +1. 端口分配 - 从可用端口池中分配唯一端口 +2. 构建启动命令 - 使用 UV 和 uvicorn 构建标准化启动命令 +3. 进程启动 - 使用 subprocess 启动插件进程 +4. 日志捕获 - 启动独立线程读取进程输出 +5. 健康检查 - 轮询插件的 `/health` 端点等待服务就绪 + +实际启动命令结构: +```bash +uv run --project python -m uvicorn .server:app --host 127.0.0.1 --port --log-level info --access-log --no-use-colors +``` + +#### 远程插件启动 + +远程插件启动过程相对简单: +1. 设置状态为 STARTING +2. 向远程服务的 `/health` 端点发送 HTTP 请求 +3. 检查响应状态,确认服务可用性 +4. 更新插件状态为 RUNNING 或错误状态 + +### 4. 停止插件服务 + +停止插件服务的处理也区分插件类型: + +- **远程插件**: 仅更新内部状态为 STOPPED,不进行实际停止操作 +- **本地插件**: 执行完整的停止流程,包括发送终止信号、等待进程退出、释放端口、清理状态记录 + +### 5. 日志管理 + +本地插件的日志管理功能包括: +- 为每个运行的插件启动独立的日志读取线程 +- 分别处理 stdout 和 stderr 流 +- 日志格式化(添加时间戳和插件名) +- 内存存储最近的1000条日志记录 +- 同时输出到主系统日志 + +## 主要接口方法 + +基于实际代码实现,主要公共方法包括: + +| 方法名 | 描述 | +|--------|------| +| `discover_plugins()` | 发现 `plugins_dir` 中的所有可用插件并加载 Schema | +| `start_plugin_service(plugin_name)` | 异步启动指定插件服务,返回服务 URL | +| `stop_plugin_service(plugin_name)` | 停止指定插件服务 | +| `stop_all_services()` | 停止所有当前正在运行的插件服务 | +| `get_plugins_by_service(service_type)` | 获取指定服务类型的所有插件列表 | +| `get_all_plugins()` | 获取所有已发现插件的列表 | +| `get_plugin_schemas(plugin_name)` | 获取指定插件的配置 Schema | +| `get_plugin_port(plugin_name)` | 获取正在运行的指定插件所使用的端口号 | +| `is_plugin_running(plugin_name)` | 检查指定的插件服务当前是否正在运行 | +| `get_port_status()` | 获取端口分配状态 | +| `configure_ports(port_ranges)` | 重新配置端口范围 | +| `get_plugin_logs(plugin_name, lines)` | 获取指定插件最近的日志 | +| `clear_plugin_logs(plugin_name)` | 清除指定插件存储的日志记录 | +| `create_plugin_instance(plugin_name, config)` | 为指定插件创建新的配置实例 | +| `list_plugin_instances(plugin_name)` | 列出指定插件的所有实例 | +| `delete_plugin_instance(plugin_name, instance_id)` | 删除指定插件的特定实例 | +| `force_cleanup_plugin(plugin_name)` | 强制清理插件状态(处理卡死情况)| + +## 依赖与配置 + +`UnifiedPluginManager` 依赖的 `LauncherSettings` 配置项涵盖插件目录、端口范围、超时设置和环境同步选项。 + +### 环境变量配置 + +| 环境变量 | 默认值 | 描述 | +|----------|--------|------| +| `OLV_LAUNCHER_PLUGINS_DIR` | `"plugins"` | 插件根目录 | +| `OLV_LAUNCHER_DEFAULT_PORT_RANGES` | `["8001-8020", "9001-9020", "10001-10020"]` | 端口范围列表 | +| `OLV_LAUNCHER_PLUGIN_STARTUP_TIMEOUT` | `600` | 插件启动超时时间(秒)| +| `OLV_LAUNCHER_PLUGIN_HEALTH_TIMEOUT` | `10` | 健康检查超时时间(秒)| +| `OLV_LAUNCHER_ENABLE_UV_SYNC` | `True` | 是否启用 UV 环境同步 | +| `OLV_LAUNCHER_UV_SYNC_TIMEOUT` | `300` | UV 同步超时时间(秒)| diff --git a/docs/development/launcher/port-manager.md b/docs/development/launcher/port-manager.md new file mode 100644 index 0000000..db87908 --- /dev/null +++ b/docs/development/launcher/port-manager.md @@ -0,0 +1,93 @@ +# 端口管理器 (PortManager) + +OLV Launcher 中的 `PortManager` 负责为插件服务动态分配和管理网络端口。这确保了每个插件服务都能在唯一的端口上运行,避免冲突。 + +## 核心职责 + +- **端口范围管理**: 解析和管理一个或多个预定义的端口范围。 +- **端口分配**: 为请求服务的插件从可用端口池中分配一个唯一的端口。 +- **端口释放**: 回收已停止服务的插件所占用的端口,使其可供其他插件使用。 +- **状态查询**: 提供查询当前端口分配状态(已分配端口、可用端口、总范围)的功能。 + +## 初始化 + +`PortManager` 在实例化时需要一个端口范围列表。这些范围定义了管理器可以从中分配端口的池。 + +```python +class PortManager: + def __init__(self, port_ranges: List[str]): + """ + Initialize port manager with configurable port ranges. + + Args: + port_ranges: List of port range strings like ["8001-8005", "12000-12010"] + """ + # ... +``` + +端口范围字符串列表由 `LauncherSettings` 中的 `OLV_LAUNCHER_DEFAULT_PORT_RANGES` 配置项提供,其默认值为 `["8001-8020", "9001-9020", "10001-10020"]`。详见 [配置管理](./configuration.md)。 + +## 工作流程 + +```mermaid +flowchart TD + A[初始化 PortManager] --> B[解析端口范围字符串] + B --> C{范围格式有效?} + C -->|否| D[抛出 PortAllocationError] + C -->|是| E[转换为 PortRange 对象] + E --> F[构建可用端口集合] + + G[插件请求端口] --> H{插件已分配端口?} + H -->|是| I[返回已分配端口] + H -->|否| J[查找最小可用端口] + J --> K{找到可用端口?} + K -->|否| L[抛出 PortAllocationError] + K -->|是| M[记录到 allocated_ports] + M --> N[返回分配的端口] + + O[插件停止服务] --> P[释放端口] + P --> Q[从 allocated_ports 移除] + Q --> R[端口重新可用] +``` + +### 1. 解析端口范围 (`_parse_port_ranges`) + +- 初始化时,`PortManager` 会解析传入的字符串格式的端口范围。 +- 每个范围可以是单个端口 (如 `"8080"`) 或一个连字符连接的起止端口 (如 `"8001-8005"`)。 +- 解析后的范围将转换为 `PortRange` 对象(定义见 [数据模型](./models.md#portrange-portpy))并存储。 +- 如果任何范围字符串格式无效(例如,起始端口大于结束端口,或非法的端口号),则会抛出 `PortAllocationError`。 + +### 2. 构建可用端口集 (`_build_available_ports`) + +- 解析完所有端口范围后,`PortManager` 会将这些范围内的所有端口号聚合起来,形成一个初始的可用端口集合 (`available_ports`)。 + +### 3. 分配端口 (`allocate_port`) + +- 当插件需要启动时,会调用 `allocate_port(plugin_name)` 方法。 +- 如果该 `plugin_name` 已经分配过端口,则直接返回已分配的端口。 +- 否则,`PortManager` 会从 `available_ports` 集合中查找一个当前未被任何其他插件占用的最小可用端口。 +- 找到可用端口后,会将其记录在 `allocated_ports` 字典中(键为 `plugin_name`,值为端口号),并从 `available_ports` 逻辑上移除(通过检查 `allocated_ports.values()` 来判断是否可用)。 +- 如果在所有已定义的端口范围内都找不到可用端口,则会抛出 `PortAllocationError`。 + +### 4. 释放端口 (`release_port`) + +- 当插件停止服务时,会调用 `release_port(plugin_name)` 方法。 +- `PortManager` 会从 `allocated_ports` 字典中移除该插件的条目。 +- 该端口随后即可被其他插件分配和使用。 + +## 主要接口方法 + +下表总结了 `PortManager` 提供的主要公共方法: + +| 方法名 | 描述 | 返回类型 | +|-----------------------|----------------------------------------------------------------------|---------------------------| +| `allocate_port(plugin_name)` | 为指定插件分配一个端口。如果已分配,则返回现有端口。如果无可用端口,则抛出 `PortAllocationError`。 | `int` | +| `release_port(plugin_name)` | 释放指定插件占用的端口。 | `None` | +| `get_port(plugin_name)` | 获取指定插件当前分配的端口号。如果未分配,则返回 `None`。 | `Optional[int]` | +| `get_status()` | 获取当前端口分配的详细状态,包括已分配端口、可用端口列表和总端口范围。 | `Dict` | + +## 依赖与数据模型 + +- `PortManager` 依赖于 `PortRange` 数据模型来表示和操作端口范围。详情请参阅 [数据模型文档](./models.md#portrange-portpy)。 +- 端口分配失败时,会抛出 `PortAllocationError` 异常。该异常定义在 `olv_launcher.exceptions` 模块中。 +- 端口范围的配置通过 `LauncherSettings` 进行管理,具体配置项为 `OLV_LAUNCHER_DEFAULT_PORT_RANGES`,详情请参阅 [配置管理文档](./configuration.md)。 diff --git a/docs/development/launcher/readme.md b/docs/development/launcher/readme.md new file mode 100644 index 0000000..e953e4f --- /dev/null +++ b/docs/development/launcher/readme.md @@ -0,0 +1,77 @@ +# OLV Launcher 概述 + +OLV Launcher 是 OLV(Open Loop Voice)平台的中央插件和服务管理系统。它负责发现、管理和协调各种服务类型(ASR、TTS、LLM)的插件。Launcher 提供统一的 REST API 接口,用于管理插件的生命周期、健康状态、端口分配,以及提供 JSON Schema 和 UI Schema 的统一访问。 + +## 架构概述 + +OLV Launcher 采用模块化设计,基于 FastAPI 框架实现 RESTful API 服务。其核心组件包括插件管理器、端口管理器、路由模块和 schema 管理系统。每个插件可以提供 JSON Schema(用于数据验证)和 UI Schema(用于前端表单生成)。 + +```mermaid +graph TD + Client[客户端] -->|HTTP 请求| FastAPI[FastAPI 应用] + FastAPI -->|依赖注入| PM[插件管理器] + PM -->|管理| Plugins[插件] + PM -->|使用| PortM[端口管理器] + PortM -->|分配| Ports[端口资源] + PM -->|加载| Schemas[Schema 文件] + + subgraph Routers[路由模块] + R1[插件路由] + R2[服务路由] + R3[端口路由] + R4[Schema 路由] + end + + FastAPI -->|路由| Routers + + subgraph Services[插件服务类型] + S1[ASR 服务] + S2[TTS 服务] + S3[LLM 服务] + end + + subgraph SchemaTypes[Schema 类型] + JS[JSON Schema] + US[UI Schema] + ET[Engine 配置] + CT[Character 配置] + end + + Plugins -->|类型| Services + Schemas -->|类型| SchemaTypes +``` + +## 目录结构 + +``` +src/olv_launcher/ +├── __init__.py # 模块初始化文件 +├── server.py # FastAPI 应用程序入口点 +├── plugin_manager.py # 插件管理器实现 +├── port_mananger.py # 端口管理器实现 +├── dependencies.py # 依赖注入函数 +├── exceptions.py # 自定义异常类 +├── middleware.py # 中间件实现 +├── models/ # Pydantic 模型包 +│ ├── __init__.py # 模型包初始化 +│ ├── api.py # API 模型定义 +│ ├── config.py # 配置模型定义 +│ ├── service.py # 服务模型定义 +│ ├── port.py # 端口相关模型 +│ └── plugin.py # 插件相关模型 +└── routers/ # 路由模块 + ├── __init__.py # 路由包初始化 + ├── plugins.py # 插件管理路由 + ├── services.py # 服务管理路由 + └── ports.py # 端口管理路由 +``` + +## 文档导航 + +- [部署指南 (Deployment)](./deployment.md) - 开发环境部署 +- [插件开发指南 (Plugin Development)](../plugins/readme.md) - 如何开发插件 +- [配置管理 (Configuration)](./configuration.md) - 环境变量和配置选项 +- [插件管理器 (Plugin Manager)](./plugin-manager.md) - 插件生命周期管理 +- [端口管理器 (Port Manager)](./port-manager.md) - 端口分配和管理 +- [数据模型 (Models)](./models.md) - Pydantic 数据模型 +- [API 参考 (API Reference)](./api-reference.md) - 完整的 `openapi.json` diff --git a/docs/development/olv-main/README.md b/docs/development/olv-main/README.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/development/plugin/asr_api.md b/docs/development/plugin/asr_api.md new file mode 100644 index 0000000..44521e7 --- /dev/null +++ b/docs/development/plugin/asr_api.md @@ -0,0 +1,285 @@ +# ASR Plugin API 结构 + +## 实例路由策略 + +### 最小超集匹配(Minimal Superset Matching) + +ASR Plugin 使用**最小超集匹配策略**来选择最合适的引擎实例: + +**匹配规则:** +1. **空配置处理**:如果 `plugin_config` 为空或未提供,自动选择第一个可用实例 +2. **必要条件**:实例配置必须包含 `plugin_config` 中的所有字段,且值完全匹配 +3. **允许额外字段**:实例配置可以包含 `plugin_config` 中没有的额外字段 +4. **最优选择**:在所有匹配的实例中,选择配置字段数量最少的实例 +5. **同等情况下**:如果有多个实例具有相同的最少字段数,选择第一个 + +## ASR Plugin 端点 + +ASR Plugin 提供以下端点: + +### 1. POST `/transcribe` +转录音频数据,使用已存在的插件配置实例。 + +**请求体:** +```json +{ + "audio": "base64_encoded_audio_data", // Base64 编码的音频数据 + "plugin_config": { + // 可选的插件特定配置参数,用于标识和创建实例 + // 如果不提供,将自动使用第一个可用实例 + // 相同的 plugin_config 会复用同一个实例 + // 通常包含模型路径、引擎类型等需要长时间加载的配置 + }, + "custom": { + // 可选的运行时参数,不参与实例路由 + // 用于覆盖默认设置,如语言、采样率等 + // 这些参数不会影响实例的创建和复用 + } +} +``` + +**响应:** +```json +{ + "text": "转录的文本结果" +} +``` + +**错误处理:** +- 如果 `plugin_config` 对应的实例不存在,返回 404 错误 +- 如果没有可用实例,返回 404 错误 +- 不会自动创建新实例 + +### 2. GET `/health` +检查 ASR 服务健康状态。 + +**响应:** +```json +{ + "status": "running" // running, starting, error, stopped +} +``` + +**状态说明:** +- `running`: 所有实例正常运行 +- `starting`: 部分实例正在初始化 +- `error`: 所有实例出错 +- `stopped`: 服务已停止 + +### 3. POST `/create_instance` (内部端点) +创建新的引擎实例(由 launcher 调用)。 + +**请求体:** +```json +{ + "config": { + // 插件特定的完整配置参数 + // 用于初始化模型和引擎实例 + } +} +``` + +**响应:** +```json +{ + "message": "Instance created successfully", + "instance_id": "abc123def456", + "config": { /* 返回的配置信息 */ } +} +``` + +### 4. DELETE `/delete_instance/{instance_id}` (内部端点) +删除引擎实例(由 launcher 调用)。 + +**响应:** +```json +{ + "message": "Instance 'abc123def456' deleted successfully", + "instance_id": "abc123def456" +} +``` + +### 5. GET `/list_instances` (内部端点) +列出所有引擎实例(由 launcher 调用)。 + +**响应:** +```json +{ + "total_instances": 2, + "instances": { + "abc123def456": { + "instance_id": "abc123def456", + "ready": true + }, + "def789ghi012": { + "instance_id": "def789ghi012", + "ready": false + } + } +} +``` + +### 6. GET `/plugin-config` +获取插件配置信息(用于远程插件发现)。 + +**响应:** +```json +{ + "name": "plugin_name", + "version": "1.0.0", + "description": "插件描述", + "service_type": "asr", + "plugin_json_schema": { /* JSON Schema 定义 */ }, + "plugin_ui_schema": { /* UI Schema 定义 */ } +} +``` + +## 参数说明 + +### plugin_config vs custom + +#### plugin_config +- **用途**: 用于创建和标识引擎实例 +- **特点**: 参与实例路由,相同配置会复用实例 +- **内容**: 包含需要长时间加载的配置,如模型路径、引擎类型、硬件设置等 +- **生命周期**: 在实例创建时使用,实例存在期间保持不变 + +#### custom +- **用途**: 运行时参数覆盖 +- **特点**: 不参与实例路由,不影响实例的创建和复用 +- **内容**: 包含可以快速变更的参数,如语言设置、采样率、解码参数等 +- **生命周期**: 每次请求时使用,可以随请求变化 + +## 音频格式要求 + +- **编码格式**: Base64 编码的音频数据 +- **采样率**: 通常为 16kHz,具体要求取决于插件 +- **位深度**: 通常为 16-bit PCM +- **声道数**: 通常为单声道 +- **数据格式**: 小端序(Little Endian) + +## 错误码 + +- **400**: 请求参数错误 + - 缺少必需字段 `audio` 或 `plugin_config` + - 音频数据格式无效 +- **404**: 实例不存在 + - 指定的 `plugin_config` 对应的实例未找到 +- **500**: 服务器内部错误 + - 转录过程中发生错误 + - 引擎初始化失败 +- **503**: 服务不可用 + - 引擎实例未就绪 + +## 使用流程 + +### 1. 启动插件 +通过 Launcher API 启动 ASR 插件: +```bash +curl -X POST http://127.0.0.1:7000/plugins/{plugin_name}/start +``` + +### 2. 创建实例 +通过 Launcher API 创建插件实例: +```bash +curl -X POST http://127.0.0.1:7000/plugins/{plugin_name}/instances \ + -H "Content-Type: application/json" \ + -d '{ + "config": { + // 插件特定的配置参数 + } + }' +``` + +### 3. 转录音频 +使用插件实例进行音频转录: + +**使用指定配置:** +```bash +curl -X POST http://127.0.0.1:8080/transcribe \ + -H "Content-Type: application/json" \ + -d '{ + "audio": "base64_encoded_audio_data", + "plugin_config": { + // 与创建实例时相同的配置 + }, + "custom": { + // 可选的运行时参数 + } + }' +``` + +**使用第一个可用实例(简化调用):** +```bash +curl -X POST http://127.0.0.1:8080/transcribe \ + -H "Content-Type: application/json" \ + -d '{ + "audio": "base64_encoded_audio_data", + "custom": { + // 可选的运行时参数 + } + }' +``` + +## 实例管理 + +ASR 插件使用**最小超集匹配策略**来管理实例: + +1. **空配置支持**: 当 `plugin_config` 为空或未提供时,自动选择第一个可用实例,简化调用 +2. **智能复用**: 使用最小超集匹配算法选择最合适的现有实例 +3. **配置灵活性**: 简化的 `plugin_config` 可以匹配更完整的实例配置 +4. **自动清理**: 实例在删除时会自动清理资源 +5. **状态检查**: 实例具有就绪状态检查机制 +6. **最优选择**: 优先选择配置最精简的匹配实例,提高资源利用率 + +## 错误处理示例 + +### 1. 实例不存在错误 +```json +{ + "detail": "No matching instance found for the provided plugin_config" +} +``` + +### 2. 没有可用实例错误 +```json +{ + "detail": "No engine instances available" +} +``` + +### 3. 音频字段缺失错误 +```json +{ + "detail": "audio field is required" +} +``` + +### 4. 实例未就绪错误 +```json +{ + "detail": "ASR engine instance not ready" +} +``` + +### 5. 转录失败错误 +```json +{ + "detail": "Transcription failed: [具体错误信息]" +} +``` + +## 性能优化建议 + +1. **实例复用**: 对于相同配置,复用已存在的实例可避免重复初始化开销 +2. **参数分离**: 将需要长时间加载的配置放在 `plugin_config` 中,将可变参数放在 `custom` 中 +3. **批处理**: 对于大量音频文件,考虑使用相同实例进行批量处理 +4. **音频预处理**: 确保音频格式符合要求,避免格式转换开销 + +## 开发注意事项 + +1. **音频编码**: 所有音频数据都必须使用 Base64 编码传输 +2. **实例生命周期**: 实例由 Launcher 管理,插件本身不直接创建或删除实例 +3. **异步处理**: 转录过程是异步的,适合处理大文件 +4. **资源管理**: 实例会自动管理模型资源的加载和释放 +5. **配置设计**: 合理划分 `plugin_config` 和 `custom` 参数,优化实例复用率 \ No newline at end of file diff --git a/docs/development/plugin/tts_api.md b/docs/development/plugin/tts_api.md new file mode 100644 index 0000000..fbccc6d --- /dev/null +++ b/docs/development/plugin/tts_api.md @@ -0,0 +1,360 @@ +# TTS Plugin API 结构 + +## 实例路由策略 + +### 最小超集匹配(Minimal Superset Matching) + +TTS Plugin 使用**最小超集匹配策略**来选择最合适的引擎实例: + +**匹配规则:** +1. **空配置处理**:如果 `plugin_config` 为空或未提供,自动选择第一个可用实例 +2. **必要条件**:实例配置必须包含 `plugin_config` 中的所有字段,且值完全匹配 +3. **允许额外字段**:实例配置可以包含 `plugin_config` 中没有的额外字段 +4. **最优选择**:在所有匹配的实例中,选择配置字段数量最少的实例 +5. **同等情况下**:如果有多个实例具有相同的最少字段数,选择第一个 + +## TTS Plugin 端点 + +TTS Plugin 提供以下端点: + +### 1. POST `/synthesize` +合成语音数据,使用已存在的插件配置实例。 + +**请求体:** +```json +{ + "text": "要合成的文本内容", // 待合成的文本 + "plugin_config": { + // 可选的插件特定配置参数,用于标识和创建实例 + // 如果不提供,将自动使用第一个可用实例 + // 相同的 plugin_config 会复用同一个实例 + // 通常包含 API 密钥、模型选择、语音ID等需要长时间加载的配置 + }, + "custom": { + // 可选的运行时参数,不参与实例路由 + // 用于覆盖默认设置,如语速、音量、延迟模式等 + // 这些参数不会影响实例的创建和复用 + } +} +``` + +**响应:** +```json +{ + "audio": "base64_encoded_audio_data" +} +``` + +**错误处理:** +- 如果 `plugin_config` 对应的实例不存在,返回 404 错误 +- 如果没有可用实例,返回 404 错误 +- 不会自动创建新实例 + +### 2. WebSocket `/synthesize_stream` +流式语音合成,支持实时文本输入和音频输出。 + +**连接流程:** + +1. **建立连接** + ``` + ws://localhost:8000/synthesize_stream + ``` + +2. **发送初始配置** + ```json + { + "plugin_config": { + // 可选的插件特定配置参数 + // 如果不提供,将自动使用第一个可用实例 + }, + "custom": { + // 可选的运行时参数 + } + } + ``` + +3. **接收就绪信号** + ```json + { + "status": "ready" + } + ``` + +4. **发送文本块** + ```json + { + "type": "text", + "text": "文本片段 " + } + ``` + +5. **接收音频块** + ```json + { + "type": "audio", + "audio": "base64_encoded_audio_chunk" + } + ``` + +6. **发送结束信号** + ```json + { + "type": "end" + } + ``` + +7. **接收完成信号** + ```json + { + "type": "complete" + } + ``` + +**错误处理:** +- 如果配置无效,连接会被关闭并返回错误信息 +- 如果引擎不支持流式合成,会返回相应错误 + +### 3. GET `/health` +检查 TTS 服务健康状态。 + +**响应:** +```json +{ + "status": "running" // running, starting, error, stopped +} +``` + +**状态说明:** +- `running`: 所有实例正常运行 +- `starting`: 部分实例正在初始化 +- `error`: 所有实例出错 +- `stopped`: 服务已停止 + +### 4. POST `/create_instance` (内部端点) +创建新的引擎实例(由 launcher 调用)。 + +**请求体:** +```json +{ + "config": { + // 插件特定的完整配置参数 + // 用于初始化引擎实例 + } +} +``` + +**响应:** +```json +{ + "message": "Instance created successfully", + "instance_id": "abc123def456", + "config": { /* 返回的配置信息 */ } +} +``` + +### 5. DELETE `/instances/{instance_id}` (内部端点) +删除引擎实例(由 launcher 调用)。 + +**响应:** +```json +{ + "message": "Instance abc123def456 deleted successfully" +} +``` + +### 6. GET `/instances` (内部端点) +列出所有引擎实例(由 launcher 调用)。 + +**响应:** +```json +{ + "instances": { + "abc123def456": { + "ready": true, + "supports_streaming": true, // 是否支持流式合成 + "config": { /* 实例配置 */ } + } + } +} +``` + +### 7. GET `/plugin-config` +获取插件配置信息(用于远程插件发现)。 + +**响应:** +```json +{ + "name": "plugin_name", + "version": "1.0.0", + "description": "插件描述", + "service_type": "tts", + "plugin_json_schema": { /* JSON Schema 定义 */ }, + "plugin_ui_schema": { /* UI Schema 定义 */ } +} +``` + +## 参数说明 + +### plugin_config vs custom + +#### plugin_config +- **用途**: 用于创建和标识引擎实例 +- **特点**: 参与实例路由,相同配置会复用实例 +- **内容**: 包含需要长时间加载的配置,如 API 密钥、模型选择、语音参考等 +- **生命周期**: 在实例创建时使用,实例存在期间保持不变 + +#### custom +- **用途**: 运行时参数覆盖 +- **特点**: 不参与实例路由,不影响实例的创建和复用 +- **内容**: 包含可以快速变更的参数,如语速、音量、延迟模式、温度等 +- **生命周期**: 每次请求时使用,可以随请求变化 + +## 支持的音频格式 + +音频格式支持取决于具体插件实现,常见格式包括: +- **MP3**: 压缩格式,适合网络传输 +- **WAV**: 无损格式,质量最高 +- **PCM**: 原始音频数据 +- **Opus**: 低延迟格式,适合流式应用 + +## 延迟模式 + +延迟模式取决于具体插件实现,常见模式包括: +- **normal**: 标准延迟,更高的稳定性 +- **balanced**: 平衡模式,在延迟和稳定性之间取平衡 +- **fast**: 快速模式,最低延迟但可能降低稳定性 + +## 错误码 + +- **400**: 请求参数错误 + - 缺少必需字段 `text` 或 `plugin_config` + - 文本内容格式无效 +- **404**: 实例不存在 + - 指定的 `plugin_config` 对应的实例未找到 +- **500**: 服务器内部错误 + - 合成过程中发生错误 + - 引擎初始化失败 +- **503**: 服务不可用 + - 引擎实例未就绪 + +## 使用流程 + +### 1. 启动插件 +通过 Launcher API 启动 TTS 插件: +```bash +curl -X POST http://127.0.0.1:7000/plugins/{plugin_name}/start +``` + +### 2. 创建实例 +通过 Launcher API 创建插件实例: +```bash +curl -X POST http://127.0.0.1:7000/plugins/{plugin_name}/instances \ + -H "Content-Type: application/json" \ + -d '{ + "config": { + // 插件特定的配置参数 + } + }' +``` + +### 3. 合成语音 +使用插件实例进行语音合成: + +**使用指定配置:** +```bash +curl -X POST http://127.0.0.1:8080/synthesize \ + -H "Content-Type: application/json" \ + -d '{ + "text": "要合成的文本", + "plugin_config": { + // 与创建实例时相同的配置 + }, + "custom": { + // 可选的运行时参数 + } + }' +``` + +**使用第一个可用实例(简化调用):** +```bash +curl -X POST http://127.0.0.1:8080/synthesize \ + -H "Content-Type: application/json" \ + -d '{ + "text": "要合成的文本", + "custom": { + // 可选的运行时参数 + } + }' +``` + +## 实例管理 + +TTS 插件使用**最小超集匹配策略**来管理实例: + +1. **空配置支持**: 当 `plugin_config` 为空或未提供时,自动选择第一个可用实例,简化调用 +2. **智能复用**: 使用最小超集匹配算法选择最合适的现有实例 +3. **配置灵活性**: 简化的 `plugin_config` 可以匹配更完整的实例配置 +4. **自动清理**: 实例在删除时会自动清理资源 +5. **状态检查**: 实例具有就绪状态检查机制 +6. **流式支持**: 部分实例支持流式合成功能 +7. **最优选择**: 优先选择配置最精简的匹配实例,提高资源利用率 + +## 错误处理示例 + +### 1. 实例不存在错误 +```json +{ + "detail": "No matching instance found for the provided plugin_config" +} +``` + +### 2. 没有可用实例错误 +```json +{ + "detail": "No engine instances available" +} +``` + +### 3. 文本字段缺失错误 +```json +{ + "detail": "text field is required" +} +``` + +### 4. 实例未就绪错误 +```json +{ + "detail": "TTS engine instance not ready" +} +``` + +### 5. 合成失败错误 +```json +{ + "detail": "Synthesis failed: [具体错误信息]" +} +``` + +### 6. 流式合成不支持错误 +```json +{ + "error": "TTS engine does not support streaming" +} +``` + +## 性能优化建议 + +1. **实例复用**: 对于相同配置,复用已存在的实例可避免重复初始化开销 +2. **参数分离**: 将需要长时间加载的配置放在 `plugin_config` 中,将可变参数放在 `custom` 中 +3. **流式合成**: 对于长文本,使用 WebSocket 流式合成可以获得更好的用户体验 +4. **格式选择**: 根据使用场景选择合适的音频格式,平衡质量和传输效率 + +## 开发注意事项 + +1. **音频编码**: 所有音频数据都使用 Base64 编码传输 +2. **实例生命周期**: 实例由 Launcher 管理,插件本身不直接创建或删除实例 +3. **异步处理**: 合成过程是异步的,适合处理长文本 +4. **WebSocket 管理**: 流式连接需要正确处理连接状态和异常 +5. **配置设计**: 合理划分 `plugin_config` 和 `custom` 参数,优化实例复用率 +6. **流式支持**: 不是所有 TTS 引擎都支持流式合成,需要在实现中明确标识 diff --git a/docs/marketplace/README.md b/docs/marketplace/README.md new file mode 100644 index 0000000..e69de29 diff --git a/get_tree.py b/get_tree.py new file mode 100644 index 0000000..ddf7738 --- /dev/null +++ b/get_tree.py @@ -0,0 +1,34 @@ +import os + +IGNORE_PATTERNS = [ + '__pycache__', '.git', '.venv', '.ruff_cache', '.cursor', 'node_modules', "egg-info" +] +MAX_LEVEL = 3 + +def is_ignored(name): + for pat in IGNORE_PATTERNS: + if pat in name or name.endswith('.pyc'): + return True + return False + +def tree(dir_path, level=1, prefix=''): + if level > MAX_LEVEL: + return + items = [] + try: + items = sorted(os.listdir(dir_path), key=lambda x: (not os.path.isdir(os.path.join(dir_path, x)), x.lower())) + except PermissionError: + return + for i, name in enumerate(items): + full_path = os.path.join(dir_path, name) + if is_ignored(name): + continue + connector = '└── ' if i == len(items) - 1 else '├── ' + print(prefix + connector + name) + if os.path.isdir(full_path): + extension = ' ' if i == len(items) - 1 else '│ ' + tree(full_path, level + 1, prefix + extension) + +if __name__ == '__main__': + print('.') + tree('.', 1, '') \ No newline at end of file diff --git a/olv-launcher-ui/.gitignore b/olv-launcher-ui/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/olv-launcher-ui/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/olv-launcher-ui/README.md b/olv-launcher-ui/README.md new file mode 100644 index 0000000..e69de29 diff --git a/olv-launcher-ui/app/favicon.ico b/olv-launcher-ui/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/olv-launcher-ui/app/favicon.ico differ diff --git a/olv-launcher-ui/app/globals.css b/olv-launcher-ui/app/globals.css new file mode 100644 index 0000000..af9a978 --- /dev/null +++ b/olv-launcher-ui/app/globals.css @@ -0,0 +1,192 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +/* Custom toast styles */ +[data-sonner-toaster] [data-type="success"] { + background-color: rgb(22 163 74) !important; + border-color: rgb(21 128 61) !important; + color: white !important; +} + +[data-sonner-toaster] [data-type="error"] { + background-color: rgb(220 38 38) !important; + border-color: rgb(185 28 28) !important; + color: white !important; +} + +[data-sonner-toaster] [data-type="warning"] { + background-color: rgb(217 119 6) !important; + border-color: rgb(180 83 9) !important; + color: white !important; +} + +[data-sonner-toaster] [data-type="info"] { + background-color: rgb(37 99 235) !important; + border-color: rgb(29 78 216) !important; + color: white !important; +} + +/* Dark mode toast styles */ +.dark [data-sonner-toaster] [data-type="success"] { + background-color: rgb(34 197 94) !important; + border-color: rgb(22 163 74) !important; +} + +.dark [data-sonner-toaster] [data-type="error"] { + background-color: rgb(239 68 68) !important; + border-color: rgb(220 38 38) !important; +} + +.dark [data-sonner-toaster] [data-type="warning"] { + background-color: rgb(245 158 11) !important; + border-color: rgb(217 119 6) !important; +} + +.dark [data-sonner-toaster] [data-type="info"] { + background-color: rgb(59 130 246) !important; + border-color: rgb(37 99 235) !important; +} + +/* Custom scrollbar styles for terminal logs */ +.terminal-scroll { + scrollbar-width: thin; + scrollbar-color: #475569 #1e293b; +} + +.terminal-scroll::-webkit-scrollbar { + width: 8px; +} + +.terminal-scroll::-webkit-scrollbar-track { + background: #1e293b; + border-radius: 4px; +} + +.terminal-scroll::-webkit-scrollbar-thumb { + background: #475569; + border-radius: 4px; +} + +.terminal-scroll::-webkit-scrollbar-thumb:hover { + background: #64748b; +} diff --git a/olv-launcher-ui/app/layout.tsx b/olv-launcher-ui/app/layout.tsx new file mode 100644 index 0000000..c0fccf5 --- /dev/null +++ b/olv-launcher-ui/app/layout.tsx @@ -0,0 +1,24 @@ +import type { Metadata } from "next"; +import { GeistSans } from "geist/font/sans"; +import "./globals.css"; +import { Toaster } from "@/components/ui/sonner"; + +export const metadata: Metadata = { + title: "OLV Launcher", + description: "Central plugin and service management for OLV platform", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + + ); +} diff --git a/olv-launcher-ui/app/marketplace/page.tsx b/olv-launcher-ui/app/marketplace/page.tsx new file mode 100644 index 0000000..255dc70 --- /dev/null +++ b/olv-launcher-ui/app/marketplace/page.tsx @@ -0,0 +1,286 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +'use client'; + +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { + RefreshCw, + Search, + Download, + Package, + AlertCircle, + Store, + Filter, + CheckCircle, + ArrowUpCircle +} from 'lucide-react'; +import { mutate } from 'swr'; + +import { useMarketplacePlugins, useMarketplaceActions, useMarketplaceSearch, usePluginStatus } from '@/hooks/use-marketplace'; +import { MarketplacePluginCard } from '@/components/marketplace-plugin-card'; +import { Navigation } from '@/components/navigation'; +import { MarketplacePluginInfo } from '@/lib/api'; + +export default function MarketplacePage() { + const { searchParams, updateSearch, resetSearch } = useMarketplaceSearch(); + const { data: marketplaceData, error: marketplaceError, isLoading: marketplaceLoading, mutate: refreshData } = useMarketplacePlugins(searchParams); + const { refreshMarketplace, checkUpdates, loading } = useMarketplaceActions(); + + const [activeTab, setActiveTab] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + + const plugins = marketplaceData?.plugins || []; + const { getStatusCounts, getPluginsByStatus } = usePluginStatus(plugins); + const statusCounts = getStatusCounts(); + + // Handle search input + useEffect(() => { + const timeoutId = setTimeout(() => { + updateSearch({ query: searchQuery }); + }, 500); + + return () => clearTimeout(timeoutId); + }, [searchQuery, updateSearch]); + + const handleRefreshMarketplace = async () => { + try { + await refreshMarketplace(true); + await refreshData(); + // Also refresh the main plugins list + mutate('plugins'); + } catch (error) { + // Error is handled in the hook + } + }; + + const handleCheckAllUpdates = async () => { + try { + await checkUpdates(); + await refreshData(); + } catch (error) { + // Error is handled in the hook + } + }; + + const handleServiceTypeFilter = (serviceType: string) => { + updateSearch({ service_type: serviceType === 'all' ? '' : serviceType }); + }; + + + + const getFilteredPlugins = (status?: string) => { + if (!status || status === 'all') return plugins; + return getPluginsByStatus(status); + }; + + const renderPluginGrid = (pluginList: MarketplacePluginInfo[]) => { + if (pluginList.length === 0) { + return ( + + + +

No Plugins Found

+

+ {searchParams.query ? + 'No plugins match your search criteria. Try adjusting your filters.' : + 'No plugins are available in this category.' + } +

+
+
+ ); + } + + return ( +
+ {pluginList.map((plugin) => ( + + ))} +
+ ); + }; + + return ( +
+ {/* Navigation */} + + +
+ {/* Header */} +
+
+

+ + Plugin Marketplace +

+

+ Discover and install plugins for your OLV platform +

+
+ +
+ + + +
+
+ + {/* Error Alert */} + {marketplaceError && ( + + + + Failed to load marketplace: {marketplaceError.message} + + + + )} + + {/* Search and Filters */} + + + + + Search & Filter + + + +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+
+ + + + +
+
+
+ + {/* Status Tabs */} + + + + + All ({plugins.length}) + + + + Available ({statusCounts.available}) + + + + Installed ({statusCounts.installed}) + + + + Updates ({statusCounts.updateAvailable}) + + + +
+ + {marketplaceLoading ? ( +
+ + Loading marketplace... +
+ ) : ( + renderPluginGrid(getFilteredPlugins('all')) + )} +
+ + + {renderPluginGrid(getFilteredPlugins('available'))} + + + + {renderPluginGrid(getFilteredPlugins('installed'))} + + + + {renderPluginGrid(getFilteredPlugins('update_available'))} + +
+
+ + {/* Stats */} + {marketplaceData && ( + + +
+ + Showing {plugins.length} of {marketplaceData.total_count} plugins + + {marketplaceData.has_more && ( + More results available - refine your search to see all + )} +
+
+
+ )} +
+
+ ); +} diff --git a/olv-launcher-ui/app/page.tsx b/olv-launcher-ui/app/page.tsx new file mode 100644 index 0000000..4da743f --- /dev/null +++ b/olv-launcher-ui/app/page.tsx @@ -0,0 +1,342 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +'use client'; + +import { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Button } from '@/components/ui/button'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { PluginCard } from '@/components/plugin-card'; +import { WorkspaceManager } from '@/components/workspace-manager'; +import { Navigation } from '@/components/navigation'; +import { usePlugins, usePluginsByService } from '@/hooks/use-plugins'; +import { mutate } from 'swr'; +import { toast } from 'sonner'; +import { + Server, + RefreshCw, + AlertCircle, + CheckCircle, + Clock, + Monitor, + Cloud, + Store, +} from 'lucide-react'; +import { PluginStatus } from '@/lib/api'; + +export default function HomePage() { + const { data: pluginsData, error: pluginsError, isLoading: pluginsLoading } = usePlugins(); + const [activeServiceTab, setActiveServiceTab] = useState('all'); + const [isRefreshing, setIsRefreshing] = useState(false); + + const handleRefresh = async (showToast = false) => { + setIsRefreshing(true); + try { + await Promise.all([ + mutate('health'), + mutate('plugins'), + activeServiceTab !== 'all' ? mutate(`plugins/${activeServiceTab}`) : Promise.resolve() + ]); + if (showToast) { + toast.success('Data refreshed successfully'); + } + } catch (error) { + if (showToast) { + toast.error('Failed to refresh data'); + } + } finally { + setIsRefreshing(false); + } + }; + + const serviceTypes = ['asr', 'tts', 'llm']; + const plugins = pluginsData?.plugins || []; + + const getPluginsByService = (serviceType: string) => { + if (serviceType === 'all') return plugins; + return plugins.filter(plugin => plugin.service_type.toLowerCase() === serviceType.toLowerCase()); + }; + + const getServiceStats = () => { + const stats = { + total: plugins.length, + running: plugins.filter(p => p.status === PluginStatus.RUNNING).length, + stopped: plugins.filter(p => p.status === PluginStatus.STOPPED).length, + error: plugins.filter(p => [PluginStatus.ERROR].includes(p.status)).length, + local: plugins.filter(p => p.is_local).length, + remote: plugins.filter(p => !p.is_local).length, + }; + return stats; + }; + + const stats = getServiceStats(); + + return ( +
+ {/* Navigation */} + + + {/* Header */} +
+
+
+
+

OLV Launcher

+

+ Central plugin and service management for OLV platform +

+
+
+ + + +
+
+
+
+ +
+ {/* System Status */} +
+

System Status

+ +
+ {/* Total Plugins */} + + + Total Plugins + + + +
{stats.total}
+

+ Discovered plugins +

+
+
+ + {/* Running Services */} + + + Running Services + + + +
{stats.running}
+

+ Active services +

+
+
+ + {/* Local Plugins */} + + + Local Plugins + + + +
{stats.local}
+

+ Locally managed +

+
+
+ + {/* Remote Plugins */} + + + Remote Plugins + + + +
{stats.remote}
+

+ Remotely hosted +

+
+
+ + +
+
+ + {/* Workspace Management */} +
+ handleRefresh(false)} /> +
+ + {/* Plugin Management */} +
+
+

Plugin Management

+
+ + Auto-refresh enabled +
+
+ + {pluginsError && ( + + + + Failed to load plugin information: {pluginsError.message} + + + + )} + + + + + + All ({stats.total}) + + {serviceTypes.map(serviceType => { + const servicePlugins = getPluginsByService(serviceType); + return ( + + {serviceType.toUpperCase()} ({servicePlugins.length}) + + ); + })} + + + + {pluginsLoading ? ( +
+ + Loading plugins... +
+ ) : ( +
+ {plugins.map((plugin) => ( + handleRefresh(true)} + /> + ))} +
+ )} +
+ + {serviceTypes.map(serviceType => ( + + handleRefresh(true)} + /> + + ))} +
+ + {plugins.length === 0 && !pluginsLoading && ( + + + +

No Plugins Found

+

+ No plugins were discovered in the plugins directory. + Make sure your plugins are properly installed and configured. +

+ +
+
+ )} +
+
+
+ ); +} + +// Service Tab Content Component +function ServiceTabContent({ serviceType, onRefresh }: { serviceType: string; onRefresh: () => void }) { + const { data: serviceData, error, isLoading } = usePluginsByService(serviceType); + + if (isLoading) { + return ( +
+ + Loading {serviceType.toUpperCase()} plugins... +
+ ); + } + + if (error) { + return ( + + + + Failed to load {serviceType.toUpperCase()} plugins: {error.message} + + + ); + } + + const plugins = serviceData?.plugins || []; + + if (plugins.length === 0) { + return ( + + + +

No {serviceType.toUpperCase()} Plugins

+

+ No {serviceType.toUpperCase()} plugins were found. + Install {serviceType.toUpperCase()} plugins to manage them here. +

+
+
+ ); + } + + return ( +
+ {plugins.map((plugin) => ( + + ))} +
+ ); +} diff --git a/olv-launcher-ui/components.json b/olv-launcher-ui/components.json new file mode 100644 index 0000000..335484f --- /dev/null +++ b/olv-launcher-ui/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/olv-launcher-ui/components/marketplace-plugin-card.tsx b/olv-launcher-ui/components/marketplace-plugin-card.tsx new file mode 100644 index 0000000..fa134f9 --- /dev/null +++ b/olv-launcher-ui/components/marketplace-plugin-card.tsx @@ -0,0 +1,334 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useState } from 'react'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Progress } from '@/components/ui/progress'; +import { + Download, + Trash2, + ArrowUpCircle, + ExternalLink, + Calendar, + User, + Package, + AlertCircle, + Loader2 +} from 'lucide-react'; +import { mutate } from 'swr'; + +import { MarketplacePluginInfo } from '@/lib/api'; +import { useMarketplaceActions } from '@/hooks/use-marketplace'; +import { useInstallationProgress } from '@/hooks/use-marketplace'; + +interface MarketplacePluginCardProps { + plugin: MarketplacePluginInfo; + onRefresh?: () => void; +} + +export function MarketplacePluginCard({ plugin, onRefresh }: MarketplacePluginCardProps) { + const { installPlugin, uninstallPlugin, updatePlugin, refreshPluginStatuses, loading } = useMarketplaceActions(); + const { data: installProgress } = useInstallationProgress( + loading[`install_${plugin.name}`] || loading[`update_${plugin.name}`] ? plugin.name : null + ); + + const [showDetails, setShowDetails] = useState(false); + + const isInstalling = loading[`install_${plugin.name}`] || installProgress?.status === 'installing'; + const isUpdating = loading[`update_${plugin.name}`] || installProgress?.status === 'updating'; + const isUninstalling = loading[`uninstall_${plugin.name}`]; + const isLoading = isInstalling || isUpdating || isUninstalling; + + const handleInstall = async () => { + try { + await installPlugin(plugin.name); + // Wait a moment for the installation to complete, then refresh statuses + setTimeout(async () => { + await refreshPluginStatuses(); + onRefresh?.(); + // Also refresh the main plugins list + mutate('plugins'); + }, 1000); + } catch (error) { + // Error is handled in the hook + } + }; + + const handleUninstall = async () => { + try { + await uninstallPlugin(plugin.name); + // Wait a moment for the uninstallation to complete, then refresh statuses + setTimeout(async () => { + await refreshPluginStatuses(); + onRefresh?.(); + // Also refresh the main plugins list + mutate('plugins'); + }, 1000); + } catch (error) { + // Error is handled in the hook + } + }; + + const handleUpdate = async () => { + try { + await updatePlugin(plugin.name); + // Wait a moment for the update to complete, then refresh statuses + setTimeout(async () => { + await refreshPluginStatuses(); + onRefresh?.(); + // Also refresh the main plugins list + mutate('plugins'); + }, 1000); + } catch (error) { + // Error is handled in the hook + } + }; + + const getStatusBadge = () => { + switch (plugin.status) { + case 'available': + return Available; + case 'installed': + return Installed; + case 'update_available': + return Update Available; + default: + return Unknown; + } + }; + + const getServiceTypeBadge = () => { + const colors = { + asr: 'bg-purple-100 text-purple-800 border-purple-200', + tts: 'bg-green-100 text-green-800 border-green-200', + llm: 'bg-blue-100 text-blue-800 border-blue-200', + }; + + return ( + + {plugin.service_type.toUpperCase()} + + ); + }; + + const formatFileSize = (bytes?: number) => { + if (!bytes) return 'Unknown size'; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; + }; + + const formatDate = (dateString?: string) => { + if (!dateString) return 'Unknown'; + return new Date(dateString).toLocaleDateString(); + }; + + return ( + + +
+
+ {plugin.name} + + {plugin.description || 'No description available'} + +
+
+ {getServiceTypeBadge()} + {getStatusBadge()} +
+
+
+ + + {/* Plugin Info */} +
+
+ + v{plugin.version} + {plugin.installed_version && plugin.installed_version !== plugin.version && ( + (installed: v{plugin.installed_version}) + )} +
+ + {plugin.author && ( +
+ + {plugin.author} +
+ )} + + + + {plugin.updated_at && ( +
+ + Updated {formatDate(plugin.updated_at)} +
+ )} +
+ + {/* Tags */} + {plugin.tags && plugin.tags.length > 0 && ( +
+ {plugin.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} + {plugin.tags.length > 3 && ( + + +{plugin.tags.length - 3} more + + )} +
+ )} + + {/* Installation Progress */} + {installProgress && (isInstalling || isUpdating) && ( +
+
+ + {installProgress.current_step} +
+ +
+ Step {installProgress.current_step_index} of {installProgress.total_steps} + ({installProgress.progress_percentage.toFixed(0)}%) +
+
+ )} + + {/* Error Message */} + {installProgress?.error_message && ( +
+ + {installProgress.error_message} +
+ )} + + {/* Additional Details */} + {showDetails && ( +
+
+ Size: {formatFileSize(plugin.file_size)} +
+ {plugin.license && ( +
+ License: {plugin.license} +
+ )} + {plugin.dependencies && plugin.dependencies.length > 0 && ( +
+ Dependencies: +
+ {plugin.dependencies.map((dep) => ( + + {dep} + + ))} +
+
+ )} +
+ )} +
+ + +
+
+ + + {plugin.repository_url && ( + + )} +
+ +
+ {plugin.status === 'available' && ( + + )} + + {plugin.status === 'installed' && ( + + )} + + {plugin.status === 'update_available' && ( + <> + + + + + )} +
+
+
+
+ ); +} diff --git a/olv-launcher-ui/components/navigation.tsx b/olv-launcher-ui/components/navigation.tsx new file mode 100644 index 0000000..3102321 --- /dev/null +++ b/olv-launcher-ui/components/navigation.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { usePathname } from 'next/navigation'; +import Link from 'next/link'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Home, Store, Settings } from 'lucide-react'; + +interface NavItem { + href: string; + label: string; + icon: React.ComponentType<{ size?: number }>; + description: string; + badge?: string | number; +} + +export function Navigation() { + const pathname = usePathname(); + + const navItems: NavItem[] = [ + { + href: '/', + label: 'Dashboard', + icon: Home, + description: 'Plugin management and system status', + }, + { + href: '/marketplace', + label: 'Marketplace', + icon: Store, + description: 'Discover and install plugins', + }, + ]; + + return ( + + ); +} diff --git a/olv-launcher-ui/components/plugin-card.tsx b/olv-launcher-ui/components/plugin-card.tsx new file mode 100644 index 0000000..1a0e7f0 --- /dev/null +++ b/olv-launcher-ui/components/plugin-card.tsx @@ -0,0 +1,194 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { useState } from 'react'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { PluginStatusBadge } from './plugin-status-badge'; +import { PluginInfo, PluginStatus } from '@/lib/api'; +import { usePluginActions } from '@/hooks/use-plugins'; +import { Play, Square, Settings, Terminal, Monitor, Cloud, ExternalLink } from 'lucide-react'; +import { PluginConfigDialog } from '@/components/plugin-config-dialog'; +import { PluginLogsDialog } from '@/components/plugin-logs-dialog'; + +interface PluginCardProps { + plugin: PluginInfo; + onRefresh?: () => void; +} + +export function PluginCard({ plugin, onRefresh }: PluginCardProps) { + const { startPlugin, stopPlugin, loading } = usePluginActions(); + const [configDialogOpen, setConfigDialogOpen] = useState(false); + const [logsDialogOpen, setLogsDialogOpen] = useState(false); + + const isRunning = plugin.status === PluginStatus.RUNNING; + const isLoading = loading[plugin.name]; + + // Use plugin status directly + const displayStatus = plugin.status; + + const handleStart = async () => { + try { + await startPlugin(plugin.name); + onRefresh?.(); + } catch (error) { + // Error is handled in the hook + } + }; + + const handleStop = async () => { + try { + await stopPlugin(plugin.name); + onRefresh?.(); + } catch (error) { + // Error is handled in the hook + } + }; + + const getServiceTypeColor = (serviceType: string) => { + switch (serviceType.toLowerCase()) { + case 'asr': + return 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800'; + case 'tts': + return 'bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-900/20 dark:text-purple-400 dark:border-purple-800'; + case 'llm': + return 'bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/20 dark:text-orange-400 dark:border-orange-800'; + default: + return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/20 dark:text-gray-400 dark:border-gray-800'; + } + }; + + return ( + <> + + +
+
+ {plugin.name} + + {plugin.description || 'No description available'} + +
+
+ + {plugin.service_type.toUpperCase()} + + +
+
+
+ + +
+
+ {plugin.version && ( +
+ Version: + {plugin.version} +
+ )} +
+ Type: +
+ {plugin.is_local ? ( + + ) : ( + + )} + + {plugin.is_local ? 'Local' : 'Remote'} + +
+
+
+
+ URL: +
+ + {plugin.service_url || 'None'} + + {!plugin.is_local && plugin.service_url && ( + + )} +
+
+
+
+ + +
+ {/* Start/Stop button */} + {isRunning ? ( + + ) : ( + + )} + + {/* Configuration button */} + + + {/* Logs button */} + +
+
+
+ + {/* Configuration Dialog */} + + + {/* Logs Dialog */} + + + ); +} \ No newline at end of file diff --git a/olv-launcher-ui/components/plugin-config-dialog.tsx b/olv-launcher-ui/components/plugin-config-dialog.tsx new file mode 100644 index 0000000..3867207 --- /dev/null +++ b/olv-launcher-ui/components/plugin-config-dialog.tsx @@ -0,0 +1,487 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useState, useEffect } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Loader2, AlertCircle, Settings, RefreshCw, Plus, Trash2, Database, Copy, Eye, CheckCircle, Clock } from 'lucide-react'; +import { apiClient, PluginSchemasResponse, PluginInstanceListResponse, PluginInstanceInfo, PluginStatus } from '@/lib/api'; +import { toast } from 'sonner'; + +// Import RJSF with shadcn theme +import { RJSFSchema, UiSchema, RJSFValidationError } from '@rjsf/utils'; +import validator from '@rjsf/validator-ajv8'; +import { generateForm } from '@rjsf/shadcn'; + +interface PluginConfigDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + pluginName: string; + pluginStatus?: PluginStatus; + onInstanceChanged?: () => void; +} + +// Form data type +type FormData = Record; + +// Import RJSF types +import { IChangeEvent } from '@rjsf/core'; + +export function PluginConfigDialog({ open, onOpenChange, pluginName, pluginStatus, onInstanceChanged }: PluginConfigDialogProps) { + const [loading, setLoading] = useState(false); + const [saving, setSaving] = useState(false); + const [instancesLoading, setInstancesLoading] = useState(false); + const [error, setError] = useState(null); + const [schemas, setSchemas] = useState(null); + const [instances, setInstances] = useState(null); + + // Form data state - for plugin configuration + const [pluginFormData, setPluginFormData] = useState({}); + + useEffect(() => { + if (open && pluginName) { + // Check if plugin is running before attempting to load schemas + if (pluginStatus && pluginStatus !== PluginStatus.RUNNING) { + setError(`Plugin ${pluginName} is not running (status: ${pluginStatus})`); + setLoading(false); + return; + } + + loadSchemas(); + loadInstances(); + } else { + // Reset state when dialog closes + setError(null); + setInstances(null); + } + }, [open, pluginName, pluginStatus]); + + const loadSchemas = async () => { + setLoading(true); + setError(null); + try { + const response = await apiClient.getPluginSchemas(pluginName); + setSchemas(response); + + // Initialize form data with default values if available + if (response.schemas.plugin_json_schema) { + const pluginSchema = response.schemas.plugin_json_schema as { default?: FormData }; + setPluginFormData(pluginSchema.default || {}); + } + + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'Failed to load schemas'; + setError(message); + + // Check if error is related to plugin not running + if (message.includes('not running') || message.includes('Connection refused') || message.includes('404')) { + toast.error(`Plugin ${pluginName} must be running to load configuration schemas`); + } else { + toast.error(`Failed to load configuration schemas: ${message}`); + } + } finally { + setLoading(false); + } + }; + + const loadInstances = async () => { + setInstancesLoading(true); + try { + const response = await apiClient.listPluginInstances(pluginName); + setInstances(response); + } catch (err: unknown) { + // Don't show error toast for instances loading failure + + const message = err instanceof Error ? err.message : 'Failed to load instances'; + console.error('Failed to load instances:', message); + } finally { + setInstancesLoading(false); + } + }; + + const handleCreateInstance = async (data: IChangeEvent) => { + setSaving(true); + try { + // Clear previous errors + setError(null); + + if (!data.formData) { + throw new Error('No configuration data provided'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const response = await apiClient.createPluginInstance(pluginName, data.formData); + + toast.success(`Instance created successfully: ${response.instance_id.substring(0, 8)}...`); + + // Reload instances list + await loadInstances(); + + // Reset form to default values + if (schemas?.schemas.plugin_json_schema) { + const pluginSchema = schemas.schemas.plugin_json_schema as { default?: FormData }; + setPluginFormData(pluginSchema.default || {}); + } + + if (onInstanceChanged) { + onInstanceChanged(); + } + + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to create instance'; + toast.error(`Failed to create instance: ${message}`); + } finally { + setSaving(false); + } + }; + + const handleDeleteInstance = async (instanceId: string) => { + try { + await apiClient.deletePluginInstance(pluginName, instanceId); + toast.success(`Instance ${instanceId.substring(0, 8)}... deleted successfully`); + + // Reload instances list + await loadInstances(); + + if (onInstanceChanged) { + onInstanceChanged(); + } + + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to delete instance'; + toast.error(`Failed to delete instance: ${message}`); + } + }; + + const handleCopyInstanceId = (instanceId: string) => { + navigator.clipboard.writeText(instanceId); + toast.success('Instance ID copied to clipboard'); + }; + + const handleFormError = (errors: RJSFValidationError[]) => { + // Only show toast notification for validation errors, don't update state + if (errors.length > 0) { + const errorMessages = errors + .filter(error => error.message) + .map(error => error.message) + .join(', '); + + toast.error(`Please fill in required fields: ${errorMessages}`); + } + }; + + const renderInstanceCard = (instanceId: string, instance: PluginInstanceInfo) => { + const shortId = instanceId.substring(0, 8); + const isReady = instance.ready; + + return ( + + +
+
+ + Instance {shortId}... + + {isReady ? ( + <> + + Ready + + ) : ( + <> + + Initializing + + )} + +
+
+ + +
+
+
+ +
+
ID: {instanceId}
+
+ Status: +
+ {isReady ? ( + <> + + Ready for use + + ) : ( + <> + + Initializing... + + )} +
+
+
+
+
+ ); + }; + + const renderCreateInstanceForm = () => { + if (!schemas?.schemas.plugin_json_schema) { + return ( +
+
+
+
+ +
+
+

+ No Configuration Schema Available +

+

+ No plugin configuration schema is available for this plugin. The plugin may not support configuration options. +

+
+
+
+
+ ); + } + + // Generate the shadcn form components + const ShadcnForm = generateForm(); + + return ( +
+
+ + + + + Create New Instance + + + Configure a new instance for {pluginName} plugin + + + + ) => setPluginFormData(e.formData || {})} + showErrorList={false} + noHtml5Validate={false} + liveValidate={false} + id="plugin-config-form" + > +
+ {/* Hide the default submit button */} +
+
+
+
+
+ +
+
+ + +
+
+
+ ); + }; + + const renderInstancesList = () => { + if (instancesLoading) { + return ( +
+
+ + Loading instances... +
+
+ ); + } + + const instanceEntries = Object.entries(instances?.instances.instances || {}); + + return ( +
+
+
+

Plugin Instances

+

+ {instances?.instances.total_instances} instance{instances?.instances.total_instances !== 1 ? 's' : ''} found +

+
+ +
+ +
+
+ {instanceEntries.map(([instanceId, instance]) => + renderInstanceCard(instanceId, instance) + )} +
+
+
+ ); + }; + + const hasPluginSchema = schemas?.schemas.plugin_json_schema && + Object.keys(schemas.schemas.plugin_json_schema).length > 0; + + return ( + + + + + + Manage {pluginName} Instances + + + Create and manage plugin instances with different configurations + + + +
+ {loading && ( +
+ + Loading configuration schemas... +
+ )} + + {error && ( +
+
+
+
+ +
+
+

+ {(error.includes('not running') || error.includes('Connection refused') || error.includes('404')) + ? 'Plugin Not Running' + : 'Configuration Error'} +

+

+ {(error.includes('not running') || error.includes('Connection refused') || error.includes('404')) + ? `Plugin ${pluginName} must be running to access configuration management. Please start the plugin first.` + : error} +

+ +
+
+
+
+ )} + + {schemas && !loading && ( + + + + + View Instances + + + + Create Instance + + + +
+ + {renderInstancesList()} + + + + {renderCreateInstanceForm()} + +
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/olv-launcher-ui/components/plugin-logs-dialog.tsx b/olv-launcher-ui/components/plugin-logs-dialog.tsx new file mode 100644 index 0000000..8ed36bf --- /dev/null +++ b/olv-launcher-ui/components/plugin-logs-dialog.tsx @@ -0,0 +1,340 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useState, useEffect, useRef } from 'react'; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { Loader2, Terminal, Trash2, Download, RefreshCw, AlertCircle } from 'lucide-react'; +import { apiClient } from '@/lib/api'; +import { usePluginActions } from '@/hooks/use-plugins'; +import { toast } from 'sonner'; + +interface PluginLogsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + pluginName: string; +} + +export function PluginLogsDialog({ open, onOpenChange, pluginName }: PluginLogsDialogProps) { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); + const [clearing, setClearing] = useState(false); + const [error, setError] = useState(null); + const [autoRefresh, setAutoRefresh] = useState(false); + const { clearLogs } = usePluginActions(); + const scrollAreaRef = useRef(null); + const intervalRef = useRef(null); + + useEffect(() => { + if (open && pluginName) { + loadLogs(); + } else { + // Reset state when dialog closes + setError(null); + setAutoRefresh(false); + } + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [open, pluginName]); + + useEffect(() => { + if (autoRefresh && open) { + intervalRef.current = setInterval(() => loadLogs(true), 3000); // Refresh every 3 seconds + } else if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [autoRefresh, open]); + + // Auto-scroll to bottom when new logs are added + useEffect(() => { + if (scrollAreaRef.current && logs.length > 0) { + scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight; + } + }, [logs]); + + const loadLogs = async (isAutoRefresh = false) => { + if (!pluginName) return; + + if (!isAutoRefresh) { + setLoading(true); + } else { + setRefreshing(true); + } + setError(null); + + try { + const response = await apiClient.getPluginLogs(pluginName, 200); + setLogs(response.logs); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'Failed to load logs'; + setError(message); + if (!isAutoRefresh) { + toast.error(`Failed to load logs: ${message}`); + } + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + const handleClearLogs = async () => { + setClearing(true); + try { + await clearLogs(pluginName); + setLogs([]); + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to clear logs'; + toast.error(`Failed to clear logs: ${message}`); + } finally { + setClearing(false); + } + }; + + const handleDownloadLogs = () => { + try { + const logContent = logs.join('\n'); + const blob = new Blob([logContent], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${pluginName}-logs-${new Date().toISOString().split('T')[0]}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success('Logs downloaded successfully'); + } catch (err) { + const message = err instanceof Error ? err.message : 'Failed to download logs'; + toast.error(message); + } + }; + + return ( + + + + + + terminal@olv:~$ tail -f {pluginName}.log + + + Real-time log streaming for {pluginName} plugin + + + + {/* Terminal Header Bar */} +
+
+
+
+
+
+
+ + {pluginName} - Log Terminal + +
+
+ + + + + + + +
+
+ + {/* Terminal Content */} +
+ {loading && logs.length === 0 && ( +
+ + Initializing terminal... +
+ )} + + {error && ( +
+
+
+ + ERROR: {error} +
+ +
+
+ )} + + {logs.length > 0 ? ( +
+
+ {logs.map((line, index) => { + // Parse timestamp and log level + const timestampMatch = line.match(/^\[([\d-:\s]+)\]/); + const levelMatch = line.match(/\[(DEBUG|INFO|WARNING|ERROR|CRITICAL)\]/i); + + let lineColor = 'text-slate-300'; + let bgClass = ''; + + if (levelMatch) { + switch (levelMatch[1].toUpperCase()) { + case 'ERROR': + case 'CRITICAL': + lineColor = 'text-red-400'; + bgClass = 'bg-red-900/10'; + break; + case 'WARNING': + lineColor = 'text-yellow-400'; + bgClass = 'bg-yellow-900/10'; + break; + case 'INFO': + lineColor = 'text-blue-400'; + bgClass = 'bg-blue-900/10'; + break; + case 'DEBUG': + lineColor = 'text-slate-500'; + break; + } + } + + return ( +
+
+ + {String(index + 1).padStart(3, '0')} + +
+ {timestampMatch && ( + + {timestampMatch[1]} + + )} + {levelMatch && ( + + {levelMatch[1].toUpperCase()} + + )} + {line.replace(/^\[[\d-:\s]+\]/, '').replace(/\[(DEBUG|INFO|WARNING|ERROR|CRITICAL)\]/i, '').trim()} +
+
+
+ ); + })} + + {/* Terminal Cursor */} +
+ $ + + {autoRefresh ? 'auto-refreshing...' : 'ready'} + +
+
+
+
+ ) : !loading && !error && ( +
+ +
+
No log entries found
+
Terminal is ready for input...
+
+
+ )} +
+ + {/* Terminal Status Bar */} +
+
+ + {logs.length > 0 ? `${logs.length} lines` : 'empty'} + + {autoRefresh && ( + +
+ live +
+ )} +
+
+ encoding: utf-8 + tail -f mode + {new Date().toLocaleTimeString()} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/olv-launcher-ui/components/plugin-status-badge.tsx b/olv-launcher-ui/components/plugin-status-badge.tsx new file mode 100644 index 0000000..ff6107d --- /dev/null +++ b/olv-launcher-ui/components/plugin-status-badge.tsx @@ -0,0 +1,71 @@ +import { Badge } from '@/components/ui/badge'; +import { AlertCircle, XCircle, Loader2, StopCircle, Play } from 'lucide-react'; +import { PluginStatus } from '@/lib/api'; + +interface PluginStatusBadgeProps { + status: PluginStatus; + size?: 'sm' | 'lg'; +} + +export function PluginStatusBadge({ status, size = 'sm' }: PluginStatusBadgeProps) { + const getStatusConfig = () => { + switch (status) { + case PluginStatus.RUNNING: + return { + variant: 'default' as const, + icon: Play, + className: 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-800', + text: 'Running', + }; + case PluginStatus.STOPPED: + return { + variant: 'destructive' as const, + icon: StopCircle, + className: 'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800', + text: 'Stopped', + }; + case PluginStatus.STARTING: + return { + variant: 'outline' as const, + icon: Loader2, + className: 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400 dark:border-blue-800', + text: 'Starting', + }; + case PluginStatus.STOPPING: + return { + variant: 'outline' as const, + icon: Loader2, + className: 'bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/20 dark:text-orange-400 dark:border-orange-800', + text: 'Stopping', + }; + case PluginStatus.ERROR: + return { + variant: 'destructive' as const, + icon: XCircle, + className: 'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800', + text: 'Error', + }; + default: + return { + variant: 'outline' as const, + icon: AlertCircle, + className: 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/20 dark:text-yellow-400 dark:border-yellow-800', + text: 'Unknown', + }; + } + }; + + const config = getStatusConfig(); + const Icon = config.icon; + const iconSize = size === 'sm' ? 12 : 16; + + return ( + + + {config.text} + + ); +} \ No newline at end of file diff --git a/olv-launcher-ui/components/ui/alert-dialog.tsx b/olv-launcher-ui/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..0863e40 --- /dev/null +++ b/olv-launcher-ui/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/olv-launcher-ui/components/ui/alert.tsx b/olv-launcher-ui/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/olv-launcher-ui/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/olv-launcher-ui/components/ui/badge.tsx b/olv-launcher-ui/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/olv-launcher-ui/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/olv-launcher-ui/components/ui/button.tsx b/olv-launcher-ui/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/olv-launcher-ui/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/olv-launcher-ui/components/ui/card.tsx b/olv-launcher-ui/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/olv-launcher-ui/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/olv-launcher-ui/components/ui/dialog.tsx b/olv-launcher-ui/components/ui/dialog.tsx new file mode 100644 index 0000000..7d7a9d3 --- /dev/null +++ b/olv-launcher-ui/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/olv-launcher-ui/components/ui/input.tsx b/olv-launcher-ui/components/ui/input.tsx new file mode 100644 index 0000000..03295ca --- /dev/null +++ b/olv-launcher-ui/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/olv-launcher-ui/components/ui/label.tsx b/olv-launcher-ui/components/ui/label.tsx new file mode 100644 index 0000000..fb5fbc3 --- /dev/null +++ b/olv-launcher-ui/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/olv-launcher-ui/components/ui/progress.tsx b/olv-launcher-ui/components/ui/progress.tsx new file mode 100644 index 0000000..e7a416c --- /dev/null +++ b/olv-launcher-ui/components/ui/progress.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Progress } diff --git a/olv-launcher-ui/components/ui/scroll-area.tsx b/olv-launcher-ui/components/ui/scroll-area.tsx new file mode 100644 index 0000000..8e4fa13 --- /dev/null +++ b/olv-launcher-ui/components/ui/scroll-area.tsx @@ -0,0 +1,58 @@ +"use client" + +import * as React from "react" +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" + +import { cn } from "@/lib/utils" + +function ScrollArea({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + + ) +} + +function ScrollBar({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { ScrollArea, ScrollBar } diff --git a/olv-launcher-ui/components/ui/select.tsx b/olv-launcher-ui/components/ui/select.tsx new file mode 100644 index 0000000..dcbbc0c --- /dev/null +++ b/olv-launcher-ui/components/ui/select.tsx @@ -0,0 +1,185 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Select({ + ...props +}: React.ComponentProps) { + return +} + +function SelectGroup({ + ...props +}: React.ComponentProps) { + return +} + +function SelectValue({ + ...props +}: React.ComponentProps) { + return +} + +function SelectTrigger({ + className, + size = "default", + children, + ...props +}: React.ComponentProps & { + size?: "sm" | "default" +}) { + return ( + + {children} + + + + + ) +} + +function SelectContent({ + className, + children, + position = "popper", + ...props +}: React.ComponentProps) { + return ( + + + + + {children} + + + + + ) +} + +function SelectLabel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function SelectSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SelectScrollUpButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function SelectScrollDownButton({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +} diff --git a/olv-launcher-ui/components/ui/separator.tsx b/olv-launcher-ui/components/ui/separator.tsx new file mode 100644 index 0000000..67c73e5 --- /dev/null +++ b/olv-launcher-ui/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/olv-launcher-ui/components/ui/sheet.tsx b/olv-launcher-ui/components/ui/sheet.tsx new file mode 100644 index 0000000..84649ad --- /dev/null +++ b/olv-launcher-ui/components/ui/sheet.tsx @@ -0,0 +1,139 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Sheet({ ...props }: React.ComponentProps) { + return +} + +function SheetTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function SheetClose({ + ...props +}: React.ComponentProps) { + return +} + +function SheetPortal({ + ...props +}: React.ComponentProps) { + return +} + +function SheetOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetContent({ + className, + children, + side = "right", + ...props +}: React.ComponentProps & { + side?: "top" | "right" | "bottom" | "left" +}) { + return ( + + + + {children} + + + Close + + + + ) +} + +function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function SheetTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function SheetDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Sheet, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/olv-launcher-ui/components/ui/slider.tsx b/olv-launcher-ui/components/ui/slider.tsx new file mode 100644 index 0000000..09391e8 --- /dev/null +++ b/olv-launcher-ui/components/ui/slider.tsx @@ -0,0 +1,63 @@ +"use client" + +import * as React from "react" +import * as SliderPrimitive from "@radix-ui/react-slider" + +import { cn } from "@/lib/utils" + +function Slider({ + className, + defaultValue, + value, + min = 0, + max = 100, + ...props +}: React.ComponentProps) { + const _values = React.useMemo( + () => + Array.isArray(value) + ? value + : Array.isArray(defaultValue) + ? defaultValue + : [min, max], + [value, defaultValue, min, max] + ) + + return ( + + + + + {Array.from({ length: _values.length }, (_, index) => ( + + ))} + + ) +} + +export { Slider } diff --git a/olv-launcher-ui/components/ui/sonner.tsx b/olv-launcher-ui/components/ui/sonner.tsx new file mode 100644 index 0000000..0e5836d --- /dev/null +++ b/olv-launcher-ui/components/ui/sonner.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner, ToasterProps } from "sonner" + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + div]:!text-white", + success: "!bg-green-600 !border-green-700 !text-white [&>div]:!text-white", + warning: "!bg-yellow-600 !border-yellow-700 !text-white [&>div]:!text-white", + info: "!bg-blue-600 !border-blue-700 !text-white [&>div]:!text-white", + }, + }} + style={ + { + "--normal-bg": "var(--popover)", + "--normal-text": "var(--popover-foreground)", + "--normal-border": "var(--border)", + } as React.CSSProperties + } + {...props} + /> + ) +} + +export { Toaster } diff --git a/olv-launcher-ui/components/ui/switch.tsx b/olv-launcher-ui/components/ui/switch.tsx new file mode 100644 index 0000000..6a2b524 --- /dev/null +++ b/olv-launcher-ui/components/ui/switch.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SwitchPrimitive from "@radix-ui/react-switch" + +import { cn } from "@/lib/utils" + +function Switch({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +export { Switch } diff --git a/olv-launcher-ui/components/ui/table.tsx b/olv-launcher-ui/components/ui/table.tsx new file mode 100644 index 0000000..51b74dd --- /dev/null +++ b/olv-launcher-ui/components/ui/table.tsx @@ -0,0 +1,116 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/olv-launcher-ui/components/ui/tabs.tsx b/olv-launcher-ui/components/ui/tabs.tsx new file mode 100644 index 0000000..497ba5e --- /dev/null +++ b/olv-launcher-ui/components/ui/tabs.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/olv-launcher-ui/components/ui/textarea.tsx b/olv-launcher-ui/components/ui/textarea.tsx new file mode 100644 index 0000000..7f21b5e --- /dev/null +++ b/olv-launcher-ui/components/ui/textarea.tsx @@ -0,0 +1,18 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { + return ( + + + + +
+ +
+{
+  "audio_data": "Actual Base64 data will appear here after recording",
+  "character_specific_config": {
+    "use_itn": true,
+    "whisper_language": "zh",
+    "whisper_task": "transcribe",
+    "character_name": "Test Character",
+    "voice_style": "formal",
+    "language_preference": "chinese"
+  }
+}
+            
+ +
+ + + + + \ No newline at end of file diff --git a/tests/olv_main/core/test_config.py b/tests/olv_main/core/test_config.py new file mode 100644 index 0000000..99de1fb --- /dev/null +++ b/tests/olv_main/core/test_config.py @@ -0,0 +1,104 @@ +from unittest.mock import patch + +import pytest +from pydantic import ValidationError + +from olv_main.core.config import OLVMainSettings + + +def test_default_settings(): + """Verify that default settings are loaded correctly without any env vars.""" + settings = OLVMainSettings() + + assert settings.host == "localhost" + assert settings.port == 12393 + assert not settings.reload + assert settings.health_check_interval == 30 + assert settings.launcher_url == "http://127.0.0.1:7000" + assert settings.log_level == "INFO" + assert settings.log_format == "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + assert settings.cors_origins == ["*"] + assert settings.cors_credentials is True + assert settings.cors_methods == ["*"] + assert settings.cors_headers == ["*"] + + +def test_settings_from_env_vars(monkeypatch): + """Verify that settings are correctly loaded from environment variables.""" + monkeypatch.setenv("OLV_CORE_HOST", "0.0.0.0") + monkeypatch.setenv("OLV_CORE_PORT", "8888") + monkeypatch.setenv("OLV_CORE_RELOAD", "true") + monkeypatch.setenv("OLV_CORE_HEALTH_CHECK_INTERVAL", "60") + monkeypatch.setenv("OLV_CORE_LAUNCHER_URL", "http://localhost:8000") + monkeypatch.setenv("OLV_CORE_LOG_LEVEL", "DEBUG") + monkeypatch.setenv("OLV_CORE_LOG_FORMAT", "custom_format") + monkeypatch.setenv("OLV_CORE_CORS_ORIGINS", '["http://localhost:3000"]') + monkeypatch.setenv("OLV_CORE_CORS_CREDENTIALS", "false") + monkeypatch.setenv("OLV_CORE_CORS_METHODS", '["GET", "POST"]') + monkeypatch.setenv("OLV_CORE_CORS_HEADERS", '["X-Test"]') + + settings = OLVMainSettings() + + assert settings.host == "0.0.0.0" + assert settings.port == 8888 + assert settings.reload is True + assert settings.health_check_interval == 60 + assert settings.launcher_url == "http://localhost:8000" + assert settings.log_level == "DEBUG" + assert settings.log_format == "custom_format" + assert settings.cors_origins == ["http://localhost:3000"] + assert settings.cors_credentials is False + assert settings.cors_methods == ["GET", "POST"] + assert settings.cors_headers == ["X-Test"] + + +def test_settings_from_env_file(tmp_path): + """Verify that settings are correctly loaded from a .env file.""" + env_file = tmp_path / ".env" + env_file.write_text( + """ + OLV_CORE_HOST=192.168.1.1 + OLV_CORE_PORT=9999 + OLV_CORE_RELOAD=1 + OLV_CORE_LOG_LEVEL=WARNING + """ + ) + + settings = OLVMainSettings(_env_file=env_file, _env_file_encoding="utf-8") + + assert settings.host == "192.168.1.1" + assert settings.port == 9999 + assert settings.reload is True + assert settings.log_level == "WARNING" + # Check a default value is still there + assert settings.health_check_interval == 30 + + +def test_env_vars_override_env_file(monkeypatch, tmp_path): + """Verify that environment variables have priority over .env file settings.""" + env_file = tmp_path / ".env" + env_file.write_text("OLV_CORE_PORT=9999") + + monkeypatch.setenv("OLV_CORE_PORT", "8888") + + # Pydantic-settings uses the current working directory to find the .env file. + # We change it temporarily to the tmp_path. + with patch("os.getcwd", return_value=str(tmp_path)): + settings = OLVMainSettings() + + assert settings.port == 8888 + + +def test_validation_error_for_invalid_port(): + """Verify that a validation error is raised for an invalid port number.""" + with pytest.raises(ValidationError): + OLVMainSettings(port=1000) + + with pytest.raises(ValidationError): + OLVMainSettings(port=70000) + + +def test_validation_error_for_invalid_health_check_interval(): + """Verify that a validation error is raised for an invalid health check interval.""" + with pytest.raises(ValidationError): + OLVMainSettings(health_check_interval=4) \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..dfd56ba --- /dev/null +++ b/uv.lock @@ -0,0 +1,608 @@ +version = 1 +revision = 2 +requires-python = ">=3.10, <3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "fastapi" +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/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639, upload-time = "2024-10-16T19:45:08.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/6f/972f8eb0ea7d98a1c6be436e2142d51ad2a64ee18e02b0e7ff1f62171ab1/httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0", size = 198780, upload-time = "2024-10-16T19:44:06.882Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b0/17c672b4bc5c7ba7f201eada4e96c71d0a59fbc185e60e42580093a86f21/httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da", size = 103297, upload-time = "2024-10-16T19:44:08.129Z" }, + { url = "https://files.pythonhosted.org/packages/92/5e/b4a826fe91971a0b68e8c2bd4e7db3e7519882f5a8ccdb1194be2b3ab98f/httptools-0.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deee0e3343f98ee8047e9f4c5bc7cedbf69f5734454a94c38ee829fb2d5fa3c1", size = 443130, upload-time = "2024-10-16T19:44:09.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/51/ce61e531e40289a681a463e1258fa1e05e0be54540e40d91d065a264cd8f/httptools-0.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca80b7485c76f768a3bc83ea58373f8db7b015551117375e4918e2aa77ea9b50", size = 442148, upload-time = "2024-10-16T19:44:11.539Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/270b7d767849b0c96f275c695d27ca76c30671f8eb8cc1bab6ced5c5e1d0/httptools-0.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90d96a385fa941283ebd231464045187a31ad932ebfa541be8edf5b3c2328959", size = 415949, upload-time = "2024-10-16T19:44:13.388Z" }, + { url = "https://files.pythonhosted.org/packages/81/86/ced96e3179c48c6f656354e106934e65c8963d48b69be78f355797f0e1b3/httptools-0.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59e724f8b332319e2875efd360e61ac07f33b492889284a3e05e6d13746876f4", size = 417591, upload-time = "2024-10-16T19:44:15.258Z" }, + { url = "https://files.pythonhosted.org/packages/75/73/187a3f620ed3175364ddb56847d7a608a6fc42d551e133197098c0143eca/httptools-0.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:c26f313951f6e26147833fc923f78f95604bbec812a43e5ee37f26dc9e5a686c", size = 88344, upload-time = "2024-10-16T19:44:16.54Z" }, + { url = "https://files.pythonhosted.org/packages/7b/26/bb526d4d14c2774fe07113ca1db7255737ffbb119315839af2065abfdac3/httptools-0.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f47f8ed67cc0ff862b84a1189831d1d33c963fb3ce1ee0c65d3b0cbe7b711069", size = 199029, upload-time = "2024-10-16T19:44:18.427Z" }, + { url = "https://files.pythonhosted.org/packages/a6/17/3e0d3e9b901c732987a45f4f94d4e2c62b89a041d93db89eafb262afd8d5/httptools-0.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0614154d5454c21b6410fdf5262b4a3ddb0f53f1e1721cfd59d55f32138c578a", size = 103492, upload-time = "2024-10-16T19:44:19.515Z" }, + { url = "https://files.pythonhosted.org/packages/b7/24/0fe235d7b69c42423c7698d086d4db96475f9b50b6ad26a718ef27a0bce6/httptools-0.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8787367fbdfccae38e35abf7641dafc5310310a5987b689f4c32cc8cc3ee975", size = 462891, upload-time = "2024-10-16T19:44:21.067Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2f/205d1f2a190b72da6ffb5f41a3736c26d6fa7871101212b15e9b5cd8f61d/httptools-0.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b0f7fe4fd38e6a507bdb751db0379df1e99120c65fbdc8ee6c1d044897a636", size = 459788, upload-time = "2024-10-16T19:44:22.958Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4c/d09ce0eff09057a206a74575ae8f1e1e2f0364d20e2442224f9e6612c8b9/httptools-0.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40a5ec98d3f49904b9fe36827dcf1aadfef3b89e2bd05b0e35e94f97c2b14721", size = 433214, upload-time = "2024-10-16T19:44:24.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d2/84c9e23edbccc4a4c6f96a1b8d99dfd2350289e94f00e9ccc7aadde26fb5/httptools-0.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dacdd3d10ea1b4ca9df97a0a303cbacafc04b5cd375fa98732678151643d4988", size = 434120, upload-time = "2024-10-16T19:44:26.295Z" }, + { url = "https://files.pythonhosted.org/packages/d0/46/4d8e7ba9581416de1c425b8264e2cadd201eb709ec1584c381f3e98f51c1/httptools-0.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:288cd628406cc53f9a541cfaf06041b4c71d751856bab45e3702191f931ccd17", size = 88565, upload-time = "2024-10-16T19:44:29.188Z" }, + { url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683, upload-time = "2024-10-16T19:44:30.175Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337, upload-time = "2024-10-16T19:44:31.786Z" }, + { url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796, upload-time = "2024-10-16T19:44:32.825Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837, upload-time = "2024-10-16T19:44:33.974Z" }, + { url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289, upload-time = "2024-10-16T19:44:35.111Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779, upload-time = "2024-10-16T19:44:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634, upload-time = "2024-10-16T19:44:37.357Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "open-llm-vtuber" +version = "2.0.0" +source = { editable = "." } +dependencies = [ + { name = "fastapi" }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[package.metadata] +requires-dist = [ + { name = "fastapi", specifier = ">=0.115.12" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pydantic", specifier = ">=2.11.5" }, + { name = "pydantic-settings", specifier = ">=2.9.1" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.2" }, +] + +[package.metadata.requires-dev] +dev = [] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102, upload-time = "2025-05-22T21:18:08.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229, upload-time = "2025-05-22T21:18:06.329Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/92/b31726561b5dae176c2d2c2dc43a9c5bfba5d32f96f8b4c0a600dd492447/pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", size = 2028817, upload-time = "2025-04-23T18:30:43.919Z" }, + { url = "https://files.pythonhosted.org/packages/a3/44/3f0b95fafdaca04a483c4e685fe437c6891001bf3ce8b2fded82b9ea3aa1/pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", size = 1861357, upload-time = "2025-04-23T18:30:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/30/97/e8f13b55766234caae05372826e8e4b3b96e7b248be3157f53237682e43c/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", size = 1898011, upload-time = "2025-04-23T18:30:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a3/99c48cf7bafc991cc3ee66fd544c0aae8dc907b752f1dad2d79b1b5a471f/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", size = 1982730, upload-time = "2025-04-23T18:30:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/de/8e/a5b882ec4307010a840fb8b58bd9bf65d1840c92eae7534c7441709bf54b/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", size = 2136178, upload-time = "2025-04-23T18:30:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/e4/bb/71e35fc3ed05af6834e890edb75968e2802fe98778971ab5cba20a162315/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", size = 2736462, upload-time = "2025-04-23T18:30:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/c8f7593e6bc7066289bbc366f2235701dcbebcd1ff0ef8e64f6f239fb47d/pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", size = 2005652, upload-time = "2025-04-23T18:30:53.389Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7a/996d8bd75f3eda405e3dd219ff5ff0a283cd8e34add39d8ef9157e722867/pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", size = 2113306, upload-time = "2025-04-23T18:30:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/ff/84/daf2a6fb2db40ffda6578a7e8c5a6e9c8affb251a05c233ae37098118788/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", size = 2073720, upload-time = "2025-04-23T18:30:56.11Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/2258da019f4825128445ae79456a5499c032b55849dbd5bed78c95ccf163/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", size = 2244915, upload-time = "2025-04-23T18:30:57.501Z" }, + { url = "https://files.pythonhosted.org/packages/d8/7a/925ff73756031289468326e355b6fa8316960d0d65f8b5d6b3a3e7866de7/pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", size = 2241884, upload-time = "2025-04-23T18:30:58.867Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b0/249ee6d2646f1cdadcb813805fe76265745c4010cf20a8eba7b0e639d9b2/pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", size = 1910496, upload-time = "2025-04-23T18:31:00.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/ff/172ba8f12a42d4b552917aa65d1f2328990d3ccfc01d5b7c943ec084299f/pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", size = 1955019, upload-time = "2025-04-23T18:31:01.335Z" }, + { url = "https://files.pythonhosted.org/packages/3f/8d/71db63483d518cbbf290261a1fc2839d17ff89fce7089e08cad07ccfce67/pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", size = 2028584, upload-time = "2025-04-23T18:31:03.106Z" }, + { url = "https://files.pythonhosted.org/packages/24/2f/3cfa7244ae292dd850989f328722d2aef313f74ffc471184dc509e1e4e5a/pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", size = 1855071, upload-time = "2025-04-23T18:31:04.621Z" }, + { url = "https://files.pythonhosted.org/packages/b3/d3/4ae42d33f5e3f50dd467761304be2fa0a9417fbf09735bc2cce003480f2a/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", size = 1897823, upload-time = "2025-04-23T18:31:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/aa5976e8352b7695ff808599794b1fba2a9ae2ee954a3426855935799488/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", size = 1983792, upload-time = "2025-04-23T18:31:07.93Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7a/cda9b5a23c552037717f2b2a5257e9b2bfe45e687386df9591eff7b46d28/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", size = 2136338, upload-time = "2025-04-23T18:31:09.283Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/b8f9ec8dd1417eb9da784e91e1667d58a2a4a7b7b34cf4af765ef663a7e5/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", size = 2730998, upload-time = "2025-04-23T18:31:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/47/bc/cd720e078576bdb8255d5032c5d63ee5c0bf4b7173dd955185a1d658c456/pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", size = 2003200, upload-time = "2025-04-23T18:31:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/ca/22/3602b895ee2cd29d11a2b349372446ae9727c32e78a94b3d588a40fdf187/pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", size = 2113890, upload-time = "2025-04-23T18:31:15.011Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e6/e3c5908c03cf00d629eb38393a98fccc38ee0ce8ecce32f69fc7d7b558a7/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d", size = 2073359, upload-time = "2025-04-23T18:31:16.393Z" }, + { url = "https://files.pythonhosted.org/packages/12/e7/6a36a07c59ebefc8777d1ffdaf5ae71b06b21952582e4b07eba88a421c79/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", size = 2245883, upload-time = "2025-04-23T18:31:17.892Z" }, + { url = "https://files.pythonhosted.org/packages/16/3f/59b3187aaa6cc0c1e6616e8045b284de2b6a87b027cce2ffcea073adf1d2/pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", size = 2241074, upload-time = "2025-04-23T18:31:19.205Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ed/55532bb88f674d5d8f67ab121a2a13c385df382de2a1677f30ad385f7438/pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", size = 1910538, upload-time = "2025-04-23T18:31:20.541Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1b/25b7cccd4519c0b23c2dd636ad39d381abf113085ce4f7bec2b0dc755eb1/pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", size = 1952909, upload-time = "2025-04-23T18:31:22.371Z" }, + { url = "https://files.pythonhosted.org/packages/49/a9/d809358e49126438055884c4366a1f6227f0f84f635a9014e2deb9b9de54/pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", size = 1897786, upload-time = "2025-04-23T18:31:24.161Z" }, + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/30/68/373d55e58b7e83ce371691f6eaa7175e3a24b956c44628eb25d7da007917/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", size = 2023982, upload-time = "2025-04-23T18:32:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/a4/16/145f54ac08c96a63d8ed6442f9dec17b2773d19920b627b18d4f10a061ea/pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", size = 1858412, upload-time = "2025-04-23T18:32:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/b1/c6dc6c3e2de4516c0bb2c46f6a373b91b5660312342a0cf5826e38ad82fa/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", size = 1892749, upload-time = "2025-04-23T18:32:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/12/73/8cd57e20afba760b21b742106f9dbdfa6697f1570b189c7457a1af4cd8a0/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", size = 2067527, upload-time = "2025-04-23T18:32:59.771Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d5/0bb5d988cc019b3cba4a78f2d4b3854427fc47ee8ec8e9eaabf787da239c/pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", size = 2108225, upload-time = "2025-04-23T18:33:04.51Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c5/00c02d1571913d496aabf146106ad8239dc132485ee22efe08085084ff7c/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", size = 2069490, upload-time = "2025-04-23T18:33:06.391Z" }, + { url = "https://files.pythonhosted.org/packages/22/a8/dccc38768274d3ed3a59b5d06f59ccb845778687652daa71df0cab4040d7/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", size = 2237525, upload-time = "2025-04-23T18:33:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e7/4f98c0b125dda7cf7ccd14ba936218397b44f50a56dd8c16a3091df116c3/pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", size = 2238446, upload-time = "2025-04-23T18:33:10.313Z" }, + { url = "https://files.pythonhosted.org/packages/ce/91/2ec36480fdb0b783cd9ef6795753c1dea13882f2e68e73bce76ae8c21e6a/pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", size = 2066678, upload-time = "2025-04-23T18:33:12.224Z" }, + { url = "https://files.pythonhosted.org/packages/7b/27/d4ae6487d73948d6f20dddcd94be4ea43e74349b56eba82e9bdee2d7494c/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", size = 2025200, upload-time = "2025-04-23T18:33:14.199Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/b3cb95375f05d33801024079b9392a5ab45267a63400bf1866e7ce0f0de4/pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", size = 1859123, upload-time = "2025-04-23T18:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/0d0b5adeda59a261cd30a1235a445bf55c7e46ae44aea28f7bd6ed46e091/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", size = 1892852, upload-time = "2025-04-23T18:33:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d37bdebbda2e449cb3f519f6ce950927b56d62f0b84fd9cb9e372a26a3d5/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", size = 2067484, upload-time = "2025-04-23T18:33:20.475Z" }, + { url = "https://files.pythonhosted.org/packages/8c/55/1f95f0a05ce72ecb02a8a8a1c3be0579bbc29b1d5ab68f1378b7bebc5057/pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", size = 2108896, upload-time = "2025-04-23T18:33:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/53/89/2b2de6c81fa131f423246a9109d7b2a375e83968ad0800d6e57d0574629b/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", size = 2069475, upload-time = "2025-04-23T18:33:24.528Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e9/1f7efbe20d0b2b10f6718944b5d8ece9152390904f29a78e68d4e7961159/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", size = 2239013, upload-time = "2025-04-23T18:33:26.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/5309c905a93811524a49b4e031e9851a6b00ff0fb668794472ea7746b448/pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", size = 2238715, upload-time = "2025-04-23T18:33:28.656Z" }, + { url = "https://files.pythonhosted.org/packages/32/56/8a7ca5d2cd2cda1d245d34b1c9a942920a718082ae8e54e5f3e5a58b7add/pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", size = 2066757, upload-time = "2025-04-23T18:33:30.645Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741, upload-time = "2024-10-14T23:38:35.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/76/44a55515e8c9505aa1420aebacf4dd82552e5e15691654894e90d0bd051a/uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f", size = 1442019, upload-time = "2024-10-14T23:37:20.068Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/62d5800358a78cc25c8a6c72ef8b10851bdb8cca22e14d9c74167b7f86da/uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d", size = 801898, upload-time = "2024-10-14T23:37:22.663Z" }, + { url = "https://files.pythonhosted.org/packages/f3/96/63695e0ebd7da6c741ccd4489b5947394435e198a1382349c17b1146bb97/uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26", size = 3827735, upload-time = "2024-10-14T23:37:25.129Z" }, + { url = "https://files.pythonhosted.org/packages/61/e0/f0f8ec84979068ffae132c58c79af1de9cceeb664076beea86d941af1a30/uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb", size = 3825126, upload-time = "2024-10-14T23:37:27.59Z" }, + { url = "https://files.pythonhosted.org/packages/bf/fe/5e94a977d058a54a19df95f12f7161ab6e323ad49f4dabc28822eb2df7ea/uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f", size = 3705789, upload-time = "2024-10-14T23:37:29.385Z" }, + { url = "https://files.pythonhosted.org/packages/26/dd/c7179618e46092a77e036650c1f056041a028a35c4d76945089fcfc38af8/uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c", size = 3800523, upload-time = "2024-10-14T23:37:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/57/a7/4cf0334105c1160dd6819f3297f8700fda7fc30ab4f61fbf3e725acbc7cc/uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8", size = 1447410, upload-time = "2024-10-14T23:37:33.612Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7c/1517b0bbc2dbe784b563d6ab54f2ef88c890fdad77232c98ed490aa07132/uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0", size = 805476, upload-time = "2024-10-14T23:37:36.11Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ea/0bfae1aceb82a503f358d8d2fa126ca9dbdb2ba9c7866974faec1cb5875c/uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e", size = 3960855, upload-time = "2024-10-14T23:37:37.683Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ca/0864176a649838b838f36d44bf31c451597ab363b60dc9e09c9630619d41/uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb", size = 3973185, upload-time = "2024-10-14T23:37:40.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/bf/08ad29979a936d63787ba47a540de2132169f140d54aa25bc8c3df3e67f4/uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6", size = 3820256, upload-time = "2024-10-14T23:37:42.839Z" }, + { url = "https://files.pythonhosted.org/packages/da/e2/5cf6ef37e3daf2f06e651aae5ea108ad30df3cb269102678b61ebf1fdf42/uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d", size = 3937323, upload-time = "2024-10-14T23:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284, upload-time = "2024-10-14T23:37:47.833Z" }, + { url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349, upload-time = "2024-10-14T23:37:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089, upload-time = "2024-10-14T23:37:51.703Z" }, + { url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770, upload-time = "2024-10-14T23:37:54.122Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321, upload-time = "2024-10-14T23:37:55.766Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022, upload-time = "2024-10-14T23:37:58.195Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/03/e2/8ed598c42057de7aa5d97c472254af4906ff0a59a66699d426fc9ef795d7/watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9", size = 94537, upload-time = "2025-04-08T10:36:26.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/4d/d02e6ea147bb7fff5fd109c694a95109612f419abed46548a930e7f7afa3/watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40", size = 405632, upload-time = "2025-04-08T10:34:41.832Z" }, + { url = "https://files.pythonhosted.org/packages/60/31/9ee50e29129d53a9a92ccf1d3992751dc56fc3c8f6ee721be1c7b9c81763/watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb", size = 395734, upload-time = "2025-04-08T10:34:44.236Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8c/759176c97195306f028024f878e7f1c776bda66ccc5c68fa51e699cf8f1d/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11", size = 455008, upload-time = "2025-04-08T10:34:45.617Z" }, + { url = "https://files.pythonhosted.org/packages/55/1a/5e977250c795ee79a0229e3b7f5e3a1b664e4e450756a22da84d2f4979fe/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487", size = 459029, upload-time = "2025-04-08T10:34:46.814Z" }, + { url = "https://files.pythonhosted.org/packages/e6/17/884cf039333605c1d6e296cf5be35fad0836953c3dfd2adb71b72f9dbcd0/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256", size = 488916, upload-time = "2025-04-08T10:34:48.571Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e0/bcb6e64b45837056c0a40f3a2db3ef51c2ced19fda38484fa7508e00632c/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85", size = 523763, upload-time = "2025-04-08T10:34:50.268Z" }, + { url = "https://files.pythonhosted.org/packages/24/e9/f67e9199f3bb35c1837447ecf07e9830ec00ff5d35a61e08c2cd67217949/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358", size = 502891, upload-time = "2025-04-08T10:34:51.419Z" }, + { url = "https://files.pythonhosted.org/packages/23/ed/a6cf815f215632f5c8065e9c41fe872025ffea35aa1f80499f86eae922db/watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614", size = 454921, upload-time = "2025-04-08T10:34:52.67Z" }, + { url = "https://files.pythonhosted.org/packages/92/4c/e14978599b80cde8486ab5a77a821e8a982ae8e2fcb22af7b0886a033ec8/watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f", size = 631422, upload-time = "2025-04-08T10:34:53.985Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1a/9263e34c3458f7614b657f974f4ee61fd72f58adce8b436e16450e054efd/watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d", size = 625675, upload-time = "2025-04-08T10:34:55.173Z" }, + { url = "https://files.pythonhosted.org/packages/96/1f/1803a18bd6ab04a0766386a19bcfe64641381a04939efdaa95f0e3b0eb58/watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff", size = 277921, upload-time = "2025-04-08T10:34:56.318Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3b/29a89de074a7d6e8b4dc67c26e03d73313e4ecf0d6e97e942a65fa7c195e/watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92", size = 291526, upload-time = "2025-04-08T10:34:57.95Z" }, + { url = "https://files.pythonhosted.org/packages/39/f4/41b591f59021786ef517e1cdc3b510383551846703e03f204827854a96f8/watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827", size = 405336, upload-time = "2025-04-08T10:34:59.359Z" }, + { url = "https://files.pythonhosted.org/packages/ae/06/93789c135be4d6d0e4f63e96eea56dc54050b243eacc28439a26482b5235/watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4", size = 395977, upload-time = "2025-04-08T10:35:00.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/1cd89bd83728ca37054512d4d35ab69b5f12b8aa2ac9be3b0276b3bf06cc/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d", size = 455232, upload-time = "2025-04-08T10:35:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/d8a4d44ffe960517e487c9c04f77b06b8abf05eb680bed71c82b5f2cad62/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63", size = 459151, upload-time = "2025-04-08T10:35:03.358Z" }, + { url = "https://files.pythonhosted.org/packages/6c/da/267a1546f26465dead1719caaba3ce660657f83c9d9c052ba98fb8856e13/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418", size = 489054, upload-time = "2025-04-08T10:35:04.561Z" }, + { url = "https://files.pythonhosted.org/packages/b1/31/33850dfd5c6efb6f27d2465cc4c6b27c5a6f5ed53c6fa63b7263cf5f60f6/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9", size = 523955, upload-time = "2025-04-08T10:35:05.786Z" }, + { url = "https://files.pythonhosted.org/packages/09/84/b7d7b67856efb183a421f1416b44ca975cb2ea6c4544827955dfb01f7dc2/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6", size = 502234, upload-time = "2025-04-08T10:35:07.187Z" }, + { url = "https://files.pythonhosted.org/packages/71/87/6dc5ec6882a2254cfdd8b0718b684504e737273903b65d7338efaba08b52/watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25", size = 454750, upload-time = "2025-04-08T10:35:08.859Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6c/3786c50213451a0ad15170d091570d4a6554976cf0df19878002fc96075a/watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5", size = 631591, upload-time = "2025-04-08T10:35:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b3/1427425ade4e359a0deacce01a47a26024b2ccdb53098f9d64d497f6684c/watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01", size = 625370, upload-time = "2025-04-08T10:35:12.412Z" }, + { url = "https://files.pythonhosted.org/packages/15/ba/f60e053b0b5b8145d682672024aa91370a29c5c921a88977eb565de34086/watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246", size = 277791, upload-time = "2025-04-08T10:35:13.719Z" }, + { url = "https://files.pythonhosted.org/packages/50/ed/7603c4e164225c12c0d4e8700b64bb00e01a6c4eeea372292a3856be33a4/watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096", size = 291622, upload-time = "2025-04-08T10:35:15.071Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c2/99bb7c96b4450e36877fde33690ded286ff555b5a5c1d925855d556968a1/watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed", size = 283699, upload-time = "2025-04-08T10:35:16.732Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8c/4f0b9bdb75a1bfbd9c78fad7d8854369283f74fe7cf03eb16be77054536d/watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2", size = 401511, upload-time = "2025-04-08T10:35:17.956Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4e/7e15825def77f8bd359b6d3f379f0c9dac4eb09dd4ddd58fd7d14127179c/watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f", size = 392715, upload-time = "2025-04-08T10:35:19.202Z" }, + { url = "https://files.pythonhosted.org/packages/58/65/b72fb817518728e08de5840d5d38571466c1b4a3f724d190cec909ee6f3f/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec", size = 454138, upload-time = "2025-04-08T10:35:20.586Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a4/86833fd2ea2e50ae28989f5950b5c3f91022d67092bfec08f8300d8b347b/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21", size = 458592, upload-time = "2025-04-08T10:35:21.87Z" }, + { url = "https://files.pythonhosted.org/packages/38/7e/42cb8df8be9a37e50dd3a818816501cf7a20d635d76d6bd65aae3dbbff68/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512", size = 487532, upload-time = "2025-04-08T10:35:23.143Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fd/13d26721c85d7f3df6169d8b495fcac8ab0dc8f0945ebea8845de4681dab/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d", size = 522865, upload-time = "2025-04-08T10:35:24.702Z" }, + { url = "https://files.pythonhosted.org/packages/a1/0d/7f9ae243c04e96c5455d111e21b09087d0eeaf9a1369e13a01c7d3d82478/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6", size = 499887, upload-time = "2025-04-08T10:35:25.969Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0f/a257766998e26aca4b3acf2ae97dff04b57071e991a510857d3799247c67/watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234", size = 454498, upload-time = "2025-04-08T10:35:27.353Z" }, + { url = "https://files.pythonhosted.org/packages/81/79/8bf142575a03e0af9c3d5f8bcae911ee6683ae93a625d349d4ecf4c8f7df/watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2", size = 630663, upload-time = "2025-04-08T10:35:28.685Z" }, + { url = "https://files.pythonhosted.org/packages/f1/80/abe2e79f610e45c63a70d271caea90c49bbf93eb00fa947fa9b803a1d51f/watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663", size = 625410, upload-time = "2025-04-08T10:35:30.42Z" }, + { url = "https://files.pythonhosted.org/packages/91/6f/bc7fbecb84a41a9069c2c6eb6319f7f7df113adf113e358c57fc1aff7ff5/watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249", size = 277965, upload-time = "2025-04-08T10:35:32.023Z" }, + { url = "https://files.pythonhosted.org/packages/99/a5/bf1c297ea6649ec59e935ab311f63d8af5faa8f0b86993e3282b984263e3/watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705", size = 291693, upload-time = "2025-04-08T10:35:33.225Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7b/fd01087cc21db5c47e5beae507b87965db341cce8a86f9eb12bf5219d4e0/watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417", size = 283287, upload-time = "2025-04-08T10:35:34.568Z" }, + { url = "https://files.pythonhosted.org/packages/1a/03/81f9fcc3963b3fc415cd4b0b2b39ee8cc136c42fb10a36acf38745e9d283/watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d", size = 405947, upload-time = "2025-04-08T10:36:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/54/97/8c4213a852feb64807ec1d380f42d4fc8bfaef896bdbd94318f8fd7f3e4e/watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034", size = 397276, upload-time = "2025-04-08T10:36:15.131Z" }, + { url = "https://files.pythonhosted.org/packages/78/12/d4464d19860cb9672efa45eec1b08f8472c478ed67dcd30647c51ada7aef/watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965", size = 455550, upload-time = "2025-04-08T10:36:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/b07bcdf1034d8edeaef4c22f3e9e3157d37c5071b5f9492ffdfa4ad4bed7/watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57", size = 455542, upload-time = "2025-04-08T10:36:18.655Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { 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, upload-time = "2025-03-05T20:02:16.706Z" }, + { 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, upload-time = "2025-03-05T20:02:18.832Z" }, + { 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, upload-time = "2025-03-05T20:02:20.187Z" }, + { 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, upload-time = "2025-03-05T20:02:22.286Z" }, + { 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, upload-time = "2025-03-05T20:02:24.368Z" }, + { 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, upload-time = "2025-03-05T20:02:25.669Z" }, + { 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, upload-time = "2025-03-05T20:02:26.99Z" }, + { 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, upload-time = "2025-03-05T20:02:30.291Z" }, + { 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, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +]