mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-02 16:07:15 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0d8801902 | ||
|
|
5c5b1ee6b5 | ||
|
|
08f2f4bfe2 | ||
|
|
f993462973 | ||
|
|
ae70b8d60f | ||
|
|
38db6b51db | ||
|
|
31e9f4272c | ||
|
|
b8659c1af3 | ||
|
|
741115b36b | ||
|
|
1211db5cbb |
@@ -1,5 +1,26 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 3.1.2
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: fix dark mode select disabled color, ensure disabled cursor on display mode dropdown [@shamoon](https://github.com/shamoon) ([#13881](https://github.com/paperless-ngx/paperless-ngx/pull/13881))
|
||||||
|
- Fix: add disable to the drag-drop list component [@shamoon](https://github.com/shamoon) ([#13880](https://github.com/paperless-ngx/paperless-ngx/pull/13880))
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Chore: update screenshots for v3+ [@shamoon](https://github.com/shamoon) ([#13883](https://github.com/paperless-ngx/paperless-ngx/pull/13883))
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>2 changes</summary>
|
||||||
|
|
||||||
|
- Fix: fix dark mode select disabled color, ensure disabled cursor on display mode dropdown [@shamoon](https://github.com/shamoon) ([#13881](https://github.com/paperless-ngx/paperless-ngx/pull/13881))
|
||||||
|
- Fix: add disable to the drag-drop list component [@shamoon](https://github.com/shamoon) ([#13880](https://github.com/paperless-ngx/paperless-ngx/pull/13880))
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 3.1.1
|
## paperless-ngx 3.1.1
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ dependencies = [
|
|||||||
"httpx-oauth~=0.17",
|
"httpx-oauth~=0.17",
|
||||||
"ijson>=3.5.1",
|
"ijson>=3.5.1",
|
||||||
"imap-tools~=1.14.0",
|
"imap-tools~=1.14.0",
|
||||||
"jinja2~=3.1.5",
|
"jinja2~=3.1.6",
|
||||||
"langdetect~=1.0.9",
|
"langdetect~=1.0.9",
|
||||||
"llama-index-core>=0.14.23",
|
"llama-index-core>=0.14.23",
|
||||||
"llama-index-embeddings-huggingface>=0.6.1",
|
"llama-index-embeddings-huggingface>=0.6.1",
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
</h6>
|
</h6>
|
||||||
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
|
||||||
@for (view of savedViewService.sidebarViews; track view.id) {
|
@for (view of savedViewService.sidebarViews; track view.id) {
|
||||||
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews() || !canSaveSettings"
|
<li class="nav-item app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews() || !canSaveSettings"
|
||||||
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
|
||||||
(cdkDragEnded)="onDragEnd($event)">
|
(cdkDragEnded)="onDragEnd($event)">
|
||||||
<a class="nav-link" routerLink="view/{{view.id}}"
|
<a class="nav-link" routerLink="view/{{view.id}}"
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
}
|
}
|
||||||
</a>
|
</a>
|
||||||
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
|
@if (settingsService.organizingSidebarSavedViews() && canSaveSettings) {
|
||||||
<div class="position-absolute end-0 top-0 px-3 py-2" [class.me-n3]="slimSidebarEnabled" cdkDragHandle>
|
<div class="position-absolute end-0 top-0 px-1 py-2" [class.me-n2]="slimSidebarEnabled" cdkDragHandle>
|
||||||
<i-bs name="grip-vertical"></i-bs>
|
<i-bs name="grip-vertical"></i-bs>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
</li>
|
</li>
|
||||||
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
<li class="nav-item" [class.visually-hidden]="slimSidebarEnabled">
|
||||||
<div class="text-muted small d-flex align-items-center flex-wrap nav-label">
|
<div class="text-muted small d-flex align-items-center flex-wrap nav-label">
|
||||||
<div class="me-3">
|
<div class="me-2">
|
||||||
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer"
|
<a class="text-muted text-decoration-none" target="_blank" rel="noopener noreferrer"
|
||||||
href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover
|
href="https://github.com/paperless-ngx/paperless-ngx" ngbPopover="GitHub" i18n-ngbPopover
|
||||||
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
[disablePopover]="!slimSidebarPopoversEnabled" placement="end" container="body"
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@if (!settingsService.updateCheckingIsSet || appRemoteVersion()) {
|
@if (!settingsService.updateCheckingIsSet || appRemoteVersion()) {
|
||||||
<div class="version-check">
|
<div class="version-check d-flex align-items-center">
|
||||||
<ng-template #updateAvailablePopContent>
|
<ng-template #updateAvailablePopContent>
|
||||||
<span class="small">Paperless-ngx {{ appRemoteVersion().version }} <ng-container i18n>is
|
<span class="small">Paperless-ngx {{ appRemoteVersion().version }} <ng-container i18n>is
|
||||||
available.</ng-container><br /><ng-container i18n>Click to view.</ng-container></span>
|
available.</ng-container><br /><ng-container i18n>Click to view.</ng-container></span>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
@if (useDropdown) {
|
@if (useDropdown) {
|
||||||
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
|
<div class="btn-group w-100" role="group" ngbDropdown #dropdown="ngbDropdown" (openChange)="onOpenChange($event)" [popperOptions]="popperOptions">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdown_toggle" ngbDropdownToggle [disabled]="disabled" [aria-label]="title">
|
<button class="btn btn-sm" [ngClass]="!editing && isActive ? 'btn-primary' : 'btn-outline-primary'" id="dropdown_toggle" ngbDropdownToggle [disabled]="disabled" [aria-label]="title">
|
||||||
<i-bs name="{{icon}}"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
|
<i-bs name="{{icon}}"></i-bs><div class="d-none d-sm-inline ms-1">{{title}}</div>
|
||||||
@if (isActive) {
|
@if (isActive) {
|
||||||
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
|
<pngx-clearable-badge [selected]="isActive" (cleared)="reset()"></pngx-clearable-badge>
|
||||||
|
|||||||
+7
-4
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
getLocaleNumberSymbol,
|
getLocaleNumberSymbol,
|
||||||
|
NgClass,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
NumberSymbol,
|
NumberSymbol,
|
||||||
} from '@angular/common'
|
} from '@angular/common'
|
||||||
@@ -48,25 +49,26 @@ import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.comp
|
|||||||
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
||||||
|
|
||||||
export class CustomFieldQueriesModel {
|
export class CustomFieldQueriesModel {
|
||||||
private _queries: CustomFieldQueryElement[] = []
|
private readonly _queries = signal<CustomFieldQueryElement[]>([])
|
||||||
private rootSubscriptions: Subscription[] = []
|
private rootSubscriptions: Subscription[] = []
|
||||||
|
|
||||||
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
public readonly changed = new Subject<CustomFieldQueriesModel>()
|
||||||
|
|
||||||
public get queries(): CustomFieldQueryElement[] {
|
public get queries(): CustomFieldQueryElement[] {
|
||||||
return this._queries
|
return this._queries()
|
||||||
}
|
}
|
||||||
|
|
||||||
public set queries(value: CustomFieldQueryElement[]) {
|
public set queries(value: CustomFieldQueryElement[]) {
|
||||||
this.teardownRootSubscriptions()
|
this.teardownRootSubscriptions()
|
||||||
this._queries = value ?? []
|
const queries = value ?? []
|
||||||
for (const element of this._queries) {
|
for (const element of queries) {
|
||||||
this.rootSubscriptions.push(
|
this.rootSubscriptions.push(
|
||||||
element.changed.subscribe(() => {
|
element.changed.subscribe(() => {
|
||||||
this.changed.next(this)
|
this.changed.next(this)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
this._queries.set(queries)
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear(fireEvent = true) {
|
public clear(fireEvent = true) {
|
||||||
@@ -209,6 +211,7 @@ export class CustomFieldQueriesModel {
|
|||||||
DocumentLinkComponent,
|
DocumentLinkComponent,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
NgbDatepickerModule,
|
NgbDatepickerModule,
|
||||||
|
NgClass,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
NgxBootstrapIconsModule,
|
NgxBootstrapIconsModule,
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body">
|
<div class="row sticky-top py-3 mt-n2 mt-md-n3 bg-body rounded shadow-sm">
|
||||||
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [filterRules]="list.filterRules" (filterRulesChange)="onFilterRulesChange($event)" (resetFilterRules)="onFilterRulesReset($event)" [unmodifiedFilterRules]="unmodifiedFilterRules()" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
<pngx-filter-editor [hidden]="isBulkEditing" [disabled]="isBulkEditing" [filterRules]="list.filterRules" (filterRulesChange)="onFilterRulesChange($event)" (resetFilterRules)="onFilterRulesReset($event)" [unmodifiedFilterRules]="unmodifiedFilterRules()" [selectionData]="list.selectionData" #filterEditor></pngx-filter-editor>
|
||||||
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1034,6 +1034,49 @@ describe('FilterEditorComponent', () => {
|
|||||||
).toEqual([42, CustomFieldQueryOperator.Exists, 'true'])
|
).toEqual([42, CustomFieldQueryOperator.Exists, 'true'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should reflect ingested custom field query rules in the dropdown toggle', () => {
|
||||||
|
const dropdown = fixture.debugElement.query(
|
||||||
|
By.css('pngx-custom-fields-query-dropdown')
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
||||||
|
).toBeNull()
|
||||||
|
|
||||||
|
// switching to a view with a custom field query
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_CUSTOM_FIELDS_QUERY,
|
||||||
|
value: '["OR",[[42,"exists","true"]]]',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
||||||
|
).not.toBeNull()
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement
|
||||||
|
.querySelector('#dropdown_toggle')
|
||||||
|
.classList.contains('btn-primary')
|
||||||
|
).toBeTruthy()
|
||||||
|
|
||||||
|
// and back to a view without one
|
||||||
|
component.filterRules = [
|
||||||
|
{
|
||||||
|
rule_type: FILTER_HAS_TAGS_ALL,
|
||||||
|
value: '19',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement.querySelector('pngx-clearable-badge')
|
||||||
|
).toBeNull()
|
||||||
|
expect(
|
||||||
|
dropdown.nativeElement
|
||||||
|
.querySelector('#dropdown_toggle')
|
||||||
|
.classList.contains('btn-primary')
|
||||||
|
).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('should ingest filter rules for owner', () => {
|
it('should ingest filter rules for owner', () => {
|
||||||
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
|
||||||
OwnerFilterType.NONE
|
OwnerFilterType.NONE
|
||||||
|
|||||||
+5
-16
@@ -47,6 +47,8 @@ $grid-breakpoints: (
|
|||||||
);
|
);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
--bs-border-radius: #{$border-radius};
|
||||||
|
|
||||||
@each $name, $value in $grid-breakpoints {
|
@each $name, $value in $grid-breakpoints {
|
||||||
--bs-breakpoint-#{$name}: #{$value};
|
--bs-breakpoint-#{$name}: #{$value};
|
||||||
}
|
}
|
||||||
@@ -78,19 +80,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
--bs-btn-border-radius: .425rem;
|
--bs-border-radius-sm: #{$border-radius};
|
||||||
--bs-border-radius-sm: .425rem;
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control,
|
|
||||||
.form-select,
|
|
||||||
.input-group-text {
|
|
||||||
border-radius: .425rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination, .input-group {
|
.pagination, .input-group {
|
||||||
--bs-border-radius-sm: .425rem;
|
--bs-border-radius-sm: #{$border-radius};
|
||||||
}
|
}
|
||||||
|
|
||||||
@media(min-width: 768px) {
|
@media(min-width: 768px) {
|
||||||
@@ -689,10 +684,6 @@ table.table {
|
|||||||
--bs-toast-max-width: var(--pngx-toast-max-width);
|
--bs-toast-max-width: var(--pngx-toast-max-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
|
||||||
--bs-border-radius: .425rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-primary {
|
.alert-primary {
|
||||||
--bs-alert-color: var(--bs-primary);
|
--bs-alert-color: var(--bs-primary);
|
||||||
--bs-alert-bg: var(--pngx-primary-faded);
|
--bs-alert-bg: var(--pngx-primary-faded);
|
||||||
@@ -824,8 +815,6 @@ code {
|
|||||||
--bs-accordion-bg: var(--bs-light);
|
--bs-accordion-bg: var(--bs-light);
|
||||||
--bs-accordion-active-color: var(--bs-primary);
|
--bs-accordion-active-color: var(--bs-primary);
|
||||||
--bs-accordion-active-bg: var(--pngx-bg-alt);
|
--bs-accordion-active-bg: var(--pngx-bg-alt);
|
||||||
--bs-border-radius: .425rem;
|
|
||||||
--bs-accordion-inner-border-radius: calc(.425rem - 1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordion-button::after {
|
.accordion-button::after {
|
||||||
@@ -849,7 +838,7 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Animate items as they're being sorted. */
|
/* Animate items as they're being sorted. */
|
||||||
.cdk-drop-list-dragging .cdk-drag {
|
.cdk-drop-list-dragging .cdk-drag:not(.cdk-drag-preview) {
|
||||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,%3csvg xmlns='h
|
|||||||
--bs-tertiary-bg: var(--pngx-bg-darker);
|
--bs-tertiary-bg: var(--pngx-bg-darker);
|
||||||
--bs-dark-border-subtle: var(--pngx-bg-darker);
|
--bs-dark-border-subtle: var(--pngx-bg-darker);
|
||||||
--bs-border-color-translucent: rgba(0, 0, 0, .175); // override bs
|
--bs-border-color-translucent: rgba(0, 0, 0, .175); // override bs
|
||||||
|
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15); // slightly darker than bs default
|
||||||
|
|
||||||
.text-dark, .text-light {
|
.text-dark, .text-light {
|
||||||
color: var(--bs-body-color) !important;
|
color: var(--bs-body-color) !important;
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ from django.core.cache import cache
|
|||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
from paperless.signed_pickle import SignedPickleError
|
||||||
|
from paperless.signed_pickle import signed_pickle_dumps
|
||||||
|
from paperless.signed_pickle import signed_pickle_loads
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.core.cache.backends.base import BaseCache
|
from django.core.cache.backends.base import BaseCache
|
||||||
@@ -118,9 +121,11 @@ class StoredLRUCache(LRUCache):
|
|||||||
serialized_data = self._backend.get(self._backend_key)
|
serialized_data = self._backend.get(self._backend_key)
|
||||||
try:
|
try:
|
||||||
self._data = (
|
self._data = (
|
||||||
pickle.loads(serialized_data) if serialized_data else OrderedDict()
|
signed_pickle_loads(serialized_data)
|
||||||
|
if serialized_data
|
||||||
|
else OrderedDict()
|
||||||
)
|
)
|
||||||
except pickle.PickleError:
|
except (SignedPickleError, pickle.PickleError):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Cache exists in backend but could not be read (possibly invalid format)",
|
"Cache exists in backend but could not be read (possibly invalid format)",
|
||||||
)
|
)
|
||||||
@@ -132,7 +137,7 @@ class StoredLRUCache(LRUCache):
|
|||||||
"""
|
"""
|
||||||
self._backend.set(
|
self._backend.set(
|
||||||
self._backend_key,
|
self._backend_key,
|
||||||
pickle.dumps(self._data),
|
signed_pickle_dumps(self._data),
|
||||||
self.backend_ttl,
|
self.backend_ttl,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ from documents.caching import CLASSIFIER_VERSION_KEY
|
|||||||
from documents.caching import StoredLRUCache
|
from documents.caching import StoredLRUCache
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
|
from paperless.signed_pickle import SignedPickleError
|
||||||
|
from paperless.signed_pickle import signed_pickle_dumps
|
||||||
|
from paperless.signed_pickle import signed_pickle_loads
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.classifier")
|
logger = logging.getLogger("paperless.classifier")
|
||||||
|
|
||||||
@@ -527,10 +530,17 @@ class DocumentClassifier:
|
|||||||
serialized_result = read_cache.get(key)
|
serialized_result = read_cache.get(key)
|
||||||
if serialized_result is None:
|
if serialized_result is None:
|
||||||
result = self.data_vectorizer.transform([self.preprocess_content(content)])
|
result = self.data_vectorizer.transform([self.preprocess_content(content)])
|
||||||
read_cache.set(key, pickle.dumps(result), CACHE_5_MINUTES)
|
read_cache.set(key, signed_pickle_dumps(result), CACHE_5_MINUTES)
|
||||||
else:
|
else:
|
||||||
read_cache.touch(key, CACHE_5_MINUTES)
|
try:
|
||||||
result = pickle.loads(serialized_result)
|
result = signed_pickle_loads(serialized_result)
|
||||||
|
except SignedPickleError:
|
||||||
|
result = self.data_vectorizer.transform(
|
||||||
|
[self.preprocess_content(content)],
|
||||||
|
)
|
||||||
|
read_cache.set(key, signed_pickle_dumps(result), CACHE_5_MINUTES)
|
||||||
|
else:
|
||||||
|
read_cache.touch(key, CACHE_5_MINUTES)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def predict_correspondent(self, content: str) -> int | None:
|
def predict_correspondent(self, content: str) -> int | None:
|
||||||
|
|||||||
@@ -462,7 +462,11 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
|
|||||||
"""
|
"""
|
||||||
Returns a sanitized filename for the document, not including any paths.
|
Returns a sanitized filename for the document, not including any paths.
|
||||||
"""
|
"""
|
||||||
result = str(self)
|
# Root owns metadata for all versions
|
||||||
|
context_document = (
|
||||||
|
self.root_document if self.root_document_id is not None else self
|
||||||
|
)
|
||||||
|
result = str(context_document)
|
||||||
|
|
||||||
if counter:
|
if counter:
|
||||||
result += f"_{counter:02}"
|
result += f"_{counter:02}"
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
|
|||||||
- API is called
|
- API is called
|
||||||
THEN:
|
THEN:
|
||||||
- Last correspondence date is returned only if requested for list, and for detail
|
- Last correspondence date is returned only if requested for list, and for detail
|
||||||
|
- The date is scoped to documents the requesting user may view
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Document.objects.create(
|
Document.objects.create(
|
||||||
@@ -145,6 +146,32 @@ class TestApiObjects(DirectoriesMixin, APITestCase):
|
|||||||
response.data["last_correspondence"],
|
response.data["last_correspondence"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# A newer document owned by another user must not leak through the
|
||||||
|
# aggregate for a non-superuser who cannot view it
|
||||||
|
other = User.objects.create_user(username="other")
|
||||||
|
Document.objects.create(
|
||||||
|
mime_type="application/pdf",
|
||||||
|
correspondent=self.c1,
|
||||||
|
created=datetime.date(2023, 6, 1),
|
||||||
|
checksum="hidden",
|
||||||
|
owner=other,
|
||||||
|
)
|
||||||
|
|
||||||
|
user = User.objects.create_user(username="regular")
|
||||||
|
user.user_permissions.add(
|
||||||
|
Permission.objects.get(codename="view_correspondent"),
|
||||||
|
)
|
||||||
|
self.client.force_authenticate(user=user)
|
||||||
|
|
||||||
|
response = self.client.get("/api/correspondents/?last_correspondence=true")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
result = next(r for r in response.data["results"] if r["id"] == self.c1.id)
|
||||||
|
self.assertIn("2022-01-02", result["last_correspondence"])
|
||||||
|
|
||||||
|
response = self.client.get(f"/api/correspondents/{self.c1.id}/")
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn("2022-01-02", response.data["last_correspondence"])
|
||||||
|
|
||||||
def test_paginated_objects_include_all_only_for_legacy_version(self) -> None:
|
def test_paginated_objects_include_all_only_for_legacy_version(self) -> None:
|
||||||
response_v10 = self.client.get("/api/correspondents/")
|
response_v10 = self.client.get("/api/correspondents/")
|
||||||
self.assertEqual(response_v10.status_code, status.HTTP_200_OK)
|
self.assertEqual(response_v10.status_code, status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import pickle
|
|
||||||
|
|
||||||
from documents.caching import StoredLRUCache
|
from documents.caching import StoredLRUCache
|
||||||
|
from paperless.signed_pickle import HMAC_SIZE
|
||||||
|
from paperless.signed_pickle import signed_pickle_dumps
|
||||||
|
from paperless.signed_pickle import signed_pickle_loads
|
||||||
|
|
||||||
|
|
||||||
def test_lru_cache_entries() -> None:
|
def test_lru_cache_entries() -> None:
|
||||||
@@ -42,4 +43,16 @@ def test_stored_lru_cache_key_ttl(mocker) -> None:
|
|||||||
key, data, timeout = mock_backend.set.call_args[0]
|
key, data, timeout = mock_backend.set.call_args[0]
|
||||||
assert key == "test_key"
|
assert key == "test_key"
|
||||||
assert timeout == 321
|
assert timeout == 321
|
||||||
assert pickle.loads(data) == {"x": "X", "y": "Y"}
|
assert signed_pickle_loads(data) == {"x": "X", "y": "Y"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_stored_lru_cache_rejects_tampered_data(mocker) -> None:
|
||||||
|
serialized_data = bytearray(signed_pickle_dumps({"x": "X"}))
|
||||||
|
serialized_data[HMAC_SIZE] ^= 0xFF
|
||||||
|
mock_backend = mocker.Mock()
|
||||||
|
mock_backend.get.return_value = bytes(serialized_data)
|
||||||
|
cache = StoredLRUCache("test_key", backend=mock_backend)
|
||||||
|
|
||||||
|
cache.load()
|
||||||
|
|
||||||
|
assert cache.get("x") is None
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ from documents.models import MatchingModel
|
|||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
from paperless.signed_pickle import HMAC_SIZE
|
||||||
|
from paperless.signed_pickle import signed_pickle_dumps
|
||||||
|
|
||||||
|
|
||||||
def dummy_preprocess(content: str, **kwargs):
|
def dummy_preprocess(content: str, **kwargs):
|
||||||
@@ -265,6 +267,27 @@ class TestClassifier(DirectoriesMixin, TestCase):
|
|||||||
self.assertEqual(mock_preprocess_content.call_count, 2)
|
self.assertEqual(mock_preprocess_content.call_count, 2)
|
||||||
self.assertEqual(mock_transform.call_count, 2)
|
self.assertEqual(mock_transform.call_count, 2)
|
||||||
|
|
||||||
|
def test_vectorize_recomputes_tampered_cache_entry(self) -> None:
|
||||||
|
cached = bytearray(signed_pickle_dumps(["cached vector"]))
|
||||||
|
cached[HMAC_SIZE] ^= 0xFF
|
||||||
|
self.classifier.data_vectorizer = mock.Mock()
|
||||||
|
self.classifier.data_vectorizer.transform.return_value = ["fresh vector"]
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"documents.classifier.read_cache.get",
|
||||||
|
return_value=bytes(cached),
|
||||||
|
),
|
||||||
|
mock.patch("documents.classifier.read_cache.set") as cache_set,
|
||||||
|
mock.patch("documents.classifier.read_cache.touch") as cache_touch,
|
||||||
|
):
|
||||||
|
result = self.classifier._vectorize("content")
|
||||||
|
|
||||||
|
self.assertEqual(result, ["fresh vector"])
|
||||||
|
self.classifier.data_vectorizer.transform.assert_called_once()
|
||||||
|
cache_set.assert_called_once()
|
||||||
|
cache_touch.assert_not_called()
|
||||||
|
|
||||||
def test_no_retrain_if_no_change(self) -> None:
|
def test_no_retrain_if_no_change(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
|
|||||||
@@ -156,6 +156,40 @@ class TestDocument(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(doc.get_public_filename(), "2020-12-25 test")
|
self.assertEqual(doc.get_public_filename(), "2020-12-25 test")
|
||||||
|
|
||||||
|
def test_version_file_name_uses_root_document_metadata(self) -> None:
|
||||||
|
root_correspondent = Correspondent.objects.create(name="Root correspondent")
|
||||||
|
version_correspondent = Correspondent.objects.create(
|
||||||
|
name="Version correspondent",
|
||||||
|
)
|
||||||
|
root = Document.objects.create(
|
||||||
|
mime_type="application/pdf",
|
||||||
|
title="Root title",
|
||||||
|
created=date(2020, 12, 25),
|
||||||
|
correspondent=root_correspondent,
|
||||||
|
)
|
||||||
|
version = Document.objects.create(
|
||||||
|
mime_type="application/pdf",
|
||||||
|
title="Version title",
|
||||||
|
created=date(1990, 1, 1),
|
||||||
|
correspondent=version_correspondent,
|
||||||
|
root_document=root,
|
||||||
|
version_index=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
version.get_public_filename(),
|
||||||
|
"2020-12-25 Root correspondent Root title.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
|
root.title = "Updated root title"
|
||||||
|
root.save(update_fields=("title",))
|
||||||
|
version.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
version.get_public_filename(),
|
||||||
|
"2020-12-25 Root correspondent Updated root title.pdf",
|
||||||
|
)
|
||||||
|
|
||||||
def test_suggestion_content_uses_latest_version_content_for_root_documents(
|
def test_suggestion_content_uses_latest_version_content_for_root_documents(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -192,6 +192,50 @@ class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
|
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
|
||||||
self.assertIn("sharelink_notfound=1", response["Location"])
|
self.assertIn("sharelink_notfound=1", response["Location"])
|
||||||
|
|
||||||
|
def test_share_link_missing_file_redirects(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A share link whose document file is missing from disk
|
||||||
|
WHEN:
|
||||||
|
- The public share link is requested anonymously
|
||||||
|
THEN:
|
||||||
|
- The user is redirected to login instead of a 500 error
|
||||||
|
"""
|
||||||
|
doc = DocumentFactory.create(filename="missing-original.pdf")
|
||||||
|
share_link = ShareLink.objects.create(
|
||||||
|
slug="missingfilelink",
|
||||||
|
document=doc,
|
||||||
|
file_version=ShareLink.FileVersion.ORIGINAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.get(f"/share/{share_link.slug}/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
|
||||||
|
self.assertIn("sharelink_notfound=1", response["Location"])
|
||||||
|
|
||||||
|
def test_download_ready_bundle_missing_file_returns_503(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A READY bundle whose zip file is missing from disk
|
||||||
|
WHEN:
|
||||||
|
- The public share link is requested anonymously
|
||||||
|
THEN:
|
||||||
|
- A 503 is returned instead of a 500 error
|
||||||
|
"""
|
||||||
|
bundle = ShareLinkBundle.objects.create(
|
||||||
|
slug="missingbundlefile",
|
||||||
|
file_version=ShareLink.FileVersion.ARCHIVE,
|
||||||
|
status=ShareLinkBundle.Status.READY,
|
||||||
|
file_path="bundles/gone.zip",
|
||||||
|
)
|
||||||
|
bundle.documents.set([self.document])
|
||||||
|
|
||||||
|
self.client.logout()
|
||||||
|
response = self.client.get(f"/share/{bundle.slug}/")
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_503_SERVICE_UNAVAILABLE)
|
||||||
|
|
||||||
|
|
||||||
class ShareLinkBundleTaskTests(DirectoriesMixin, APITestCase):
|
class ShareLinkBundleTaskTests(DirectoriesMixin, APITestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|||||||
+26
-9
@@ -577,13 +577,19 @@ class CorrespondentViewSet(
|
|||||||
def list(self, request, *args, **kwargs):
|
def list(self, request, *args, **kwargs):
|
||||||
if request.query_params.get("last_correspondence", None):
|
if request.query_params.get("last_correspondence", None):
|
||||||
self.queryset = self.queryset.annotate(
|
self.queryset = self.queryset.annotate(
|
||||||
last_correspondence=Max("documents__created"),
|
last_correspondence=Max(
|
||||||
|
"documents__created",
|
||||||
|
filter=self.get_document_count_filter(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return super().list(request, *args, **kwargs)
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
self.queryset = self.queryset.annotate(
|
self.queryset = self.queryset.annotate(
|
||||||
last_correspondence=Max("documents__created"),
|
last_correspondence=Max(
|
||||||
|
"documents__created",
|
||||||
|
filter=self.get_document_count_filter(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return super().retrieve(request, *args, **kwargs)
|
return super().retrieve(request, *args, **kwargs)
|
||||||
|
|
||||||
@@ -4573,6 +4579,10 @@ class ShareLinkViewSet(
|
|||||||
class ShareLinkBundleViewSet(PassUserMixin, ModelViewSet[ShareLinkBundle]):
|
class ShareLinkBundleViewSet(PassUserMixin, ModelViewSet[ShareLinkBundle]):
|
||||||
model = ShareLinkBundle
|
model = ShareLinkBundle
|
||||||
|
|
||||||
|
# Bundles are immutable once created; rebuild via the dedicated action
|
||||||
|
# rather than PUT/PATCH.
|
||||||
|
http_method_names = ["get", "post", "delete", "head", "options"]
|
||||||
|
|
||||||
queryset = ShareLinkBundle.objects.all()
|
queryset = ShareLinkBundle.objects.all()
|
||||||
|
|
||||||
serializer_class = ShareLinkBundleSerializer
|
serializer_class = ShareLinkBundleSerializer
|
||||||
@@ -4707,12 +4717,15 @@ class SharedLinkView(View):
|
|||||||
and share_link.expiration < timezone.now()
|
and share_link.expiration < timezone.now()
|
||||||
):
|
):
|
||||||
return HttpResponseRedirect("/accounts/login/?sharelink_expired=1")
|
return HttpResponseRedirect("/accounts/login/?sharelink_expired=1")
|
||||||
return serve_file(
|
try:
|
||||||
doc=share_link.document,
|
return serve_file(
|
||||||
use_archive=share_link.file_version == ShareLink.FileVersion.ARCHIVE
|
doc=share_link.document,
|
||||||
and share_link.document.has_archive_version,
|
use_archive=share_link.file_version == ShareLink.FileVersion.ARCHIVE
|
||||||
disposition="inline",
|
and share_link.document.has_archive_version,
|
||||||
)
|
disposition="inline",
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return HttpResponseRedirect("/accounts/login/?sharelink_notfound=1")
|
||||||
|
|
||||||
bundle = ShareLinkBundle.objects.filter(slug=slug).first()
|
bundle = ShareLinkBundle.objects.filter(slug=slug).first()
|
||||||
if bundle is None:
|
if bundle is None:
|
||||||
@@ -4734,7 +4747,11 @@ class SharedLinkView(View):
|
|||||||
|
|
||||||
file_path = bundle.absolute_file_path
|
file_path = bundle.absolute_file_path
|
||||||
|
|
||||||
if bundle.status == ShareLinkBundle.Status.FAILED or file_path is None:
|
if (
|
||||||
|
bundle.status == ShareLinkBundle.Status.FAILED
|
||||||
|
or file_path is None
|
||||||
|
or not file_path.exists()
|
||||||
|
):
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
_(
|
_(
|
||||||
"The share link bundle is unavailable.",
|
"The share link bundle is unavailable.",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+3
-31
@@ -1,12 +1,12 @@
|
|||||||
import hmac
|
|
||||||
import os
|
import os
|
||||||
import pickle
|
|
||||||
from hashlib import sha256
|
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
from celery.signals import worker_process_init
|
from celery.signals import worker_process_init
|
||||||
from kombu.serialization import register
|
from kombu.serialization import register
|
||||||
|
|
||||||
|
from paperless.signed_pickle import signed_pickle_dumps
|
||||||
|
from paperless.signed_pickle import signed_pickle_loads
|
||||||
|
|
||||||
# Set the default Django settings module for the 'celery' program.
|
# Set the default Django settings module for the 'celery' program.
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings")
|
||||||
|
|
||||||
@@ -18,34 +18,6 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "paperless.settings")
|
|||||||
# on the worker side using Django's SECRET_KEY.
|
# on the worker side using Django's SECRET_KEY.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
HMAC_SIZE = 32 # SHA-256 digest length
|
|
||||||
|
|
||||||
|
|
||||||
def _get_signing_key() -> bytes:
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
return settings.SECRET_KEY.encode()
|
|
||||||
|
|
||||||
|
|
||||||
def signed_pickle_dumps(obj: object) -> bytes:
|
|
||||||
data = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
|
|
||||||
signature = hmac.new(_get_signing_key(), data, sha256).digest()
|
|
||||||
return signature + data
|
|
||||||
|
|
||||||
|
|
||||||
def signed_pickle_loads(payload: bytes) -> object:
|
|
||||||
if len(payload) < HMAC_SIZE:
|
|
||||||
msg = "Signed-pickle payload too short"
|
|
||||||
raise ValueError(msg)
|
|
||||||
signature = payload[:HMAC_SIZE]
|
|
||||||
data = payload[HMAC_SIZE:]
|
|
||||||
expected = hmac.new(_get_signing_key(), data, sha256).digest()
|
|
||||||
if not hmac.compare_digest(signature, expected):
|
|
||||||
msg = "Signed-pickle HMAC verification failed — message may have been tampered with"
|
|
||||||
raise ValueError(msg)
|
|
||||||
return pickle.loads(data)
|
|
||||||
|
|
||||||
|
|
||||||
register(
|
register(
|
||||||
"signed-pickle",
|
"signed-pickle",
|
||||||
signed_pickle_dumps,
|
signed_pickle_dumps,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import pickle
|
||||||
|
from hashlib import sha256
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
HMAC_SIZE = sha256().digest_size
|
||||||
|
|
||||||
|
|
||||||
|
class SignedPickleError(ValueError):
|
||||||
|
"""Raised when a signed pickle payload cannot be authenticated."""
|
||||||
|
|
||||||
|
|
||||||
|
def _get_signing_key() -> bytes:
|
||||||
|
return settings.SECRET_KEY.encode()
|
||||||
|
|
||||||
|
|
||||||
|
def signed_pickle_dumps(obj: object) -> bytes:
|
||||||
|
data = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
|
signature = hmac.new(_get_signing_key(), data, sha256).digest()
|
||||||
|
return signature + data
|
||||||
|
|
||||||
|
|
||||||
|
def signed_pickle_loads(payload: bytes) -> Any:
|
||||||
|
if len(payload) <= HMAC_SIZE:
|
||||||
|
msg = "Signed-pickle payload too short"
|
||||||
|
raise SignedPickleError(msg)
|
||||||
|
|
||||||
|
signature = payload[:HMAC_SIZE]
|
||||||
|
data = payload[HMAC_SIZE:]
|
||||||
|
expected = hmac.new(_get_signing_key(), data, sha256).digest()
|
||||||
|
if not hmac.compare_digest(signature, expected):
|
||||||
|
msg = "Signed-pickle HMAC verification failed; payload may have been tampered with"
|
||||||
|
raise SignedPickleError(msg)
|
||||||
|
|
||||||
|
return pickle.loads(data)
|
||||||
@@ -6,9 +6,9 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from paperless.celery import HMAC_SIZE
|
|
||||||
from paperless.celery import signed_pickle_dumps
|
from paperless.celery import signed_pickle_dumps
|
||||||
from paperless.celery import signed_pickle_loads
|
from paperless.celery import signed_pickle_loads
|
||||||
|
from paperless.signed_pickle import HMAC_SIZE
|
||||||
|
|
||||||
|
|
||||||
class TestSignedPickleSerializer:
|
class TestSignedPickleSerializer:
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ urlpatterns = [
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
re_path(r"share/(?P<slug>\w+)/?$", SharedLinkView.as_view()),
|
re_path(r"^share/(?P<slug>\w+)/?$", SharedLinkView.as_view()),
|
||||||
re_path(r"^favicon.ico$", FaviconView.as_view(), name="favicon"),
|
re_path(r"^favicon.ico$", FaviconView.as_view(), name="favicon"),
|
||||||
re_path(r"admin/", admin.site.urls),
|
re_path(r"admin/", admin.site.urls),
|
||||||
re_path(
|
re_path(
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ requires-python = ">=3.11"
|
|||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.15' and sys_platform == 'darwin'",
|
"python_full_version >= '3.15' and sys_platform == 'darwin'",
|
||||||
"python_full_version >= '3.15' and sys_platform == 'linux'",
|
"python_full_version >= '3.15' and sys_platform == 'linux'",
|
||||||
|
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
|
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||||
"python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'darwin'",
|
"python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'darwin'",
|
||||||
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||||
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
|
||||||
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
|
||||||
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
|
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
|
||||||
"python_full_version < '3.12' and sys_platform == 'darwin'",
|
"python_full_version < '3.12' and sys_platform == 'darwin'",
|
||||||
"python_full_version < '3.12' and sys_platform == 'linux'",
|
"python_full_version < '3.12' and sys_platform == 'linux'",
|
||||||
@@ -3053,7 +3053,7 @@ requires-dist = [
|
|||||||
{ name = "httpx-oauth", specifier = "~=0.17" },
|
{ name = "httpx-oauth", specifier = "~=0.17" },
|
||||||
{ name = "ijson", specifier = ">=3.5.1" },
|
{ name = "ijson", specifier = ">=3.5.1" },
|
||||||
{ name = "imap-tools", specifier = "~=1.14.0" },
|
{ name = "imap-tools", specifier = "~=1.14.0" },
|
||||||
{ name = "jinja2", specifier = "~=3.1.5" },
|
{ name = "jinja2", specifier = "~=3.1.6" },
|
||||||
{ name = "langdetect", specifier = "~=1.0.9" },
|
{ name = "langdetect", specifier = "~=1.0.9" },
|
||||||
{ name = "llama-index-core", specifier = ">=0.14.23" },
|
{ name = "llama-index-core", specifier = ">=0.14.23" },
|
||||||
{ name = "llama-index-embeddings-huggingface", specifier = ">=0.6.1" },
|
{ name = "llama-index-embeddings-huggingface", specifier = ">=0.6.1" },
|
||||||
@@ -4397,24 +4397,24 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.16.1"
|
version = "0.16.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/70/25/7113f6d5498888c5fb7db34081cba7d5971c4cb1bfb26819966eee68f003/ruff-0.16.1.tar.gz", hash = "sha256:fedad7c801dabd3fb9741d76aca39246e6ddd9ca446a015875207bf19f1e6bc7", size = 4877500, upload-time = "2026-07-30T19:37:01.379Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/bd/694da69368e0973de65df2ddc73ab18d43c469d5963d9b150911de6bc513/ruff-0.16.1-py3-none-linux_armv6l.whl", hash = "sha256:58edb313b88f0c5460a26adf5f39a37a3be789494a15e3e411e35fa78b89f9a0", size = 10839126, upload-time = "2026-07-30T19:36:13.697Z" },
|
{ url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3f/f0/b626e5d5bd0dd9576263658ef12885e2288afd1029a48e26ffed65ec1ac1/ruff-0.16.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fde5a99e2f97479af66edd6622c6d5a2a7592c77cf4153d9e4428f5eeb55b60c", size = 11070253, upload-time = "2026-07-30T19:36:17.14Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/63/f40acfb6b35b88623e71684942b552c3edd96035f5d98f313815f7b277de/ruff-0.16.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e0d4c20532fca4f7fa609369161d968dd28f65d83dabbd61d8e9c7edbf7001f6", size = 10561425, upload-time = "2026-07-30T19:36:20.04Z" },
|
{ url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/dd/14ec0e9c2b4d315547dd38765004b4863e354e1b52cb308272215d9f6f6d/ruff-0.16.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30affbcedf59ad5703d9c91f82266e02b47739f797e1a7b6e158e5526a6dae38", size = 10948879, upload-time = "2026-07-30T19:36:22.476Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/e9/9d870cbae575030fdef595f04b4b97573c525b5497cce4f4498cf2f85446/ruff-0.16.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24e9c631573cbca9d20f1283f8f479b2afa4a8503504822bd71a293889f16743", size = 10643691, upload-time = "2026-07-30T19:36:24.914Z" },
|
{ url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/09/12743d544e2173f53ecd27217c65f90d2bc0f8424a66a60339e56bbc0457/ruff-0.16.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b41bdd48fb420987a9b5212e4957c26ad4abce401fa9ea9d4d85843727945f4f", size = 11435354, upload-time = "2026-07-30T19:36:28.447Z" },
|
{ url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/89/a1652b2daee52083c9554a6333b678a8b01d0400f976827bb87857f9449a/ruff-0.16.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0d1e1393b7648079e13669de1c1f4fde06d4583e84d8fd5c1551e0a77a2aa75", size = 12259033, upload-time = "2026-07-30T19:36:31.326Z" },
|
{ url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/96/ecdcb8c54ee7b123b487f807eb014e6e019155a0b81dfb669acd52f28ce3/ruff-0.16.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07bf434b1c95f4e093be4532068ef4fcf00924eb2ade8796075980902d6fd54a", size = 11667981, upload-time = "2026-07-30T19:36:34.394Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cd/90/c52e12e0d862e9572f2a33aa227409143520abe53111e9a6babbac7b4af8/ruff-0.16.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39897739f112253ee4fdd2e8aa9a4f9ded99fb2be367d5f31dfa4ded6025584c", size = 11468183, upload-time = "2026-07-30T19:36:37.339Z" },
|
{ url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/6b/4ffb7ad1d83eb16cf8cbb3c8815d3f11c88460fd162d4b372a2059be1c2a/ruff-0.16.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:82ae3c0c0d74daf17b968a10b7b3bb3ef297ab7de0c1f749646b25e690ccb150", size = 11470071, upload-time = "2026-07-30T19:36:39.91Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/72/32ae7db4c0b5e32ab611787caa19d1546800676d79f7483b7100a3561bf4/ruff-0.16.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4d5f2ed10f8242d83fc08d521301089364e3375375705356f20c0e31606ef3ef", size = 10919503, upload-time = "2026-07-30T19:36:42.65Z" },
|
{ url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/ca/3d901ba6ad6fc38da39c3448fc6c59ac945679293a17c3ceb6d6c1cba13e/ruff-0.16.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a4665b309891f83f3e3c25447935f1213e9abbd4b5640af7a1f2def9f8d413c1", size = 10649861, upload-time = "2026-07-30T19:36:45.18Z" },
|
{ url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/79/894ef1ced26552d5f8c9cf6d85b0687840e1128c55aeab7b9c2d54a0d880/ruff-0.16.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26e9ca5c9bc3971f20d3cf18a957f52ffd6a5f6564ff15c4912a144dcac22494", size = 11148137, upload-time = "2026-07-30T19:36:47.936Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/69/3609a09fa1cb46cc28b762363e440a354204e5dff01bd0c8d7437874d6b9/ruff-0.16.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:67e1e1e3fa4f0c82f0e36d4cd61e661f6e7a6196cb1aa92fe0828fa7b8f257cd", size = 11559211, upload-time = "2026-07-30T19:36:50.448Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5014,10 +5014,10 @@ version = "2.13.0+cpu"
|
|||||||
source = { registry = "https://download.pytorch.org/whl/cpu" }
|
source = { registry = "https://download.pytorch.org/whl/cpu" }
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.15' and sys_platform == 'linux'",
|
"python_full_version >= '3.15' and sys_platform == 'linux'",
|
||||||
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
|
||||||
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
|
||||||
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||||
|
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
|
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||||
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
|
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
|
||||||
"python_full_version < '3.12' and sys_platform == 'linux'",
|
"python_full_version < '3.12' and sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
@@ -5809,7 +5809,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zensical"
|
name = "zensical"
|
||||||
version = "0.0.51"
|
version = "0.0.57"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
@@ -5821,18 +5821,18 @@ dependencies = [
|
|||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
{ name = "tomli" },
|
{ name = "tomli" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b8/f7/d07ffb268ca86afb26b7f32dbabe25dec03d3aa63ba4d876720c84681d33/zensical-0.0.51.tar.gz", hash = "sha256:de25de067bedfa18f916d7f366fd64a7fbf09bfcc615b44d1ddbe3b5fe02ab49", size = 3979640, upload-time = "2026-07-17T18:08:03.445Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/83/f4/fa40086c46a2e59e3d9239031f76623622e60e0d79f3df1282df2797a5c4/zensical-0.0.57.tar.gz", hash = "sha256:25fcbdf89a57153cc3ad1108a89d17c7226da5d3c551a8839c69cbd9c472a9d8", size = 4000458, upload-time = "2026-08-21T20:43:49.5Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/21/02db3e1fb3904016bfac310037c95b9f1eaaf0ffe7b4a84f14263a7d95df/zensical-0.0.51-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:134d776afa526098e05e34713e2f577c075e57a232e01b97842bb0206716afce", size = 12791154, upload-time = "2026-07-17T18:07:20.748Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/b9/49c37dc65105d1ca4a8b600a02c84ece00218d2293b2630611c620185ca3/zensical-0.0.57-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:98867d1a6ea2c57f1ebcf4902f61601f427350f2df0c04e30cfac8ba6163cd29", size = 12888507, upload-time = "2026-08-21T20:43:20.365Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/35/b0d96f58253514cb3d08f5779020ab01ee5472334fb984b92e3fc9e9c9ac/zensical-0.0.51-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:e97ab39668ae3b452c550634e921a0336443743aae5e1fe031c7bb57d049e535", size = 12692190, upload-time = "2026-07-17T18:07:24.553Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/f7/54539984418de11387bbace39a744195555d32c98c95bf4d112b432548f5/zensical-0.0.57-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:0d7935d77d73a279545052e05d89d31960f30c1f33f53933f4c101fa271aee74", size = 12778169, upload-time = "2026-08-21T20:43:22.879Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/90/7a60e126a10c37c6b789938ff17e73fe76bba707fa029cb40ac659aeaa82/zensical-0.0.51-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c9579809f88608e7aa2cff516fff9d267d74a843cf6088a5f4227de2f092bb5", size = 13139337, upload-time = "2026-07-17T18:07:27.885Z" },
|
{ url = "https://files.pythonhosted.org/packages/40/16/74aa60aa4cfecd5bd31ce60cb6a092cb56f1bc1aaadcc173463861ea4eb5/zensical-0.0.57-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7046d433511d97aa603915f0f6792d15b7f839793abc2b66ab7b7ff753ecff5", size = 13230823, upload-time = "2026-08-21T20:43:25.141Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/c3/9101c97b90d4713ef2816db03366a45ae4762efebffd296737a2dd2df325/zensical-0.0.51-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:296dc7a14aa28b81a58eb57df2d5c9c9a4b0de7e90c11d99c943354287952925", size = 13069851, upload-time = "2026-07-17T18:07:31.814Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/d1/742d2487dd65dd18277daebcd37db56d5bd4a2408df02bde703ef8fb7b64/zensical-0.0.57-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab85c5066b95e3a877cf8971e4ce30abb1ca1459fbfcc631f0a5a2bab56351a4", size = 13170523, upload-time = "2026-08-21T20:43:27.456Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/79/0474df9e15a2c18f6281a786e10177c1b6e16feac1c568e7f36ad39b339c/zensical-0.0.51-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f779d2d87b4bf228cf2e279bc0ae6bcf3b36a9335ff283a317d01f7c15ae46b2", size = 13451083, upload-time = "2026-07-17T18:07:35.543Z" },
|
{ url = "https://files.pythonhosted.org/packages/56/6f/12b570775d344f1a3d77e26d4ae0160bcac9e41ca38f7135352ccdf9b2c8/zensical-0.0.57-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f13d1b57ad3c8b8634933a93ea870ebac11245fe0c968d27fd2a059ee1c6311", size = 13549941, upload-time = "2026-08-21T20:43:29.964Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/6f/91bbf78f704d5fd4c0c9be27d6bce3b6e4c2c339e4dcd6e7cf19ecda643c/zensical-0.0.51-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f813a1514a90890ca86248a8d54b81b2164bcbff11a6bcf11b01e1c01a1454", size = 13110446, upload-time = "2026-07-17T18:07:38.783Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/4e/436e6fc76674244c084ef7f6f17dc5ff85c76b15aef77c48b703fd0a2dda/zensical-0.0.57-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:021dd8fb70d1816cd012684fcf45d32b8f88a0cd28b7cbe71e5f8564f6d5764d", size = 13210086, upload-time = "2026-08-21T20:43:32.098Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/89/aa9a95f81771614c37bdc52b8ab21fcdef4c8de7c9cedf34e9bf62674281/zensical-0.0.51-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:186ef37e0eee0e969e2cfae47b1b97775e3164e2cba95c71faa4dd6ef47ed009", size = 13315871, upload-time = "2026-07-17T18:07:42.43Z" },
|
{ url = "https://files.pythonhosted.org/packages/ef/52/20f3aeda9af1090f24241670a5cc20fff7494545fea9f5fa094c82f3dbdf/zensical-0.0.57-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7e10f3c27fdc3eac3a9ae6ddcd87f3f00edc9f332050923313c95537961bfadd", size = 13408253, upload-time = "2026-08-21T20:43:34.258Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/11/1bf6e9ded29d376f8c12644cc4de04676b010fee8caa17f682606b1f16d5/zensical-0.0.51-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f5d91ce246ed930224603083cef02ae8947132fc7c52901d72015ea03526fa58", size = 13344382, upload-time = "2026-07-17T18:07:46.066Z" },
|
{ url = "https://files.pythonhosted.org/packages/e1/f2/2b18ba2f19674dbfcf745f3b66e005cc8efa66a1bcaba5e1b4f79467868a/zensical-0.0.57-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:78c85fee55c5aac3bdf8157e980c56397dca835167a5577c5429b5eb24ed990c", size = 13446689, upload-time = "2026-08-21T20:43:36.527Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/ec/663f16ff82d08b212e7c3236a88bd332f73331f94ddac1c91aaf882bbd1e/zensical-0.0.51-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1108eae82c6e8ffc33026f60b485c1512647a5333be4f547166b7c8877b98af", size = 13499628, upload-time = "2026-07-17T18:07:49.196Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/ba/68cdba447a9097e5f97742eef046020c6fa42d82972849b3a46a0718e890/zensical-0.0.57-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:478d252e1924f3876e72cf7806967cb62e50d86eddb3da04bf43e882b532fa1b", size = 13598580, upload-time = "2026-08-21T20:43:38.646Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/b4/7f1b6c3cf06d9f6ff5216523168a5d6ccc693444d5ceeb911eca97b30d98/zensical-0.0.51-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6fa0ecaf14f56841bfc595fa141396350c72aafbec73a016ebe3c824ed21ac72", size = 13451420, upload-time = "2026-07-17T18:07:52.563Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/89/6358a4df272328bed5bea90b04d43e73758bc45ff058c5cb2665e1147314/zensical-0.0.57-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66a9ca6b5f625b2a2b215eec2f3c72843a92d5d512042045ac6351d5dee9b339", size = 13557609, upload-time = "2026-08-21T20:43:40.866Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user