Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa1fec87c7 | ||
|
|
74a1dd5ea6 | ||
|
|
ef47f1660d | ||
|
|
4341c329fd | ||
|
|
fdf3ab2688 |
@@ -19,8 +19,6 @@ AUTH_HTTPS_ONLY=true
|
||||
DEBUG=False
|
||||
DEBUG_DATABASE=False
|
||||
BUNDLE_ASSETS=True
|
||||
# add `?profiler=true` to the url to enable the profiler for that request
|
||||
PROFILER=False
|
||||
|
||||
# logging into LNBITS_DATA_FOLDER/logs/
|
||||
ENABLE_LOG_TO_FILE=true
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
# Feature Spec: [FEAT-XXX] - Short Descriptive Title
|
||||
|
||||
**Milestone:** [e.g. MVP Core / Performance & Polish / Extension Framework]
|
||||
**Priority:** Must-have / Should-have / Nice-to-have
|
||||
**Spec Owner:** [Your Name / AI Agent Name]
|
||||
**Status:** Draft → Under Review → Approved → Implemented → Verified
|
||||
|
||||
## 1. Purpose & User Story
|
||||
|
||||
As a [user type], I want [goal] so that [benefit].
|
||||
|
||||
_(One clear sentence. Keep it concise.)_
|
||||
|
||||
## 2. Functional Requirements
|
||||
|
||||
- [ ] REQ-1: [Clear, testable description]
|
||||
- [ ] REQ-2: ...
|
||||
- [ ] REQ-3: ...
|
||||
|
||||
_(List what the feature must do. Make each item verifiable.)_
|
||||
|
||||
## 3. Non-Functional Requirements
|
||||
|
||||
- **Performance:** [e.g. Latency < 800ms at p95, max 5k tokens, etc.]
|
||||
- **Security / Safety:** [e.g. Input validation, no raw errors to user, etc.]
|
||||
- **Compatibility:** [e.g. Works with all existing wallet backends, no breaking changes for extensions]
|
||||
- **UI/UX:** [e.g. Follows existing Quasar/Vue patterns in wallet.js]
|
||||
- **Other:** [cost, scalability, accessibility, etc.]
|
||||
|
||||
## 4. Technical Approach (Optional – recommended for complex features)
|
||||
|
||||
- Proposed solution: [e.g. Extend existing CRUD in lnbits/core/, new extension, middleware change, etc.]
|
||||
- Key files to modify: [list expected files]
|
||||
- New dependencies: [none / specific package + version]
|
||||
- Migration / Database changes: [yes/no + description]
|
||||
|
||||
## 5. Success Criteria & Verification
|
||||
|
||||
**Must pass all of these to be accepted:**
|
||||
|
||||
- [ ] All functional requirements (REQ-\*) implemented and tested
|
||||
- [ ] Non-functional requirements met (performance, security, etc.)
|
||||
- [ ] Relevant tests pass: `make test-unit`, `make test-api`, `make test-regtest` (as applicable)
|
||||
- [ ] `make check` passes (ruff, mypy, pyright, prettier, checkbundle)
|
||||
- [ ] Constitution compliance: All changes respect CONSTITUTION.md
|
||||
- [ ] Backward compatibility: No breakage for existing extensions or wallet backends
|
||||
- [ ] Documentation updated (if applicable: README, OpenAPI, inline comments)
|
||||
|
||||
**Additional Tests / Edge Cases:**
|
||||
|
||||
- [ ] Test invalid inputs / error paths
|
||||
- [ ] Test with FakeWallet and at least one real backend
|
||||
- [ ] Test with multiple extensions installed
|
||||
|
||||
## 6. Safety & Risk Assessment
|
||||
|
||||
- Potential risks: [e.g. Payment flow impact, key exposure, extension conflicts]
|
||||
- Mitigation: [how addressed]
|
||||
- Security review needed: [yes/no]
|
||||
|
||||
## 7. Implementation Notes (for AI / Developer)
|
||||
|
||||
- Style to match: Existing code patterns in `lnbits/core/` and `wallet.js`
|
||||
- Surgical changes only (per AGENTS.md)
|
||||
- Any known gotchas or dependencies on other features:
|
||||
|
||||
## 8. Acceptance Checklist (Sign-off)
|
||||
|
||||
- [ ] Spec reviewed and approved by project owner
|
||||
- [ ] Implementation completed
|
||||
- [ ] Verification steps passed
|
||||
- [ ] PR created with link to this spec
|
||||
- [ ] Constitution & AGENTS.md compliance confirmed
|
||||
|
||||
---
|
||||
|
||||
**Created:** [Date]
|
||||
**Last Updated:** [Date]
|
||||
**Approved By:** [Name / "Approved"]
|
||||
@@ -1,122 +0,0 @@
|
||||
# AGENTS.md - Instructions for All AI Coding Agents
|
||||
|
||||
This file is the **master instruction manual** for any AI agent (Grok, Claude, Cursor, Aider, etc.) working on LNbits.
|
||||
|
||||
## 1. Core Rule (Never Break This)
|
||||
|
||||
**You MUST read and strictly follow `CONSTITUTION.md` before doing any planning, coding, refactoring, or suggesting changes.**
|
||||
|
||||
- Every single change, feature, extension, or fix **must comply** with the Constitution.
|
||||
- If you detect a violation (in new code or existing code), you **must** flag it immediately and propose a fix or ask for clarification.
|
||||
- Constitution > any other instruction (including this file, user prompts, or previous conversations).
|
||||
|
||||
## 2. Mandatory Development Workflow
|
||||
|
||||
For **any non-trivial task** (new feature, bug fix, refactor, extension change):
|
||||
|
||||
1. **Constitution Check** – Re-read relevant sections of `CONSTITUTION.md`
|
||||
2. **Feature Spec Check** – If a spec exists in `.specify/`, follow it exactly. If none exists, ask the user for clarification or propose a minimal spec.
|
||||
3. **Think Step-by-Step** – Follow the "Think Before Coding" and "Simplicity First" guidelines below.
|
||||
4. **Surgical Changes** – Only touch what is necessary.
|
||||
5. **Implement**
|
||||
6. **Verify** – Run relevant tests (`make test-unit`, `make test-api`, etc.), `make check`, and confirm compliance.
|
||||
7. **Report** – Always include a clear summary.
|
||||
|
||||
Use the following response format:
|
||||
|
||||
```markdown
|
||||
## Constitution & Spec Compliance
|
||||
|
||||
- Relevant Constitution sections checked: [list or quote key rules]
|
||||
- Feature Spec followed: [yes / no / proposed]
|
||||
|
||||
## Assumptions & Plan
|
||||
|
||||
- Assumptions: ...
|
||||
- Plan:
|
||||
1. ...
|
||||
2. ...
|
||||
- Tradeoffs considered: ...
|
||||
|
||||
## Changes Made
|
||||
|
||||
- Files changed: ...
|
||||
- Summary of modifications:
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] Passes `make check`
|
||||
- [ ] Relevant tests pass (`make test-xxx`)
|
||||
- [ ] Complies with Constitution
|
||||
- [ ] Surgical & minimal (no unrelated changes)
|
||||
```
|
||||
|
||||
## 3. Behavioral Guidelines (Merged & Project-Specific)
|
||||
|
||||
**Think Before Coding**
|
||||
|
||||
- Don't assume. Don't hide confusion. Surface tradeoffs.
|
||||
- State assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them — don't pick silently.
|
||||
- If something is unclear (especially regarding wallets, extensions, or funding sources), stop and ask.
|
||||
|
||||
**Simplicity First**
|
||||
|
||||
- Minimum code that solves the problem. Nothing speculative.
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- Respect LNbits' lean core philosophy: new functionality should preferably go into an **extension** unless it truly belongs in core.
|
||||
|
||||
**Surgical Changes**
|
||||
|
||||
- Touch only what you must. Clean up only your own mess.
|
||||
- Match existing style (Python: Black + Ruff rules; JS: Prettier).
|
||||
- Do not "improve" or refactor adjacent code unless explicitly asked.
|
||||
- When editing, remove only imports/variables/functions made unused **by your changes**.
|
||||
- Never delete pre-existing dead code unless instructed.
|
||||
|
||||
**Goal-Driven Execution**
|
||||
|
||||
- Transform tasks into verifiable goals.
|
||||
- For tests: Write or update tests first when fixing bugs or adding behavior.
|
||||
- Always consider impact on existing extensions and multiple wallet backends (LND, CLN, Boltz, VoidWallet, etc.).
|
||||
|
||||
## 4. LNbits-Specific Rules
|
||||
|
||||
- **Extensions First**: Core should remain lean. Prefer implementing new features as extensions unless they are fundamental to wallets, security, or the API.
|
||||
- **Testing**: Use `FakeWallet` for unit/API tests. Regtest tests for full Lightning flows. Never break existing test targets in the Makefile.
|
||||
- **Dependencies**: Never add new dependencies without updating `pyproject.toml` and getting approval.
|
||||
- **Frontend**: JS/Vue code (e.g. `wallet.js`) must follow existing patterns and pass `make checkbundle` when static files are affected.
|
||||
- **Database / Migrations**: Do not make raw SQL changes. Use existing CRUD/services and migration tooling.
|
||||
- **Security**: Be extremely cautious with anything touching payments, keys, LNURL, Bolt11, or admin routes.
|
||||
- **Tools**: Use `uv run` for all commands. Prefer Makefile targets (`make format`, `make check`, `make test-xxx`).
|
||||
- **Generated Files**: Never modify gRPC files or other generated code.
|
||||
|
||||
## 5. Forbidden Behaviors
|
||||
|
||||
- Ignoring Constitution rules to "be helpful"
|
||||
- Large refactors without a spec or explicit request
|
||||
- Adding features "for future use"
|
||||
- Breaking backward compatibility for extensions or existing wallet backends
|
||||
- Committing code that fails `make check` or relevant tests
|
||||
- Exposing raw errors/stack traces to users
|
||||
- Using synchronous code in hot async paths without justification
|
||||
|
||||
## 6. How to Handle This File + Constitution
|
||||
|
||||
When the user gives you a task, start your response with:
|
||||
|
||||
> Following LNbits CONSTITUTION.md and AGENTS.md...
|
||||
|
||||
Then proceed with the structured format above.
|
||||
|
||||
---
|
||||
|
||||
**These guidelines are working if:**
|
||||
|
||||
- Fewer unnecessary changes appear in diffs
|
||||
- Clarifying questions come **before** implementation
|
||||
- All changes respect the lean, extension-first, security-first nature of LNbits
|
||||
- Tests and `make check` continue to pass
|
||||
|
||||
Last Updated: April 2026
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
# CONSTITUTION.md
|
||||
|
||||
This is the immutable constitution of the LNbits project.
|
||||
Every feature spec, code change, refactor, extension, or decision by humans or AI agents **must comply** with this document.
|
||||
Changes to this file require explicit approval from the project owner/maintainers.
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
**Project Name:** LNbits
|
||||
**Core Purpose:** Free and open-source Lightning wallet and accounts system. A lightweight Python server that sits on top of any Lightning funding source, providing safe isolated wallets, a clean REST API, and a powerful extension system for adding features rapidly.
|
||||
**Target Users:** Individuals, communities, merchants, developers, and enterprises building on Bitcoin/Lightning (self-hosted or as part of larger stacks).
|
||||
**High-Level Success Criteria:**
|
||||
|
||||
- Reliable multi-wallet Lightning accounting with any backend (20+ supported funding sources)
|
||||
- Secure, extensible via 60+ extensions without bloating core
|
||||
- High code quality, test coverage, and backward compatibility for extensions
|
||||
- Production-ready performance and security for real Bitcoin value
|
||||
|
||||
**Version:** 1.5.4
|
||||
|
||||
## 2. Technology Stack (Strict)
|
||||
|
||||
- **Language:** Python >=3.10, <3.13 (strictly enforced via `pyproject.toml`)
|
||||
- **Framework:** FastAPI + Starlette (backend API)
|
||||
- **Frontend:** Vue.js + Quasar framework, with bundled static assets
|
||||
- **Database:** SQLite (aiosqlite) by default, PostgreSQL (asyncpg/psycopg2) supported via `LNBITS_DATABASE_URL`
|
||||
- **Async Runtime:** uvloop preferred
|
||||
- **Dependency Management:** uv + pyproject.toml (Hatchling build backend). Use `uv run` for all commands.
|
||||
- **Wallet Backends:** Abstracted via `lnbits.wallets` – support for LND, Core Lightning, Phoenixd, Boltz, Breez SDK, Liquid, VoidWallet fallback, etc. New backends must follow existing abstraction.
|
||||
- **Other Key Libs:** SQLAlchemy, Pydantic (v1), Loguru, Jinja2, LNURL, Bolt11, etc. (see `pyproject.toml` for pinned versions)
|
||||
- **Build/Frontend Tools:** npm for bundling (Quasar/Vue), Prettier for JS/CSS (check Makefile targets)
|
||||
|
||||
**Forbidden:**
|
||||
|
||||
- Adding new top-level dependencies without updating `pyproject.toml` **and** team approval
|
||||
- Using synchronous blocking calls in async paths (except where explicitly justified)
|
||||
- Direct database queries outside of CRUD layers or core services
|
||||
- Modifying generated files (e.g., gRPC files in wallets/boltz_grpc_files or lnd_grpc_files)
|
||||
|
||||
## 3. Architecture & Code Organization (Mandatory Rules)
|
||||
|
||||
- **Core Principle:** Modular monolith with heavy emphasis on **extensions**. All non-core features must live in extensions (installed via `lnbits/extensions`). Core stays lean.
|
||||
- **Backend Structure:**
|
||||
- `lnbits/core/` for core models, CRUD, services, routers, tasks
|
||||
- `lnbits/wallets/` for funding source abstractions
|
||||
- `lnbits/extensions/` for installed/upgradeable extensions (do not commit large extensions to core repo)
|
||||
- `lnbits/static/` for bundled frontend assets (managed via npm bundle)
|
||||
- **Key Rules:**
|
||||
- Use dependency injection and FastAPI routers properly
|
||||
- Extensions register routes/tasks via `register_ext_routes` / `register_ext_tasks`
|
||||
- Database migrations handled centrally (extension-specific migrations)
|
||||
- All new endpoints must be under proper versioning/prefixing where applicable
|
||||
- Frontend: Vue 2/Quasar components in `wallet.js` style (or updated) – keep reactive, use LNbits.utils helpers
|
||||
- No circular imports; respect existing middleware order (e.g., InstalledExtensionMiddleware before ExtensionsRedirectMiddleware)
|
||||
|
||||
**Exclusions** (do not lint/format these):
|
||||
|
||||
- `lnbits/extensions/`, `lnbits/upgrades/`, generated gRPC files, static/vendor bundles
|
||||
|
||||
## 4. Code Quality & Style
|
||||
|
||||
- **Formatting & Linting:**
|
||||
- Python: Black (line-length 88), Ruff (with selected rules: F, E, W, I, A, C, N, UP, RUF, B, S), MyPy (strict where possible), Pyright
|
||||
- JS/Frontend: Prettier
|
||||
- Run via Makefile: `make format` and `make check`
|
||||
- **Type Checking:** MyPy + Pyright enforced on `lnbits/`, `tests/`, `tools/`
|
||||
- **Testing Requirements:**
|
||||
- Unit, API, wallet, and regtest tests via pytest (see Makefile targets)
|
||||
- New core code or critical paths: high coverage expected (`--cov=lnbits`)
|
||||
- Extensions should include their own tests where possible
|
||||
- **Error Handling & Logging:** Use Loguru with structured context. Never expose raw stack traces to end users. Graceful fallbacks (e.g., VoidWallet on funding source failure).
|
||||
- **Pre-commit:** Strongly recommended (`make install-pre-commit-hook`)
|
||||
- **Bundle Integrity:** Frontend bundles must pass `make checkbundle` before commits affecting static files.
|
||||
|
||||
## 5. AI / LLM Usage Standards (if any agents or future AI features are added)
|
||||
|
||||
- Any new AI-powered features (e.g., via extensions) must use structured outputs (Pydantic/JSON mode)
|
||||
- Store prompts/templates versioned in the extension
|
||||
- Prefer deterministic behavior for financial/security paths (low temperature)
|
||||
- All AI outputs involving value/money must be validated server-side
|
||||
- Safety: Never allow untrusted model output to influence payments, wallet balances, or admin actions without guardrails
|
||||
|
||||
## 6. Safety, Security & Ethics
|
||||
|
||||
- **Critical:** Handle real Bitcoin/Lightning value → security-first mindset
|
||||
- Per-wallet isolation with separate admin/invoice/read keys
|
||||
- Rate limiting (SlowAPI) and IP blocking middleware mandatory
|
||||
- Sanitize all user inputs; validate LNURL, Bolt11, etc.
|
||||
- PII: Minimal collection; respect privacy (no unnecessary logging of sensitive data)
|
||||
- Funding source failures: Graceful degradation to VoidWallet + clear logging
|
||||
- Extensions: Hash-verified installs for vetted extensions; careful with custom extension paths
|
||||
- Audit logging via AuditMiddleware
|
||||
- Forbidden: Hard-coded secrets, insecure subprocess calls without review, SQL injection risks (use SQLAlchemy properly)
|
||||
|
||||
## 7. Performance & Cost Budgets
|
||||
|
||||
- Keep core lightweight – extensions handle heavy features
|
||||
- Async-first (uvloop, asyncpg/aiosqlite)
|
||||
- Reasonable retry logic for funding source connections (see `check_funding_source`)
|
||||
- Frontend: Optimized bundles (checkbundle enforced)
|
||||
- No unnecessary blocking operations in request paths
|
||||
|
||||
## 8. Development Workflow (Spec-Driven where possible)
|
||||
|
||||
- **All significant changes** should follow Spec-Driven Development:
|
||||
- Create/update Feature Spec in `.specify/` folder (or equivalent)
|
||||
- Reference this Constitution in every spec and PR
|
||||
- Use Makefile targets for format/check/test
|
||||
- Tests run with `FakeWallet` by default for unit/API; regtest for full flows
|
||||
- PRs must:
|
||||
- Pass `make check` and relevant tests
|
||||
- Include Constitution compliance notes (via AGENTS.md/CLAUDE.md)
|
||||
- Not break existing extensions or wallet backends
|
||||
- Branching: Protect main; use feature branches
|
||||
- Extensions: Develop separately; core repo focuses on framework stability
|
||||
|
||||
## 9. Decision Hierarchy (What Takes Precedence)
|
||||
|
||||
1. This Constitution
|
||||
2. Approved Feature Spec / Milestone
|
||||
3. Existing tests and backward compatibility (especially for extensions and wallet backends)
|
||||
4. Project maintainers / owner decision
|
||||
5. Everything else (including helpful AI suggestions)
|
||||
|
||||
If conflict: Stop, document the issue, and seek clarification from maintainers.
|
||||
|
||||
## 10. Amendment Process
|
||||
|
||||
- This Constitution can only be changed with explicit approval from project maintainers.
|
||||
- All changes must be dated, versioned, and reflected in `AGENTS.md`.
|
||||
- Minor clarifications can be proposed via PR with justification.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated:** April 2026 (based on v1.5.4)
|
||||
**Owner/Maintainers Approval:** LNbits Team
|
||||
@@ -5,7 +5,7 @@
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
 [![license-badge]](LICENSE) [![docs-badge]][docs]  [](https://extensions.lnbits.com/) [](https://shop.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) [<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
<img alt="lnbits_head" src="docs/assets/header.jpg" />
|
||||
[](https://demo.lnbits.com/tipjar/DwaUiE4kBX6mUW6pj3X5Kg)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ nav_order: 1
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
|
||||
# LNBits Admin UI
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ nav_order: 1
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
|
||||
# Backend Wallet Comparison Table
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ nav_order: 1
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
   [](https://extensions.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
   [](https://extensions.lnbits.com/) [<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits) <img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">
|
||||
|
||||
# Basic installation
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ nav_order: 1
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
|
||||
# LNbits Super User (SU)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ nav_order: 1
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
|
||||
# LNbits Roles: A Quick Overview
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ nav_order: 3
|
||||

|
||||

|
||||
[<img src="https://img.shields.io/badge/community_chat-Telegram-24A1DE">](https://t.me/lnbits)
|
||||
[<img src="https://img.shields.io/badge/supported_by-%3E__OpenSats-f97316">](https://opensats.org)
|
||||
|
||||
# Backend wallets
|
||||
|
||||
|
||||
@@ -65,7 +65,6 @@ from .middleware import (
|
||||
InstalledExtensionMiddleware,
|
||||
add_first_install_middleware,
|
||||
add_ip_block_middleware,
|
||||
add_profiler_middleware,
|
||||
add_ratelimit_middleware,
|
||||
)
|
||||
from .tasks import internal_invoice_listener, invoice_listener, run_interval
|
||||
@@ -197,9 +196,6 @@ def create_app() -> FastAPI:
|
||||
|
||||
register_exception_handlers(app)
|
||||
|
||||
if settings.profiler:
|
||||
add_profiler_middleware(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ class ExplicitRelease(BaseModel):
|
||||
details_link: str | None
|
||||
paid_features: str | None
|
||||
pay_link: str | None
|
||||
admin_only: bool = False
|
||||
super_user_only: bool = False
|
||||
|
||||
def is_version_compatible(self):
|
||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||
@@ -52,6 +54,8 @@ class GitHubRelease(BaseModel):
|
||||
id: str
|
||||
organisation: str
|
||||
repository: str
|
||||
admin_only: bool = False
|
||||
super_user_only: bool = False
|
||||
|
||||
|
||||
class Manifest(BaseModel):
|
||||
@@ -83,6 +87,8 @@ class ExtensionConfig(BaseModel):
|
||||
warning: str | None = ""
|
||||
min_lnbits_version: str | None
|
||||
max_lnbits_version: str | None
|
||||
admin_only: bool = False
|
||||
super_user_only: bool = False
|
||||
|
||||
def is_version_compatible(self) -> bool:
|
||||
return is_lnbits_version_ok(self.min_lnbits_version, self.max_lnbits_version)
|
||||
@@ -195,6 +201,8 @@ class ExtensionRelease(BaseModel):
|
||||
cost_sats: int | None = None
|
||||
paid_sats: int | None = 0
|
||||
payment_hash: str | None = None
|
||||
admin_only: bool = False
|
||||
super_user_only: bool = False
|
||||
|
||||
@property
|
||||
def archive_url(self) -> str:
|
||||
@@ -263,6 +271,8 @@ class ExtensionRelease(BaseModel):
|
||||
paid_features=e.paid_features,
|
||||
repo=e.repo,
|
||||
icon=e.icon,
|
||||
admin_only=e.admin_only,
|
||||
super_user_only=e.super_user_only,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -289,6 +299,8 @@ class ExtensionRelease(BaseModel):
|
||||
release.min_lnbits_version = config.min_lnbits_version
|
||||
release.max_lnbits_version = config.max_lnbits_version
|
||||
release.is_version_compatible = config.is_version_compatible()
|
||||
release.admin_only = config.admin_only
|
||||
release.super_user_only = config.super_user_only
|
||||
|
||||
release.icon = icon_to_github_url(f"{org}/{repo}", config.tile)
|
||||
|
||||
@@ -336,6 +348,8 @@ class ExtensionMeta(BaseModel):
|
||||
paid_features: str | None = None
|
||||
has_paid_release: bool = False
|
||||
has_free_release: bool = False
|
||||
admin_only: bool = False
|
||||
super_user_only: bool = False
|
||||
|
||||
|
||||
class InstallableExtension(BaseModel):
|
||||
@@ -399,6 +413,16 @@ class InstallableExtension(BaseModel):
|
||||
return False
|
||||
return self.meta.pay_to_enable.required is True
|
||||
|
||||
@property
|
||||
def is_admin_only(self) -> bool:
|
||||
return settings.is_admin_extension(self.id) or bool(
|
||||
self.meta and self.meta.admin_only
|
||||
)
|
||||
|
||||
@property
|
||||
def is_super_user_only(self) -> bool:
|
||||
return bool(self.meta and self.meta.super_user_only)
|
||||
|
||||
async def download_archive(self):
|
||||
logger.info(f"Downloading extension {self.name} ({self.installed_version}).")
|
||||
ext_zip_file = self.zip_path
|
||||
@@ -454,6 +478,16 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
self.name = config_json.get("name")
|
||||
self.short_description = config_json.get("short_description")
|
||||
if self.meta:
|
||||
self.meta.admin_only = config_json.get("admin_only", False)
|
||||
self.meta.super_user_only = config_json.get("super_user_only", False)
|
||||
if self.meta.installed_release:
|
||||
self.meta.installed_release.admin_only = config_json.get(
|
||||
"admin_only", False
|
||||
)
|
||||
self.meta.installed_release.super_user_only = config_json.get(
|
||||
"super_user_only", False
|
||||
)
|
||||
|
||||
if (
|
||||
self.meta
|
||||
@@ -496,6 +530,10 @@ class InstallableExtension(BaseModel):
|
||||
return
|
||||
if not release.is_version_compatible:
|
||||
return
|
||||
if not self.meta:
|
||||
self.meta = ExtensionMeta()
|
||||
self.meta.admin_only = release.admin_only
|
||||
self.meta.super_user_only = release.super_user_only
|
||||
if not self.meta or not self.meta.latest_release:
|
||||
meta = self.meta or ExtensionMeta()
|
||||
meta.latest_release = release
|
||||
@@ -558,6 +596,13 @@ class InstallableExtension(BaseModel):
|
||||
github_release.organisation, github_release.repository
|
||||
)
|
||||
source_repo = f"{github_release.organisation}/{github_release.repository}"
|
||||
admin_only = github_release.admin_only or config.admin_only
|
||||
super_user_only = github_release.super_user_only or config.super_user_only
|
||||
latest_extension_release = ExtensionRelease.from_github_release(
|
||||
source_repo, latest_release
|
||||
)
|
||||
latest_extension_release.admin_only = admin_only
|
||||
latest_extension_release.super_user_only = super_user_only
|
||||
return InstallableExtension(
|
||||
id=github_release.id,
|
||||
name=config.name,
|
||||
@@ -569,9 +614,9 @@ class InstallableExtension(BaseModel):
|
||||
config.tile,
|
||||
),
|
||||
meta=ExtensionMeta(
|
||||
latest_release=ExtensionRelease.from_github_release(
|
||||
source_repo, latest_release
|
||||
),
|
||||
admin_only=admin_only,
|
||||
super_user_only=super_user_only,
|
||||
latest_release=latest_extension_release,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -580,7 +625,12 @@ class InstallableExtension(BaseModel):
|
||||
|
||||
@classmethod
|
||||
def from_explicit_release(cls, e: ExplicitRelease) -> InstallableExtension:
|
||||
meta = ExtensionMeta(archive=e.archive, dependencies=e.dependencies)
|
||||
meta = ExtensionMeta(
|
||||
archive=e.archive,
|
||||
dependencies=e.dependencies,
|
||||
admin_only=e.admin_only,
|
||||
super_user_only=e.super_user_only,
|
||||
)
|
||||
return InstallableExtension(
|
||||
id=e.id,
|
||||
name=e.name,
|
||||
@@ -610,6 +660,8 @@ class InstallableExtension(BaseModel):
|
||||
short_description=config_json.get("short_description"),
|
||||
icon=config_json.get("tile"),
|
||||
meta=ExtensionMeta(
|
||||
admin_only=config_json.get("admin_only", False),
|
||||
super_user_only=config_json.get("super_user_only", False),
|
||||
installed_release=ExtensionRelease(
|
||||
name=ext_id,
|
||||
version=version,
|
||||
@@ -617,7 +669,9 @@ class InstallableExtension(BaseModel):
|
||||
source_repo=f"{conf_path}",
|
||||
min_lnbits_version=config_json.get("min_lnbits_version"),
|
||||
max_lnbits_version=config_json.get("max_lnbits_version"),
|
||||
)
|
||||
admin_only=config_json.get("admin_only", False),
|
||||
super_user_only=config_json.get("super_user_only", False),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -719,6 +773,13 @@ class InstallableExtension(BaseModel):
|
||||
repo_releases = await ExtensionRelease.get_github_releases(
|
||||
r.organisation, r.repository
|
||||
)
|
||||
for repo_release in repo_releases:
|
||||
repo_release.admin_only = (
|
||||
repo_release.admin_only or r.admin_only
|
||||
)
|
||||
repo_release.super_user_only = (
|
||||
repo_release.super_user_only or r.super_user_only
|
||||
)
|
||||
extension_releases += repo_releases
|
||||
|
||||
for e in manifest.extensions:
|
||||
|
||||
@@ -14,8 +14,6 @@ from lnbits.core.services.fiat_providers import (
|
||||
verify_paypal_webhook,
|
||||
)
|
||||
from lnbits.core.services.payments import create_fiat_invoice
|
||||
from lnbits.fiat import get_fiat_provider
|
||||
from lnbits.fiat.paypal import PayPalWallet
|
||||
from lnbits.fiat.base import FiatSubscriptionPaymentOptions
|
||||
from lnbits.settings import settings
|
||||
|
||||
@@ -188,11 +186,7 @@ async def handle_paypal_event(event: dict):
|
||||
resource = event.get("resource", {})
|
||||
logger.info(f"Handling PayPal event: '{event_id}'. Type: '{event_type}'.")
|
||||
|
||||
if event_type == "CHECKOUT.ORDER.APPROVED":
|
||||
await _handle_paypal_checkout_order_approved(resource)
|
||||
return
|
||||
|
||||
if event_type == "PAYMENT.CAPTURE.COMPLETED":
|
||||
if event_type in ("CHECKOUT.ORDER.APPROVED", "PAYMENT.CAPTURE.COMPLETED"):
|
||||
payment_hash = _paypal_extract_payment_hash(resource)
|
||||
if not payment_hash:
|
||||
logger.warning("PayPal event missing payment hash.")
|
||||
@@ -211,30 +205,6 @@ async def handle_paypal_event(event: dict):
|
||||
logger.warning(f"Unhandled PayPal event type: '{event_type}'.")
|
||||
|
||||
|
||||
async def _handle_paypal_checkout_order_approved(resource: dict):
|
||||
payment_hash = _paypal_extract_payment_hash(resource)
|
||||
if not payment_hash:
|
||||
logger.warning("PayPal approved event missing payment hash.")
|
||||
return
|
||||
|
||||
payment = await get_standalone_payment(payment_hash)
|
||||
if not payment:
|
||||
logger.warning(f"No payment found for hash: '{payment_hash}'.")
|
||||
return
|
||||
|
||||
fiat_provider = await get_fiat_provider("paypal")
|
||||
if not isinstance(fiat_provider, PayPalWallet):
|
||||
logger.warning("PayPal provider unavailable for approved order capture.")
|
||||
return
|
||||
|
||||
capture_status = await fiat_provider.capture_order(
|
||||
payment.extra.get("fiat_checking_id") or payment.checking_id
|
||||
)
|
||||
if capture_status.failed:
|
||||
logger.warning(f"PayPal order capture failed for hash: '{payment_hash}'.")
|
||||
return
|
||||
|
||||
|
||||
async def _handle_paypal_subscription_payment(resource: dict):
|
||||
amount_info = resource.get("amount") or {}
|
||||
currency = (amount_info.get("currency") or "").upper()
|
||||
|
||||
@@ -79,7 +79,11 @@ async def api_install_extension(data: CreateExtension):
|
||||
raise HTTPException(HTTPStatus.BAD_REQUEST, "Incompatible extension version.")
|
||||
|
||||
release.payment_hash = data.payment_hash
|
||||
ext_meta = ExtensionMeta(installed_release=release)
|
||||
ext_meta = ExtensionMeta(
|
||||
installed_release=release,
|
||||
admin_only=release.admin_only,
|
||||
super_user_only=release.super_user_only,
|
||||
)
|
||||
ext_info = InstallableExtension(
|
||||
id=data.ext_id,
|
||||
name=data.ext_id,
|
||||
@@ -176,6 +180,19 @@ async def api_update_pay_to_enable(
|
||||
)
|
||||
|
||||
|
||||
def _check_enable_extension_access(
|
||||
ext_id: str, ext: InstallableExtension, account_id: AccountId
|
||||
) -> None:
|
||||
if ext.is_super_user_only and not settings.is_super_user(account_id.id):
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, f"User not authorized for extension '{ext_id}'."
|
||||
)
|
||||
if ext.is_admin_only and not settings.is_admin_user(account_id.id):
|
||||
raise HTTPException(
|
||||
HTTPStatus.FORBIDDEN, f"User not authorized for extension '{ext_id}'."
|
||||
)
|
||||
|
||||
|
||||
@extension_router.put("/{ext_id}/enable")
|
||||
async def api_enable_extension(
|
||||
ext_id: str, account_id: AccountId = Depends(check_account_id_exists)
|
||||
@@ -191,6 +208,7 @@ async def api_enable_extension(
|
||||
raise ValueError(f"Extension '{ext_id}' is not installed.")
|
||||
if not ext.active:
|
||||
raise ValueError(f"Extension '{ext_id}' is not activated.")
|
||||
_check_enable_extension_access(ext_id, ext, account_id)
|
||||
|
||||
user_ext = await get_user_extension(account_id.id, ext_id)
|
||||
if not user_ext:
|
||||
@@ -464,6 +482,8 @@ async def get_extension_release(org: str, repo: str, tag_name: str):
|
||||
"min_lnbits_version": config.min_lnbits_version,
|
||||
"is_version_compatible": config.is_version_compatible(),
|
||||
"warning": config.warning,
|
||||
"admin_only": config.admin_only,
|
||||
"super_user_only": config.super_user_only,
|
||||
}
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
@@ -573,7 +593,8 @@ async def extensions(account_id: AccountId = Depends(check_account_id_exists)):
|
||||
(True for version in db_versions if version.db == ext.id), False
|
||||
),
|
||||
"isAvailable": ext.id in all_ext_ids,
|
||||
"isAdminOnly": ext.id in settings.lnbits_admin_extensions,
|
||||
"isAdminOnly": ext.is_admin_only,
|
||||
"isSuperUserOnly": ext.is_super_user_only,
|
||||
"isActive": ext.id not in inactive_extensions,
|
||||
"latestRelease": (
|
||||
dict(ext.meta.latest_release)
|
||||
|
||||
+2
-4
@@ -35,7 +35,7 @@ if settings.lnbits_database_url:
|
||||
else:
|
||||
if not database_uri.startswith("postgres://"):
|
||||
raise ValueError(
|
||||
"Please use the 'postgres://...' format for the database URL."
|
||||
"Please use the 'postgres://...' " "format for the database URL."
|
||||
)
|
||||
DB_TYPE = POSTGRES
|
||||
|
||||
@@ -560,9 +560,7 @@ class Filters(BaseModel, Generic[TFilterModel]):
|
||||
|
||||
def pagination(self) -> str:
|
||||
stmt = ""
|
||||
if self.limit == 0:
|
||||
self.limit = 1000
|
||||
self.limit = 10 if self.limit is None else self.limit
|
||||
self.limit = self.limit or 10
|
||||
stmt += f"LIMIT {min(1000, self.limit)} "
|
||||
if self.offset:
|
||||
stmt += f"OFFSET {self.offset}"
|
||||
|
||||
+14
-1
@@ -14,6 +14,7 @@ from lnbits.core.crud import (
|
||||
get_account,
|
||||
get_account_by_email,
|
||||
get_account_by_username,
|
||||
get_installed_extension,
|
||||
get_user_active_extensions_ids,
|
||||
get_user_from_account,
|
||||
get_wallet_for_key,
|
||||
@@ -424,7 +425,19 @@ async def check_user_extension_access(
|
||||
Check if the user has access to a particular extension.
|
||||
Raises HTTP Forbidden if the user is not allowed.
|
||||
"""
|
||||
if settings.is_admin_extension(ext_id) and not settings.is_admin_user(user_id):
|
||||
ext = await get_installed_extension(ext_id, conn=conn)
|
||||
is_admin_only = settings.is_admin_extension(ext_id) or bool(
|
||||
ext and ext.meta and ext.meta.admin_only
|
||||
)
|
||||
is_super_user_only = bool(ext and ext.meta and ext.meta.super_user_only)
|
||||
|
||||
if is_super_user_only and not settings.is_super_user(user_id):
|
||||
return SimpleStatus(
|
||||
success=False,
|
||||
message=f"User not authorized for extension '{ext_id}'.",
|
||||
)
|
||||
|
||||
if is_admin_only and not settings.is_admin_user(user_id):
|
||||
return SimpleStatus(
|
||||
success=False, message=f"User not authorized for extension '{ext_id}'."
|
||||
)
|
||||
|
||||
+7
-25
@@ -169,7 +169,7 @@ class PayPalWallet(FiatProvider):
|
||||
|
||||
return FiatInvoiceResponse(
|
||||
ok=True,
|
||||
checking_id=order_id,
|
||||
checking_id=f"fiat_paypal_{order_id}",
|
||||
payment_request=approval_url,
|
||||
)
|
||||
except Exception as exc:
|
||||
@@ -285,25 +285,6 @@ class PayPalWallet(FiatProvider):
|
||||
async def get_payment_status(self, checking_id: str) -> FiatPaymentStatus:
|
||||
raise NotImplementedError("PayPal does not support outgoing payments.")
|
||||
|
||||
async def capture_order(self, checking_id: str) -> FiatPaymentStatus:
|
||||
try:
|
||||
await self._ensure_access_token()
|
||||
paypal_id = self._normalize_paypal_id(checking_id)
|
||||
if paypal_id.startswith("subscription_"):
|
||||
logger.warning("PayPal subscriptions do not support order capture.")
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
r = await self.client.post(
|
||||
f"/v2/checkout/orders/{paypal_id}/capture",
|
||||
json={},
|
||||
headers=self._auth_headers(),
|
||||
)
|
||||
r.raise_for_status()
|
||||
return self._status_from_order(r.json())
|
||||
except Exception as exc:
|
||||
logger.warning(f"Error capturing PayPal order '{checking_id}': {exc}")
|
||||
return await self.get_invoice_status(checking_id)
|
||||
|
||||
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
|
||||
logger.warning(
|
||||
"PayPal does not support paid invoices stream. Use webhooks instead."
|
||||
@@ -315,7 +296,7 @@ class PayPalWallet(FiatProvider):
|
||||
|
||||
def _status_from_order(self, order: dict[str, Any]) -> FiatPaymentStatus:
|
||||
status = (order.get("status") or "").upper()
|
||||
if status == "COMPLETED":
|
||||
if status in ["COMPLETED", "APPROVED"]:
|
||||
return FiatPaymentSuccessStatus()
|
||||
if status in ["VOIDED", "CANCELLED", "CANCELED"]:
|
||||
return FiatPaymentFailedStatus()
|
||||
@@ -330,10 +311,11 @@ class PayPalWallet(FiatProvider):
|
||||
return FiatPaymentPendingStatus()
|
||||
|
||||
def _normalize_paypal_id(self, checking_id: str) -> str:
|
||||
normalized = checking_id
|
||||
while normalized.startswith("fiat_paypal_"):
|
||||
normalized = normalized.replace("fiat_paypal_", "", 1)
|
||||
return normalized
|
||||
return (
|
||||
checking_id.replace("fiat_paypal_", "", 1)
|
||||
if checking_id.startswith("fiat_paypal_")
|
||||
else checking_id
|
||||
)
|
||||
|
||||
def _serialize_metadata(
|
||||
self, payment_options: FiatSubscriptionPaymentOptions
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
||||
from loguru import logger
|
||||
from pyinstrument import Profiler
|
||||
from slowapi import _rate_limit_exceeded_handler
|
||||
from slowapi.errors import RateLimitExceeded
|
||||
from slowapi.middleware import SlowAPIMiddleware
|
||||
@@ -247,16 +246,3 @@ def add_first_install_middleware(app: FastAPI):
|
||||
):
|
||||
return RedirectResponse("/first_install")
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
def add_profiler_middleware(app: FastAPI):
|
||||
@app.middleware("http")
|
||||
async def profile_middleware(request: Request, call_next):
|
||||
profiling = request.query_params.get("profiler", False)
|
||||
if profiling:
|
||||
profiler = Profiler(async_mode="enabled")
|
||||
profiler.start()
|
||||
_ = await call_next(request)
|
||||
profiler.stop()
|
||||
return HTMLResponse(profiler.output_html())
|
||||
return await call_next(request)
|
||||
|
||||
+1
-2
@@ -284,7 +284,7 @@ class ThemesSettings(LNbitsSettings):
|
||||
lnbits_custom_image: str | None = Field(default="/static/images/logos/lnbits.svg")
|
||||
lnbits_ad_space_title: str = Field(default="Supported by")
|
||||
lnbits_ad_space: str = Field(
|
||||
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://firefish.io/?ref=lnbits;/static/images/firefish.png;/static/images/firefish.png"
|
||||
default="https://shop.lnbits.com/;/static/images/bitcoin-shop-banner.png;/static/images/bitcoin-shop-banner.png,https://affil.trezor.io/aff_c?offer_id=169&aff_id=33845;/static/images/bitcoin-hardware-wallet.png;/static/images/bitcoin-hardware-wallet.png,https://firefish.io/?ref=lnbits;/static/images/firefish.png;/static/images/firefish.png,https://opensats.org/;/static/images/open-sats.png;/static/images/open-sats.png"
|
||||
) # sneaky sneaky
|
||||
lnbits_ad_space_enabled: bool = Field(default=False)
|
||||
lnbits_allowed_currencies: list[str] = Field(default=[])
|
||||
@@ -1009,7 +1009,6 @@ class EnvSettings(LNbitsSettings):
|
||||
debug: bool = Field(default=False)
|
||||
debug_database: bool = Field(default=False)
|
||||
bundle_assets: bool = Field(default=True)
|
||||
profiler: bool = Field(default=False)
|
||||
# When enabled, auth cookies require HTTPS and SSO will reject insecure HTTP.
|
||||
auth_https_only: bool = Field(default=True)
|
||||
host: str = Field(default="127.0.0.1")
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -191,6 +191,7 @@ window.localisation.br = {
|
||||
only_admins_can_create_extensions:
|
||||
'Apenas contas de administrador podem criar extensões',
|
||||
admin_only: 'Apenas para Administração',
|
||||
super_user_only: 'Apenas para superusuário',
|
||||
make_user_admin: 'Tornar usuário administrador',
|
||||
revoke_admin: 'Revogar Admin',
|
||||
new_version: 'Nova Versão',
|
||||
|
||||
@@ -135,6 +135,7 @@ window.localisation.cn = {
|
||||
all: '全部',
|
||||
only_admins_can_install: '(只有管理员账户可以安装扩展)',
|
||||
admin_only: '仅限管理员',
|
||||
super_user_only: '仅限超级用户',
|
||||
new_version: '新版本',
|
||||
extension_depends_on: '依赖于:',
|
||||
extension_rating_soon: '即将推出评分',
|
||||
|
||||
@@ -140,6 +140,7 @@ window.localisation.cs = {
|
||||
only_admins_can_install:
|
||||
'(Pouze administrátorské účty mohou instalovat rozšíření)',
|
||||
admin_only: 'Pouze pro adminy',
|
||||
super_user_only: 'Pouze pro superuživatele',
|
||||
new_version: 'Nová verze',
|
||||
extension_depends_on: 'Závisí na:',
|
||||
extension_rating_soon: 'Hodnocení brzy dostupné',
|
||||
|
||||
@@ -143,6 +143,7 @@ window.localisation.de = {
|
||||
only_admins_can_install:
|
||||
'(Nur Administratorkonten können Erweiterungen installieren)',
|
||||
admin_only: 'Nur für Admins',
|
||||
super_user_only: 'Nur für Superuser',
|
||||
new_version: 'Neue Version',
|
||||
extension_depends_on: 'Hängt ab von:',
|
||||
extension_rating_soon: 'Bewertungen sind bald verfügbar',
|
||||
|
||||
@@ -189,6 +189,7 @@ window.localisation.en = {
|
||||
only_admins_can_create_extensions:
|
||||
'Only admin accounts can create extensions',
|
||||
admin_only: 'Admin Only',
|
||||
super_user_only: 'Super User Only',
|
||||
make_user_admin: 'Make User Admin',
|
||||
revoke_admin: 'Revoke Admin',
|
||||
new_version: 'New Version',
|
||||
|
||||
@@ -142,6 +142,7 @@ window.localisation.es = {
|
||||
only_admins_can_install:
|
||||
'(Solo las cuentas de administrador pueden instalar extensiones)',
|
||||
admin_only: 'Solo administradores',
|
||||
super_user_only: 'Solo superusuario',
|
||||
new_version: 'Nueva Versión',
|
||||
extension_depends_on: 'Depende de:',
|
||||
extension_rating_soon: 'Calificaciones próximamente',
|
||||
|
||||
@@ -154,6 +154,7 @@ window.localisation.fi = {
|
||||
all: 'Kaikki',
|
||||
only_admins_can_install: '(Vain pääkäyttäjät voivat asentaa laajennuksia)',
|
||||
admin_only: 'Pääkäyttäjille',
|
||||
super_user_only: 'Vain superkäyttäjälle',
|
||||
new_version: 'Uusi versio',
|
||||
extension_depends_on: 'Edellyttää:',
|
||||
extension_rating_soon: 'Arvostelut on tulossa pian',
|
||||
|
||||
@@ -145,6 +145,7 @@ window.localisation.fr = {
|
||||
only_admins_can_install:
|
||||
'Seuls les comptes administrateurs peuvent installer des extensions',
|
||||
admin_only: 'Réservé aux administrateurs',
|
||||
super_user_only: 'Réservé au super utilisateur',
|
||||
new_version: 'Nouvelle version',
|
||||
extension_depends_on: 'Dépend de :',
|
||||
extension_rating_soon: 'Notes des utilisateurs à venir bientôt',
|
||||
|
||||
@@ -142,6 +142,7 @@ window.localisation.it = {
|
||||
only_admins_can_install:
|
||||
'Solo gli account amministratore possono installare estensioni.',
|
||||
admin_only: 'Solo amministratore',
|
||||
super_user_only: 'Solo superutente',
|
||||
new_version: 'Nuova Versione',
|
||||
extension_depends_on: 'Dipende da:',
|
||||
extension_rating_soon: 'Valutazioni in arrivo',
|
||||
|
||||
@@ -137,6 +137,7 @@ window.localisation.jp = {
|
||||
only_admins_can_install:
|
||||
'(管理者アカウントのみが拡張機能をインストールできます)',
|
||||
admin_only: '管理者のみ',
|
||||
super_user_only: 'スーパー ユーザーのみ',
|
||||
new_version: '新しいバージョン',
|
||||
extension_depends_on: '依存先:',
|
||||
extension_rating_soon: '評価は近日公開',
|
||||
|
||||
@@ -139,6 +139,7 @@ window.localisation.kr = {
|
||||
all: '전체',
|
||||
only_admins_can_install: '(관리자 계정만이 확장 기능을 설치할 수 있습니다)',
|
||||
admin_only: '관리자 전용',
|
||||
super_user_only: '슈퍼유저 전용',
|
||||
new_version: '새로운 버전',
|
||||
extension_depends_on: '의존성 존재:',
|
||||
extension_rating_soon: '평점 기능도 곧 구현됩니다',
|
||||
|
||||
@@ -143,6 +143,7 @@ window.localisation.nl = {
|
||||
only_admins_can_install:
|
||||
'Alleen beheerdersaccounts kunnen extensies installeren',
|
||||
admin_only: 'Alleen beheerder',
|
||||
super_user_only: 'Alleen supergebruiker',
|
||||
new_version: 'Nieuwe Versie',
|
||||
extension_depends_on: 'Afhankelijk van:',
|
||||
extension_rating_soon: 'Beoordelingen binnenkort beschikbaar',
|
||||
|
||||
@@ -141,6 +141,7 @@ window.localisation.pi = {
|
||||
all: 'Arr',
|
||||
only_admins_can_install: '(Only admin accounts can install extensions)',
|
||||
admin_only: "Cap'n Only",
|
||||
super_user_only: "Big Cap'n Only",
|
||||
new_version: 'New Version',
|
||||
extension_depends_on: 'Depends on:',
|
||||
extension_rating_soon: "Ratings a'comin' soon",
|
||||
|
||||
@@ -140,6 +140,7 @@ window.localisation.pl = {
|
||||
only_admins_can_install:
|
||||
'Tylko konta administratorów mogą instalować rozszerzenia',
|
||||
admin_only: 'Tylko dla administratora',
|
||||
super_user_only: 'Tylko dla superużytkownika',
|
||||
new_version: 'Nowa wersja',
|
||||
extension_depends_on: 'Zależy od:',
|
||||
extension_rating_soon: 'Oceny będą dostępne wkrótce',
|
||||
|
||||
@@ -141,6 +141,7 @@ window.localisation.pt = {
|
||||
only_admins_can_install:
|
||||
'Apenas contas de administrador podem instalar extensões.',
|
||||
admin_only: 'Apenas para administradores',
|
||||
super_user_only: 'Apenas para superusuário',
|
||||
new_version: 'Nova Versão',
|
||||
extension_depends_on: 'Depende de:',
|
||||
extension_rating_soon: 'Avaliações em breve',
|
||||
|
||||
@@ -138,6 +138,7 @@ window.localisation.sk = {
|
||||
only_admins_can_install:
|
||||
'(Iba administrátorské účty môžu inštalovať rozšírenia)',
|
||||
admin_only: 'Iba pre administrátorov',
|
||||
super_user_only: 'Iba pre superužívateľa',
|
||||
new_version: 'Nová verzia',
|
||||
extension_depends_on: 'Závisí na:',
|
||||
extension_rating_soon: 'Hodnotenia budú čoskoro dostupné',
|
||||
|
||||
@@ -139,6 +139,7 @@ window.localisation.we = {
|
||||
all: 'Pob',
|
||||
only_admins_can_install: 'Dim ond cyfrifon gweinyddwr all osod estyniadau',
|
||||
admin_only: 'Dim ond Gweinyddwr',
|
||||
super_user_only: 'Dim ond Uwchddefnyddiwr',
|
||||
new_version: 'Fersiwn Newydd',
|
||||
extension_depends_on: 'Dibynnu ar:',
|
||||
extension_rating_soon: 'Sgôr yn dod yn fuan',
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -307,7 +307,13 @@
|
||||
:label="$t('disable')"
|
||||
></q-btn>
|
||||
<q-badge
|
||||
v-if="extension.isAdminOnly && !g.user.admin"
|
||||
v-if="extension.isSuperUserOnly && !g.user.super_user"
|
||||
v-text="$t('super_user_only')"
|
||||
>
|
||||
</q-badge>
|
||||
|
||||
<q-badge
|
||||
v-else-if="extension.isAdminOnly && !g.user.admin"
|
||||
v-text="$t('admin_only')"
|
||||
>
|
||||
</q-badge>
|
||||
@@ -316,7 +322,9 @@
|
||||
v-else-if="
|
||||
extension.isInstalled &&
|
||||
extension.isActive &&
|
||||
!g.user.extensions.includes(extension.id)
|
||||
!g.user.extensions.includes(extension.id) &&
|
||||
(!extension.isAdminOnly || g.user.admin) &&
|
||||
(!extension.isSuperUserOnly || g.user.super_user)
|
||||
"
|
||||
flat
|
||||
color="primary"
|
||||
|
||||
Generated
+2
-84
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@@ -3388,88 +3388,6 @@ files = [
|
||||
[package.extras]
|
||||
windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstrument"
|
||||
version = "5.1.2"
|
||||
description = "Call stack profiler for Python. Shows you why your code is slow!"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pyinstrument-5.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f224fe80ba288a00980af298d3808219f9d246fd95b4f91729c9c33a0dc54fe6"},
|
||||
{file = "pyinstrument-5.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7df09fc0d5b72daf48b73cdf07738761bff7f656c81aff686b3ccdd7d2abe236"},
|
||||
{file = "pyinstrument-5.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75a7e17377d4405666bbaf126b1fd7bbb7e206d7246e6db3d62864d3d4790ae3"},
|
||||
{file = "pyinstrument-5.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5381cc6583d26e04d9298acded4242f4fe71986f1472c8aee6992c6816f0cac5"},
|
||||
{file = "pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ec08a530bef8d3492d31d8b0b12d0cfde09539f2a1c4b9678662ebc3c843e478"},
|
||||
{file = "pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d671168508129b472be570bc9aee361190ba917b997c703bd134bb4de445ce7"},
|
||||
{file = "pyinstrument-5.1.2-cp310-cp310-win32.whl", hash = "sha256:5957a94f84564b374a7f856d1b322345d600964280b0d687b8ddcc483f21e576"},
|
||||
{file = "pyinstrument-5.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:38a2180a7801c51610b50e5d423674b21872efd019ccf05a11b7f9016cb1dcfc"},
|
||||
{file = "pyinstrument-5.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3739a05583ea6312c385eb59fe985cd20d9048e95f9eeeb6a2f6c35202e2d36e"},
|
||||
{file = "pyinstrument-5.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c9ee05dc75ac5fb18498c311e624f77f7f321f7ff325b251aa09e52e46f1d6a"},
|
||||
{file = "pyinstrument-5.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a49a55ca5b75218767e29cacbe515d0b66fc18cb48a937bca0f77b8dafc7202"},
|
||||
{file = "pyinstrument-5.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c45c14974ff04b1bfdc6c2a448627c6da7409c7800d0eb7bd03fb435dcb41d7"},
|
||||
{file = "pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22b9c04b3982c41c04b1c5ed05d1bc3a2ba26533450084058119f6dc160e70a3"},
|
||||
{file = "pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5c4995ee0774801790c138f0dfec17d4e7a7ef09a6d56d53cbcbf0578a711021"},
|
||||
{file = "pyinstrument-5.1.2-cp311-cp311-win32.whl", hash = "sha256:fe449e4a8ee60a2a27cf509350a584670f4c3704649601be7937598f09dbe7ca"},
|
||||
{file = "pyinstrument-5.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:3fb839429671a42bf349335af4c1ce5cf83386ac11f04df0bc40720d4cb7d77d"},
|
||||
{file = "pyinstrument-5.1.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2519865d4bf58936f2506c1c46a82d29a20f3239aa50c941df1ca9618c7da5f0"},
|
||||
{file = "pyinstrument-5.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:059442106b8b5de29ae5ac1bdc20d044fed4da534b8caba434b6ffb119037bf5"},
|
||||
{file = "pyinstrument-5.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd51f2d54fc39a4cfd73ba6be27cd0187123132ce3f445b639bff5e1b23d7e26"},
|
||||
{file = "pyinstrument-5.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12af1e83795b6c640d657d339014dd1ff718b182dec736d7d1f1d8a97534eb53"},
|
||||
{file = "pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2565513658e742c5eb691a779cb29d19d01bc9ee951d0eb76482e9f343c38c2e"},
|
||||
{file = "pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5afd0ba788a1d112da49fb77966918e01df1f9e7d62e72894d82f7acb0996c2d"},
|
||||
{file = "pyinstrument-5.1.2-cp312-cp312-win32.whl", hash = "sha256:554077b031b278593cb2301f0057be771ea62a729878c69aaf29fcdfb7b71281"},
|
||||
{file = "pyinstrument-5.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:55a905384ba43efc924b8863aa6cfd276f029e4aa70c4a0e3b7389e27b191e45"},
|
||||
{file = "pyinstrument-5.1.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7b8bab2334bf1d4c9e92d61db574300b914b594588a6b6dd67c45450152dfc29"},
|
||||
{file = "pyinstrument-5.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:13dcc138a61298ef4994b7aebff509d2c06db89dfd6e2021f0b9cd96aaa44ec3"},
|
||||
{file = "pyinstrument-5.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8abd4a7ffa2e7f9e00039a5e549e8eebc80d7ca8d43f0fb51a50ff2b117ce4a"},
|
||||
{file = "pyinstrument-5.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb3a05108edebc30f31e2c69c904576042f1158b2513ab80adc08f7848a7a8f0"},
|
||||
{file = "pyinstrument-5.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f70d588b53f3f35829d1d1ddfa05e07fcebf1434b3b1509d542ca317d8e9a2a5"},
|
||||
{file = "pyinstrument-5.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b007327e0d6a6a01d5064883dd27c19996f044ce7488d507826fee7884e6a32e"},
|
||||
{file = "pyinstrument-5.1.2-cp313-cp313-win32.whl", hash = "sha256:9ba0e6b17a7e86c3dc02d208e4c25506e8f914d9964ae89449f1f37f0b70abc0"},
|
||||
{file = "pyinstrument-5.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:660d7fc486a839814db0b2f716bc13d8b99b9c780aaeb47f74a70a34adc02a7b"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0baed297beee2bb9897e737bbd89e3b9d45a2fbbea9f1ad4e809007d780a9b1e"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ebb910a32a45bde6c3fc30c578efc28a54517990e11e94b5e48a0d5479728568"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bad403c157f9c6dba7f731a6fca5bfcd8ca2701a39bcc717dcc6e0b10055ffc4"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f456cabdb95fd343c798a7f2a56688b028f981522e283c5f59bd59195b66df5"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e9c4dcc1f2c4a0cd6b576e3604abc37496a7868243c9a1443ad3b9db69d590f"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:acf93b128328c6d80fdb85431068ac17508f0f7845e89505b0ea6130dead5ca6"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314-win32.whl", hash = "sha256:9c7f0167903ecff8b1d744f7e37b2bd4918e05a69cca724cb112f5ed59d1e41b"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:ce3f6b1f9a2b5d74819ecc07d631eadececf915f551474a75ad65ac580ec5a0e"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:af8651b239049accbeecd389d35823233f649446f76f47fd005316b05d08cef2"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c6082f1c3e43e1d22834e91ba8975f0080186df4018a04b4dd29f9623c59df1d"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c031eb066ddc16425e1e2f56aad5c1ce1e27b2432a70329e5385b85e812decee"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f447ec391cad30667ba412dce41607aaa20d4a2496a7ab867e0c199f0fe3ae3d"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:50299bddfc1fe0039898f895b10ef12f9db08acffb4d85326fad589cda24d2ee"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a193ff08825ece115ececa136832acb14c491c77ab1e6b6a361905df8753d5c6"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314t-win32.whl", hash = "sha256:de887ba19e1057bd2d86e6584f17788516a890ae6fe1b7eed9927873f416b4d8"},
|
||||
{file = "pyinstrument-5.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:b6a71f5e7f53c86c9b476b30cf19509463a63581ef17ddbd8680fee37ae509db"},
|
||||
{file = "pyinstrument-5.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:47f14f248108f1202d48f34903bddc053a47c62ce46908aee848c1f667a1925b"},
|
||||
{file = "pyinstrument-5.1.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc3f2688981af764fa2c5f5a00d7040c0c12771ca5a026730f2d826f7a28d277"},
|
||||
{file = "pyinstrument-5.1.2-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7afb24d11d24fb1762059240ed97a1a4607779d5f221f91d62b67ae089bd506d"},
|
||||
{file = "pyinstrument-5.1.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d21221a29718d5dcdc1453dbbbdd100734525672ef1e34ff03d49bc5d688ca4"},
|
||||
{file = "pyinstrument-5.1.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3b68aa9f0d5217c67370762c99a80750ce56f66e5e9281c31b5baa3e4b88894d"},
|
||||
{file = "pyinstrument-5.1.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:755702f01800934c3bec3c0d30483f97b6ff1fc1d2afcf6d816584703c9f1235"},
|
||||
{file = "pyinstrument-5.1.2-cp38-cp38-win32.whl", hash = "sha256:2bacb980c95d4c9ea6a253e5ccf3e99993082de29ff7e7fc397e9484355577b1"},
|
||||
{file = "pyinstrument-5.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:2588bc34c25d50f29d3a117c7dfac06513ee59c507b65081e66ca9569bcf45e0"},
|
||||
{file = "pyinstrument-5.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bea0687665c181c6e62677fb560739a473c4286816582a43f8eb0aa6094ed529"},
|
||||
{file = "pyinstrument-5.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:64246d2bd475870b62ed5df4808bfb33328135e8dfbdff823f9cb7d1358eb40b"},
|
||||
{file = "pyinstrument-5.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff6fd3c7907e57f082cdd405bec1b34768c1810a165538299d259ee1bff5d7b6"},
|
||||
{file = "pyinstrument-5.1.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af09af38ee8407ca273407e24a8e6470d2444561d01005ee6a8db5f2fd908c08"},
|
||||
{file = "pyinstrument-5.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1249d2799cdd57151b4444167e5c7736e2c9b5e79bd781b0779d631338509553"},
|
||||
{file = "pyinstrument-5.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d653206f50260f20bc78339c3d7aa0f19f8cf9c9f71939fbf02e2ea30353487"},
|
||||
{file = "pyinstrument-5.1.2-cp39-cp39-win32.whl", hash = "sha256:d0b0c6e289725f14d0ff73f8190c953bdcb98f21c5c29c3eafb0dca8025583cb"},
|
||||
{file = "pyinstrument-5.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:db8243e602aca43dc7ce8e40ed7d0ca4820d024c3c03824870c5a9e98f84e953"},
|
||||
{file = "pyinstrument-5.1.2.tar.gz", hash = "sha256:af149d672da9493fa37334a1cc68f7b80c3e6cb9fd99b9e426c447db5c650bf0"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
bin = ["click", "nox"]
|
||||
docs = ["furo (==2024.7.18)", "myst-parser (==3.0.1)", "sphinx (==7.4.7)", "sphinx-autobuild (==2024.4.16)", "sphinxcontrib-programoutput (==0.17)"]
|
||||
examples = ["django", "litestar", "numpy"]
|
||||
test = ["cffi (>=1.17.0)", "flaky", "greenlet (>=3)", "ipython", "pytest", "pytest-asyncio (==0.23.8)", "trio"]
|
||||
types = ["typing_extensions"]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.12.1"
|
||||
@@ -5110,4 +5028,4 @@ migration = ["psycopg2-binary"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10,<3.13"
|
||||
content-hash = "0879a6230bbc0d0e1d8d7d090e1572ba863078b18e0d78478baf2100aa245e08"
|
||||
content-hash = "42751c05e1fa8250ff90981def5e0ce8d1b284eeb2acc2c7ac9ae5847cf99e17"
|
||||
|
||||
+1
-2
@@ -82,8 +82,7 @@ dev = [
|
||||
"pytest-mock~=3.15.1",
|
||||
"types-mock~=5.2.0.20250924",
|
||||
"mock~=5.2.0",
|
||||
"grpcio-tools~=1.76.0",
|
||||
"pyinstrument>=5.1.2",
|
||||
"grpcio-tools~=1.76.0"
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
|
||||
@@ -80,19 +80,10 @@ async def test_callback_api_handles_paid_events_with_real_payments(mocker):
|
||||
},
|
||||
}
|
||||
)
|
||||
await handle_paypal_event(
|
||||
{
|
||||
"id": "evt_paypal_approved",
|
||||
"event_type": "CHECKOUT.ORDER.APPROVED",
|
||||
"resource": {
|
||||
"purchase_units": [{"invoice_id": payment.payment_hash}],
|
||||
},
|
||||
}
|
||||
)
|
||||
await handle_paypal_event(
|
||||
{
|
||||
"id": "evt_paypal",
|
||||
"event_type": "PAYMENT.CAPTURE.COMPLETED",
|
||||
"event_type": "CHECKOUT.ORDER.APPROVED",
|
||||
"resource": {
|
||||
"purchase_units": [{"invoice_id": payment.payment_hash}],
|
||||
},
|
||||
|
||||
@@ -20,7 +20,9 @@ from lnbits.core.models.extensions import (
|
||||
Extension,
|
||||
ExtensionConfig,
|
||||
ExtensionRelease,
|
||||
GitHubRelease,
|
||||
InstallableExtension,
|
||||
Manifest,
|
||||
PayToEnableInfo,
|
||||
ReleasePaymentInfo,
|
||||
UserExtensionInfo,
|
||||
@@ -152,6 +154,8 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
|
||||
short_description="Config",
|
||||
min_lnbits_version="0.1.0",
|
||||
max_lnbits_version=None,
|
||||
admin_only=True,
|
||||
super_user_only=True,
|
||||
)
|
||||
mocker.patch.object(
|
||||
ExtensionConfig,
|
||||
@@ -160,6 +164,8 @@ async def test_extension_api_install_details_and_release_endpoints(mocker):
|
||||
)
|
||||
release_info = await get_extension_release("org", ext_id, "v1.0.0")
|
||||
assert release_info["is_version_compatible"] is True
|
||||
assert release_info["admin_only"] is True
|
||||
assert release_info["super_user_only"] is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -258,6 +264,8 @@ async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user)
|
||||
catalog_entry = make_installable_extension(
|
||||
ext_id,
|
||||
pay_to_enable=PayToEnableInfo(required=True, amount=21, wallet=admin_wallet.id),
|
||||
admin_only=True,
|
||||
super_user_only=True,
|
||||
)
|
||||
mocker.patch.object(
|
||||
InstallableExtension,
|
||||
@@ -267,6 +275,8 @@ async def test_extension_api_pay_to_enable_and_catalog_views(mocker, admin_user)
|
||||
catalog = await extensions(AccountId(id=regular_user.id))
|
||||
catalog_item = next(item for item in catalog if item["id"] == ext_id)
|
||||
assert catalog_item["payToEnable"]["wallet"] is None
|
||||
assert catalog_item["isAdminOnly"] is True
|
||||
assert catalog_item["isSuperUserOnly"] is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -428,3 +438,64 @@ async def test_extension_api_review_endpoints(mocker):
|
||||
CreateExtensionReview(tag=ext_id, name="Alice", rating=900, comment="Great")
|
||||
)
|
||||
assert payment_request.payment_hash.startswith("hash_")
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_extension_api_enable_rejects_admin_and_super_only_extensions(
|
||||
admin_user,
|
||||
):
|
||||
regular_user = await create_user_account(
|
||||
Account(
|
||||
id=uuid4().hex,
|
||||
username=f"user_{uuid4().hex[:8]}",
|
||||
email=f"user_{uuid4().hex[:8]}@lnbits.com",
|
||||
)
|
||||
)
|
||||
admin_only_ext = f"admin_{uuid4().hex[:8]}"
|
||||
super_only_ext = f"super_{uuid4().hex[:8]}"
|
||||
|
||||
await create_installed_extension(
|
||||
make_installable_extension(admin_only_ext, admin_only=True)
|
||||
)
|
||||
await create_installed_extension(
|
||||
make_installable_extension(super_only_ext, super_user_only=True)
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException, match="User not authorized"):
|
||||
await api_enable_extension(admin_only_ext, AccountId(id=regular_user.id))
|
||||
|
||||
with pytest.raises(HTTPException, match="User not authorized"):
|
||||
await api_enable_extension(super_only_ext, AccountId(id=admin_user.id))
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_repo_manifest_flags_apply_to_repo_releases(mocker):
|
||||
ext_id = f"repo_{uuid4().hex[:8]}"
|
||||
release = make_extension_release(ext_id)
|
||||
manifest = Manifest(
|
||||
repos=[
|
||||
GitHubRelease(
|
||||
id=ext_id,
|
||||
organisation="lnbits",
|
||||
repository="tunnel_me_out",
|
||||
admin_only=True,
|
||||
super_user_only=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
mocker.patch.object(
|
||||
InstallableExtension,
|
||||
"fetch_manifest",
|
||||
mocker.AsyncMock(return_value=manifest),
|
||||
)
|
||||
mocker.patch.object(
|
||||
ExtensionRelease,
|
||||
"get_github_releases",
|
||||
mocker.AsyncMock(return_value=[release]),
|
||||
)
|
||||
|
||||
releases = await InstallableExtension.get_extension_releases(ext_id)
|
||||
|
||||
assert len(releases) == 1
|
||||
assert releases[0].admin_only is True
|
||||
assert releases[0].super_user_only is True
|
||||
|
||||
@@ -141,9 +141,13 @@ def make_installable_extension(
|
||||
pay_to_enable: PayToEnableInfo | None = None,
|
||||
dependencies: list[str] | None = None,
|
||||
payments: list[ReleasePaymentInfo] | None = None,
|
||||
admin_only: bool = False,
|
||||
super_user_only: bool = False,
|
||||
) -> InstallableExtension:
|
||||
release = make_extension_release(ext_id, version)
|
||||
release.is_version_compatible = compatible
|
||||
release.admin_only = admin_only
|
||||
release.super_user_only = super_user_only
|
||||
return InstallableExtension(
|
||||
id=ext_id,
|
||||
name=f"Extension {ext_id}",
|
||||
@@ -156,6 +160,8 @@ def make_installable_extension(
|
||||
pay_to_enable=pay_to_enable,
|
||||
dependencies=dependencies or [],
|
||||
payments=payments or [],
|
||||
admin_only=admin_only,
|
||||
super_user_only=super_user_only,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,32 +1,7 @@
|
||||
import pytest
|
||||
|
||||
from lnbits.db import Filters
|
||||
from tests.helpers import DbTestModel
|
||||
|
||||
TEST_DB_FETCH_PAGE_ROWS: tuple[dict[str, str], ...] = (
|
||||
{"id": "1", "name": "Alice", "value": "foo"},
|
||||
{"id": "2", "name": "Bob", "value": "bar"},
|
||||
{"id": "3", "name": "Carol", "value": "bar"},
|
||||
{"id": "4", "name": "Dave", "value": "bar"},
|
||||
{"id": "5", "name": "Dave", "value": "foo"},
|
||||
{"id": "6", "name": "Eve", "value": "foo"},
|
||||
{"id": "7", "name": "Frank", "value": "bar"},
|
||||
{"id": "8", "name": "Grace", "value": "foo"},
|
||||
{"id": "9", "name": "Heidi", "value": "bar"},
|
||||
{"id": "10", "name": "Ivan", "value": "foo"},
|
||||
{"id": "11", "name": "Judy", "value": "bar"},
|
||||
{"id": "12", "name": "Mallory", "value": "foo"},
|
||||
{"id": "13", "name": "Niaj", "value": "bar"},
|
||||
{"id": "14", "name": "Olivia", "value": "foo"},
|
||||
{"id": "15", "name": "Peggy", "value": "bar"},
|
||||
{"id": "16", "name": "Rupert", "value": "foo"},
|
||||
{"id": "17", "name": "Sybil", "value": "bar"},
|
||||
{"id": "18", "name": "Trent", "value": "foo"},
|
||||
{"id": "19", "name": "Victor", "value": "bar"},
|
||||
{"id": "20", "name": "Walter", "value": "foo"},
|
||||
{"id": "21", "name": "Zoe", "value": "bar"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def fetch_page(db):
|
||||
@@ -38,14 +13,14 @@ async def fetch_page(db):
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
for row in TEST_DB_FETCH_PAGE_ROWS:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO test_db_fetch_page (id, name, value)
|
||||
VALUES (:id, :name, :value)
|
||||
""",
|
||||
row,
|
||||
)
|
||||
await db.execute("""
|
||||
INSERT INTO test_db_fetch_page (id, name, value) VALUES
|
||||
('1', 'Alice', 'foo'),
|
||||
('2', 'Bob', 'bar'),
|
||||
('3', 'Carol', 'bar'),
|
||||
('4', 'Dave', 'bar'),
|
||||
('5', 'Dave', 'foo')
|
||||
""")
|
||||
yield
|
||||
await db.execute("DROP TABLE test_db_fetch_page")
|
||||
|
||||
@@ -58,35 +33,8 @@ async def test_db_fetch_page_simple(fetch_page, db):
|
||||
)
|
||||
|
||||
assert row
|
||||
assert row.total == len(TEST_DB_FETCH_PAGE_ROWS)
|
||||
assert len(row.data) == Filters().limit
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_db_fetch_page_limit_zero_returns_all(fetch_page, db):
|
||||
row = await db.fetch_page(
|
||||
query="select * from test_db_fetch_page",
|
||||
filters=Filters(limit=0),
|
||||
model=DbTestModel,
|
||||
)
|
||||
|
||||
assert row
|
||||
assert row.total == len(TEST_DB_FETCH_PAGE_ROWS)
|
||||
assert len(row.data) == len(TEST_DB_FETCH_PAGE_ROWS)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_db_fetch_page_limit(fetch_page, db):
|
||||
limit = 5
|
||||
row = await db.fetch_page(
|
||||
query="select * from test_db_fetch_page",
|
||||
filters=Filters(limit=limit),
|
||||
model=DbTestModel,
|
||||
)
|
||||
|
||||
assert row
|
||||
assert row.total == len(TEST_DB_FETCH_PAGE_ROWS)
|
||||
assert len(row.data) == limit
|
||||
assert row.total == 5
|
||||
assert len(row.data) == 5
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -97,7 +45,7 @@ async def test_db_fetch_page_group_by(fetch_page, db):
|
||||
group_by=["name"],
|
||||
)
|
||||
assert row
|
||||
assert row.total == len({test_row["name"] for test_row in TEST_DB_FETCH_PAGE_ROWS})
|
||||
assert row.total == 4
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -108,9 +56,7 @@ async def test_db_fetch_page_group_by_multiple(fetch_page, db):
|
||||
group_by=["value", "name"],
|
||||
)
|
||||
assert row
|
||||
assert row.total == len(
|
||||
{(test_row["value"], test_row["name"]) for test_row in TEST_DB_FETCH_PAGE_ROWS}
|
||||
)
|
||||
assert row.total == 5
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi.exceptions import HTTPException
|
||||
from httpx import AsyncClient
|
||||
from pydantic.types import UUID4
|
||||
|
||||
from lnbits.core.crud.extensions import create_installed_extension
|
||||
from lnbits.core.crud.users import delete_account
|
||||
from lnbits.core.models import User
|
||||
from lnbits.core.models.users import AccessTokenPayload
|
||||
@@ -18,10 +19,12 @@ from lnbits.decorators import (
|
||||
check_extension_builder,
|
||||
check_first_install,
|
||||
check_user_exists,
|
||||
check_user_extension_access,
|
||||
optional_user_id,
|
||||
)
|
||||
from lnbits.helpers import create_access_token
|
||||
from lnbits.settings import AuthMethods, Settings, settings
|
||||
from tests.helpers import make_installable_extension
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -225,3 +228,37 @@ async def test_check_extension_builder_requires_admin_when_disabled_for_users(
|
||||
admin_user = user_alan.copy(deep=True)
|
||||
admin_user.admin = True
|
||||
await check_extension_builder(admin_user)
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_check_user_extension_access_honors_extension_metadata(
|
||||
settings: Settings, user_alan: User, admin_user: User
|
||||
):
|
||||
admin_only_ext = f"admin_{uuid4().hex[:8]}"
|
||||
super_only_ext = f"super_{uuid4().hex[:8]}"
|
||||
|
||||
await create_installed_extension(
|
||||
make_installable_extension(admin_only_ext, admin_only=True)
|
||||
)
|
||||
await create_installed_extension(
|
||||
make_installable_extension(super_only_ext, super_user_only=True)
|
||||
)
|
||||
|
||||
regular_status = await check_user_extension_access(user_alan.id, admin_only_ext)
|
||||
assert regular_status.success is False
|
||||
|
||||
admin_status = await check_user_extension_access(admin_user.id, admin_only_ext)
|
||||
assert admin_status.success is True
|
||||
|
||||
admin_super_status = await check_user_extension_access(
|
||||
admin_user.id, super_only_ext
|
||||
)
|
||||
assert admin_super_status.success is False
|
||||
|
||||
previous_super_user = settings.super_user
|
||||
settings.super_user = admin_user.id
|
||||
try:
|
||||
super_status = await check_user_extension_access(admin_user.id, super_only_ext)
|
||||
assert super_status.success is True
|
||||
finally:
|
||||
settings.super_user = previous_super_user
|
||||
|
||||
@@ -23,14 +23,7 @@ from lnbits.core.services.fiat_providers import (
|
||||
test_connection as fiat_provider_connection,
|
||||
)
|
||||
from lnbits.core.services.users import create_user_account
|
||||
from lnbits.core.views.callback_api import handle_paypal_event
|
||||
from lnbits.fiat.paypal import PayPalWallet
|
||||
from lnbits.fiat.base import (
|
||||
FiatInvoiceResponse,
|
||||
FiatPaymentStatus,
|
||||
FiatPaymentSuccessStatus,
|
||||
FiatStatusResponse,
|
||||
)
|
||||
from lnbits.fiat.base import FiatInvoiceResponse, FiatPaymentStatus, FiatStatusResponse
|
||||
from lnbits.settings import Settings
|
||||
from tests.helpers import get_random_string
|
||||
|
||||
@@ -287,42 +280,6 @@ async def test_create_wallet_fiat_invoice_success(
|
||||
assert status.success is True
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_create_paypal_fiat_invoice_uses_raw_order_id(
|
||||
to_wallet: Wallet, settings: Settings, mocker: MockerFixture
|
||||
):
|
||||
settings.paypal_enabled = True
|
||||
settings.paypal_client_id = "client-id"
|
||||
settings.paypal_client_secret = "client-secret"
|
||||
settings.paypal_limits.service_min_amount_sats = 0
|
||||
settings.paypal_limits.service_max_amount_sats = 0
|
||||
settings.paypal_limits.service_faucet_wallet_id = None
|
||||
|
||||
invoice_data = CreateInvoice(
|
||||
unit="USD", amount=1.0, memo="Test", fiat_provider="paypal"
|
||||
)
|
||||
fiat_mock_response = FiatInvoiceResponse(
|
||||
ok=True,
|
||||
checking_id="ORDER123",
|
||||
payment_request="https://paypal.com/checkoutnow?token=ORDER123",
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
"lnbits.fiat.PayPalWallet.create_invoice",
|
||||
AsyncMock(return_value=fiat_mock_response),
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.utils.exchange_rates.get_fiat_rate_satoshis",
|
||||
AsyncMock(return_value=1000),
|
||||
)
|
||||
|
||||
payment = await payments.create_fiat_invoice(to_wallet.id, invoice_data)
|
||||
|
||||
assert payment.fiat_provider == "paypal"
|
||||
assert payment.extra.get("fiat_checking_id") == "ORDER123"
|
||||
assert payment.checking_id == "fiat_paypal_ORDER123"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_fiat_service_fee(settings: Settings):
|
||||
# settings.stripe_limits.service_min_amount_sats = 0
|
||||
@@ -680,67 +637,6 @@ async def test_verify_paypal_webhook_raises_on_failed_verification(
|
||||
)
|
||||
|
||||
|
||||
def test_paypal_order_status_approved_is_pending():
|
||||
wallet = object.__new__(PayPalWallet)
|
||||
|
||||
approved = wallet._status_from_order({"status": "APPROVED"})
|
||||
completed = wallet._status_from_order({"status": "COMPLETED"})
|
||||
|
||||
assert approved.pending is True
|
||||
assert approved.success is False
|
||||
assert completed.success is True
|
||||
|
||||
|
||||
def test_paypal_normalize_id_removes_legacy_double_prefix():
|
||||
wallet = object.__new__(PayPalWallet)
|
||||
|
||||
assert wallet._normalize_paypal_id("fiat_paypal_ORDER123") == "ORDER123"
|
||||
assert wallet._normalize_paypal_id("fiat_paypal_fiat_paypal_ORDER123") == "ORDER123"
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_handle_paypal_approved_event_captures_order(mocker: MockerFixture):
|
||||
payment = Payment(
|
||||
checking_id="fiat_paypal_ORDER123",
|
||||
payment_hash="hash_123",
|
||||
wallet_id="wallet_id",
|
||||
amount=1000,
|
||||
fee=0,
|
||||
bolt11="bolt11",
|
||||
status=PaymentState.PENDING,
|
||||
fiat_provider="paypal",
|
||||
extra={"fiat_checking_id": "ORDER123"},
|
||||
)
|
||||
provider = mocker.Mock(spec=PayPalWallet)
|
||||
provider.capture_order = AsyncMock(return_value=FiatPaymentSuccessStatus())
|
||||
|
||||
mocker.patch(
|
||||
"lnbits.core.views.callback_api.get_standalone_payment",
|
||||
AsyncMock(return_value=payment),
|
||||
)
|
||||
mocker.patch(
|
||||
"lnbits.core.views.callback_api.get_fiat_provider",
|
||||
AsyncMock(return_value=provider),
|
||||
)
|
||||
status_mock = mocker.patch(
|
||||
"lnbits.core.views.callback_api.check_fiat_status",
|
||||
AsyncMock(),
|
||||
)
|
||||
|
||||
await handle_paypal_event(
|
||||
{
|
||||
"id": "evt_paypal_approved",
|
||||
"event_type": "CHECKOUT.ORDER.APPROVED",
|
||||
"resource": {
|
||||
"purchase_units": [{"invoice_id": payment.payment_hash}],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
provider.capture_order.assert_awaited_once_with("ORDER123")
|
||||
status_mock.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_test_connection_reports_provider_status(mocker: MockerFixture):
|
||||
mocker.patch(
|
||||
|
||||
@@ -1334,7 +1334,6 @@ dev = [
|
||||
{ name = "openai" },
|
||||
{ name = "openapi-spec-validator" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pyinstrument" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-cov" },
|
||||
{ name = "pytest-httpserver" },
|
||||
@@ -1409,7 +1408,6 @@ dev = [
|
||||
{ name = "openai", specifier = "~=2.14.0" },
|
||||
{ name = "openapi-spec-validator", specifier = "~=0.7.2" },
|
||||
{ name = "pre-commit", specifier = "~=4.5.1" },
|
||||
{ name = "pyinstrument", specifier = ">=5.1.2" },
|
||||
{ name = "pytest", specifier = "~=9.0.2" },
|
||||
{ name = "pytest-cov", specifier = "~=7.0.0" },
|
||||
{ name = "pytest-httpserver", specifier = "~=1.1.3" },
|
||||
@@ -2031,38 +2029,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstrument"
|
||||
version = "5.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/32/7f/d3c4ef7c43f3294bd5a475dfa6f295a9fee5243c292d5c8122044fa83bcb/pyinstrument-5.1.2.tar.gz", hash = "sha256:af149d672da9493fa37334a1cc68f7b80c3e6cb9fd99b9e426c447db5c650bf0", size = 266889, upload-time = "2026-01-04T18:38:58.464Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/74/c66e1bf3565600d78f53195efb6f8fd31610f85a58aa3fee39c56bf71d1b/pyinstrument-5.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f224fe80ba288a00980af298d3808219f9d246fd95b4f91729c9c33a0dc54fe6", size = 131470, upload-time = "2026-01-04T18:37:22.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/6b/606c5bfa311b5be74f58ef505c678216dda2be3b76a2ac770c2b0fccff77/pyinstrument-5.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7df09fc0d5b72daf48b73cdf07738761bff7f656c81aff686b3ccdd7d2abe236", size = 124567, upload-time = "2026-01-04T18:37:24.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/70/c8a88defb77873513971f590549c48ceb70f7ef10f30a689762ef36dd877/pyinstrument-5.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:75a7e17377d4405666bbaf126b1fd7bbb7e206d7246e6db3d62864d3d4790ae3", size = 149205, upload-time = "2026-01-04T18:37:25.696Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/4b/0e64fefb939af472c3fbc63ab45224766447bde73f51579f3ecc335b0a49/pyinstrument-5.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5381cc6583d26e04d9298acded4242f4fe71986f1472c8aee6992c6816f0cac5", size = 147900, upload-time = "2026-01-04T18:37:27.343Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/6e/b4209711c61176acfeb6c351e9f88a37ed3d3bc3b749c374c0a655ee8f50/pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ec08a530bef8d3492d31d8b0b12d0cfde09539f2a1c4b9678662ebc3c843e478", size = 148133, upload-time = "2026-01-04T18:37:29.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/28/f323b70789833baf0628af7b9f797b8c1a13b695bd8aa582b1312f14b602/pyinstrument-5.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d671168508129b472be570bc9aee361190ba917b997c703bd134bb4de445ce7", size = 147652, upload-time = "2026-01-04T18:37:30.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/cd/9b0af0307a3a2cffb48ca76275c50b8bec3f85ca6e7b996e2e6cfbda1207/pyinstrument-5.1.2-cp310-cp310-win32.whl", hash = "sha256:5957a94f84564b374a7f856d1b322345d600964280b0d687b8ddcc483f21e576", size = 125793, upload-time = "2026-01-04T18:37:31.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/89/fe4c650c252aefb8064bfdff6c0a020d33d15c55dc22abfa1f352dcc2dd1/pyinstrument-5.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:38a2180a7801c51610b50e5d423674b21872efd019ccf05a11b7f9016cb1dcfc", size = 126679, upload-time = "2026-01-04T18:37:33.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/ef/0288edd620fb0cf2074d8c8e3567007a6bac66307b839d99988563de4eb8/pyinstrument-5.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3739a05583ea6312c385eb59fe985cd20d9048e95f9eeeb6a2f6c35202e2d36e", size = 131284, upload-time = "2026-01-04T18:37:35.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/4e/2a90a6997d9f7a39a6998d56de72e52673ebf5a9169a1c39dbf173e95105/pyinstrument-5.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c9ee05dc75ac5fb18498c311e624f77f7f321f7ff325b251aa09e52e46f1d6a", size = 124468, upload-time = "2026-01-04T18:37:36.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/74/7bfd403e81f9b5ec523f60cced8f516ee52312752bb2e0fafabfd90bbd78/pyinstrument-5.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a49a55ca5b75218767e29cacbe515d0b66fc18cb48a937bca0f77b8dafc7202", size = 148057, upload-time = "2026-01-04T18:37:37.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/3a/7205d7c199947d18edcd013af4ddf4d3cca85c5488fbe493050035947f7c/pyinstrument-5.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c45c14974ff04b1bfdc6c2a448627c6da7409c7800d0eb7bd03fb435dcb41d7", size = 146526, upload-time = "2026-01-04T18:37:39.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/e8/f6864172e7ebe4bc5209bafbc574a619b4c511b9506b941789b11441be7c/pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:22b9c04b3982c41c04b1c5ed05d1bc3a2ba26533450084058119f6dc160e70a3", size = 147179, upload-time = "2026-01-04T18:37:41.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/04/89ef2d1c34767bfdbcc74ab0c7e0d021d7fac5e79873239e4ca26e97d6da/pyinstrument-5.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5c4995ee0774801790c138f0dfec17d4e7a7ef09a6d56d53cbcbf0578a711021", size = 146354, upload-time = "2026-01-04T18:37:42.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/d4/64441547ec12391b92c739a3b0685059e7dfa088d928df8364676ef7abc7/pyinstrument-5.1.2-cp311-cp311-win32.whl", hash = "sha256:fe449e4a8ee60a2a27cf509350a584670f4c3704649601be7937598f09dbe7ca", size = 125790, upload-time = "2026-01-04T18:37:44.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/8b/0a5f6b239294decb0ecd932711f3470bfbd42fc2e08a94cd5c1f4f6da7f1/pyinstrument-5.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:3fb839429671a42bf349335af4c1ce5cf83386ac11f04df0bc40720d4cb7d77d", size = 126578, upload-time = "2026-01-04T18:37:45.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/d9/8fa5571ddd21b2b7189bd8b0bb4e90be1659a54dda5af51c7f6bf2b5666f/pyinstrument-5.1.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2519865d4bf58936f2506c1c46a82d29a20f3239aa50c941df1ca9618c7da5f0", size = 131419, upload-time = "2026-01-04T18:37:46.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/50/0512adb83cadfeaa1a215dc9784defff5043c5aa052d15015e3d8013af75/pyinstrument-5.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:059442106b8b5de29ae5ac1bdc20d044fed4da534b8caba434b6ffb119037bf5", size = 124446, upload-time = "2026-01-04T18:37:48.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/78/c45f0b668fb3c8c0d32058a451a8e1d34737cd7586387982185e12df1977/pyinstrument-5.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd51f2d54fc39a4cfd73ba6be27cd0187123132ce3f445b639bff5e1b23d7e26", size = 149694, upload-time = "2026-01-04T18:37:49.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/4d/2ca3ca9906ce6e05070f431c54d54ccbaf57a980cfa58032d35b0b0ac1f8/pyinstrument-5.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12af1e83795b6c640d657d339014dd1ff718b182dec736d7d1f1d8a97534eb53", size = 148461, upload-time = "2026-01-04T18:37:51.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d2/bfe84a4326172ef68655b65b49fd041eeb94c8e59ee47258589b8b79dd3b/pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2565513658e742c5eb691a779cb29d19d01bc9ee951d0eb76482e9f343c38c2e", size = 148560, upload-time = "2026-01-04T18:37:52.931Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/00/db7f5def351e869230b0165828c4edacbf3fdda8d66aff30dd73a62082c2/pyinstrument-5.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5afd0ba788a1d112da49fb77966918e01df1f9e7d62e72894d82f7acb0996c2d", size = 148178, upload-time = "2026-01-04T18:37:54.278Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/bc/aea3329576e20b987d205027b8e6442ece845d681b9f9d8682d5404f81f3/pyinstrument-5.1.2-cp312-cp312-win32.whl", hash = "sha256:554077b031b278593cb2301f0057be771ea62a729878c69aaf29fcdfb7b71281", size = 125927, upload-time = "2026-01-04T18:37:55.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e2/d928434ec3a840478e95fd0d73b0dfc0b8060a07b06f4b45e9df30444e9a/pyinstrument-5.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:55a905384ba43efc924b8863aa6cfd276f029e4aa70c4a0e3b7389e27b191e45", size = 126675, upload-time = "2026-01-04T18:37:57.278Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.12.1"
|
||||
|
||||
Reference in New Issue
Block a user