mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			717e828a1d
			...
			feature-fl
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f26f44c325 | ||
|   | 349fbce579 | ||
|   | 217b004884 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 29c36542fa | 
| @@ -1800,23 +1800,3 @@ password. All of these options come from their similarly-named [Django settings] | ||||
| #### [`PAPERLESS_EMAIL_USE_SSL=<bool>`](#PAPERLESS_EMAIL_USE_SSL) {#PAPERLESS_EMAIL_USE_SSL} | ||||
|  | ||||
| : Defaults to false. | ||||
|  | ||||
| ## Remote OCR | ||||
|  | ||||
| #### [`PAPERLESS_REMOTE_OCR_ENGINE=<str>`](#PAPERLESS_REMOTE_OCR_ENGINE) {#PAPERLESS_REMOTE_OCR_ENGINE} | ||||
|  | ||||
| : The remote OCR engine to use. Currently only Azure AI is supported as "azureai". | ||||
|  | ||||
|     Defaults to None, which disables remote OCR. | ||||
|  | ||||
| #### [`PAPERLESS_REMOTE_OCR_API_KEY=<str>`](#PAPERLESS_REMOTE_OCR_API_KEY) {#PAPERLESS_REMOTE_OCR_API_KEY} | ||||
|  | ||||
| : The API key to use for the remote OCR engine. | ||||
|  | ||||
|     Defaults to None. | ||||
|  | ||||
| #### [`PAPERLESS_REMOTE_OCR_ENDPOINT=<str>`](#PAPERLESS_REMOTE_OCR_ENDPOINT) {#PAPERLESS_REMOTE_OCR_ENDPOINT} | ||||
|  | ||||
| : The endpoint to use for the remote OCR engine. This is required for Azure AI. | ||||
|  | ||||
|     Defaults to None. | ||||
|   | ||||
| @@ -25,10 +25,9 @@ physical documents into a searchable online archive so you can keep, well, _less | ||||
| ## Features | ||||
|  | ||||
| -   **Organize and index** your scanned documents with tags, correspondents, types, and more. | ||||
| -   _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way, unless you explicitly choose to do so. | ||||
| -   _Your_ data is stored locally on _your_ server and is never transmitted or shared in any way. | ||||
| -   Performs **OCR** on your documents, adding searchable and selectable text, even to documents scanned with only images. | ||||
|     -   Utilizes the open-source Tesseract engine to recognize more than 100 languages. | ||||
|     -   _New!_ Supports remote OCR with Azure AI (opt-in). | ||||
| -   Utilizes the open-source Tesseract engine to recognize more than 100 languages. | ||||
| -   Documents are saved as PDF/A format which is designed for long term storage, alongside the unaltered originals. | ||||
| -   Uses machine-learning to automatically add tags, correspondents and document types to your documents. | ||||
| -   Supports PDF documents, images, plain text files, Office documents (Word, Excel, PowerPoint, and LibreOffice equivalents)[^1] and more. | ||||
|   | ||||
| @@ -850,18 +850,6 @@ how regularly you intend to scan documents and use paperless. | ||||
|     performed the task associated with the document, move it to the | ||||
|     inbox. | ||||
|  | ||||
| ## Remote OCR | ||||
|  | ||||
| !!! important | ||||
|  | ||||
|     This feature is disabled by default and will always remain strictly "opt-in". | ||||
|  | ||||
| Paperless-ngx supports performing OCR on documents using remote services. At the moment, this is limited to | ||||
| [Microsoft's Azure "Document Intelligence" service](https://azure.microsoft.com/en-us/products/ai-services/ai-document-intelligence). | ||||
| This is of course a paid service (with a free tier) which requires an Azure account and subscription. Azure AI is not affiliated with | ||||
| Paperless-ngx in any way. When enabled, Paperless-ngx will automatically send appropriate documents to Azure for OCR processing, bypassing | ||||
| the local OCR engine. See the [configuration](configuration.md#PAPERLESS_REMOTE_OCR_ENGINE) options for more details. | ||||
|  | ||||
| ## Architecture | ||||
|  | ||||
| Paperless-ngx consists of the following components: | ||||
|   | ||||
| @@ -15,7 +15,6 @@ classifiers = [ | ||||
| # This will allow testing to not install a webserver, mysql, etc | ||||
|  | ||||
| dependencies = [ | ||||
|   "azure-ai-documentintelligence>=1.0.2", | ||||
|   "babel>=2.17", | ||||
|   "bleach~=6.2.0", | ||||
|   "celery[redis]~=5.5.1", | ||||
| @@ -115,7 +114,7 @@ testing = [ | ||||
| ] | ||||
|  | ||||
| lint = [ | ||||
|   "pre-commit~=4.2.0", | ||||
|   "pre-commit~=4.3.0", | ||||
|   "pre-commit-uv~=4.1.3", | ||||
|   "ruff~=0.12.2", | ||||
| ] | ||||
| @@ -240,7 +239,6 @@ testpaths = [ | ||||
|   "src/paperless_tesseract/tests/", | ||||
|   "src/paperless_tika/tests", | ||||
|   "src/paperless_text/tests/", | ||||
|   "src/paperless_remote/tests/", | ||||
| ] | ||||
| addopts = [ | ||||
|   "--pythonwarnings=all", | ||||
|   | ||||
| @@ -363,11 +363,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">252</context> | ||||
|           <context context-type="linenumber">253</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">254</context> | ||||
|           <context context-type="linenumber">255</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2501522447884928778" datatype="html"> | ||||
| @@ -658,11 +658,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">287</context> | ||||
|           <context context-type="linenumber">288</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">290</context> | ||||
|           <context context-type="linenumber">291</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="2272120016352772836" datatype="html"> | ||||
| @@ -995,11 +995,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">212</context> | ||||
|           <context context-type="linenumber">213</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">214</context> | ||||
|           <context context-type="linenumber">215</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context> | ||||
| @@ -1579,11 +1579,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">275</context> | ||||
|           <context context-type="linenumber">276</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">277</context> | ||||
|           <context context-type="linenumber">278</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="103921551219467537" datatype="html"> | ||||
| @@ -1977,11 +1977,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">235</context> | ||||
|           <context context-type="linenumber">236</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">238</context> | ||||
|           <context context-type="linenumber">239</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3818027200170621545" datatype="html"> | ||||
| @@ -2334,11 +2334,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">266</context> | ||||
|           <context context-type="linenumber">267</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">268</context> | ||||
|           <context context-type="linenumber">269</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4569276013106377105" datatype="html"> | ||||
| @@ -2675,11 +2675,11 @@ | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">296</context> | ||||
|           <context context-type="linenumber">297</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">299</context> | ||||
|           <context context-type="linenumber">300</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="472206565520537964" datatype="html"> | ||||
| @@ -2697,36 +2697,36 @@ | ||||
|         <source>Open documents</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">138</context> | ||||
|           <context context-type="linenumber">139</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5687256342387781369" datatype="html"> | ||||
|         <source>Close all</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">158</context> | ||||
|           <context context-type="linenumber">159</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">160</context> | ||||
|           <context context-type="linenumber">161</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3897348120591552265" datatype="html"> | ||||
|         <source>Manage</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">169</context> | ||||
|           <context context-type="linenumber">170</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="7437910965833684826" datatype="html"> | ||||
|         <source>Correspondents</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">175</context> | ||||
|           <context context-type="linenumber">176</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">177</context> | ||||
|           <context context-type="linenumber">178</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context> | ||||
| @@ -2737,11 +2737,11 @@ | ||||
|         <source>Tags</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">182</context> | ||||
|           <context context-type="linenumber">183</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">185</context> | ||||
|           <context context-type="linenumber">186</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context> | ||||
| @@ -2772,11 +2772,11 @@ | ||||
|         <source>Document Types</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">191</context> | ||||
|           <context context-type="linenumber">192</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">193</context> | ||||
|           <context context-type="linenumber">194</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context> | ||||
| @@ -2787,11 +2787,11 @@ | ||||
|         <source>Storage Paths</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">198</context> | ||||
|           <context context-type="linenumber">199</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">200</context> | ||||
|           <context context-type="linenumber">201</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context> | ||||
| @@ -2802,11 +2802,11 @@ | ||||
|         <source>Custom Fields</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">205</context> | ||||
|           <context context-type="linenumber">206</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">207</context> | ||||
|           <context context-type="linenumber">208</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context> | ||||
| @@ -2821,11 +2821,11 @@ | ||||
|         <source>Workflows</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">221</context> | ||||
|           <context context-type="linenumber">222</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">223</context> | ||||
|           <context context-type="linenumber">224</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context> | ||||
| @@ -2836,71 +2836,71 @@ | ||||
|         <source>Mail</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">228</context> | ||||
|           <context context-type="linenumber">229</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">231</context> | ||||
|           <context context-type="linenumber">232</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="7844706011418789951" datatype="html"> | ||||
|         <source>Administration</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">246</context> | ||||
|           <context context-type="linenumber">247</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3008420115644088420" datatype="html"> | ||||
|         <source>Configuration</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">259</context> | ||||
|           <context context-type="linenumber">260</context> | ||||
|         </context-group> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">261</context> | ||||
|           <context context-type="linenumber">262</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1534029177398918729" datatype="html"> | ||||
|         <source>GitHub</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">306</context> | ||||
|           <context context-type="linenumber">307</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4112664765954374539" datatype="html"> | ||||
|         <source>is available.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">315,316</context> | ||||
|           <context context-type="linenumber">316,317</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1175891574282637937" datatype="html"> | ||||
|         <source>Click to view.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">316</context> | ||||
|           <context context-type="linenumber">317</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="9811291095862612" datatype="html"> | ||||
|         <source>Paperless-ngx can automatically check for updates</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">320</context> | ||||
|           <context context-type="linenumber">321</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="894819944961861800" datatype="html"> | ||||
|         <source> How does this work? </source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">327,329</context> | ||||
|           <context context-type="linenumber">328,330</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="509090351011426949" datatype="html"> | ||||
|         <source>Update available</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">340</context> | ||||
|           <context context-type="linenumber">341</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1542489069631984294" datatype="html"> | ||||
|   | ||||
| @@ -108,15 +108,16 @@ | ||||
|                 <li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews" | ||||
|                   cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)" | ||||
|                   (cdkDragEnded)="onDragEnd($event)"> | ||||
|                   <a class="nav-link" [class.text-truncate]="!slimSidebarEnabled" routerLink="view/{{view.id}}" | ||||
|                   <a class="nav-link" routerLink="view/{{view.id}}" | ||||
|                     routerLinkActive="active" (click)="closeMenu()" [ngbPopover]="view.name" | ||||
|                     [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" | ||||
|                     popoverClass="popover-slim"> | ||||
|                     <i-bs class="me-1" name="funnel"></i-bs><span> {{view.name}} | ||||
|                       @if (showSidebarCounts && !slimSidebarEnabled) { | ||||
|                         <span><span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span></span> | ||||
|                       } | ||||
|                     </span> | ||||
|                     <i-bs class="me-1" name="funnel"></i-bs> | ||||
|                       <span> <div class="d-inline-flex view-name"><span [class.text-truncate]="!slimSidebarEnabled">{{view.name}}</span></div> | ||||
|                         @if (showSidebarCounts && !slimSidebarEnabled) { | ||||
|                           <span class="badge bg-info text-dark ms-2 d-inline">{{ savedViewService.getDocumentCount(view) }}</span> | ||||
|                         } | ||||
|                       </span> | ||||
|                     @if (showSidebarCounts && slimSidebarEnabled) { | ||||
|                       <span class="badge bg-info text-dark position-absolute top-0 end-0 d-none d-md-block">{{ savedViewService.getDocumentCount(view) }}</span> | ||||
|                     } | ||||
|   | ||||
| @@ -19,6 +19,10 @@ | ||||
|     height: 0.8em; | ||||
|   } | ||||
|  | ||||
|   .view-name { | ||||
|     max-width: calc(100% - 50px) | ||||
|   } | ||||
|  | ||||
|   .nav-group:not(:has(.app-link)) .sidebar-heading { | ||||
|     display: none !important; | ||||
|   } | ||||
|   | ||||
| @@ -444,6 +444,12 @@ export class SettingsService { | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     this._renderer.setAttribute( | ||||
|       this.document.documentElement, | ||||
|       'data-bs-theme', | ||||
|       'dark-flat' | ||||
|     ) | ||||
|  | ||||
|     if (themeColor?.length) { | ||||
|       const hsl = hexToHsl(themeColor) | ||||
|       const bgBrightnessEstimate = estimateBrightnessForColor(themeColor) | ||||
|   | ||||
| @@ -349,3 +349,52 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt | ||||
|   } | ||||
|  | ||||
| } | ||||
|  | ||||
| [data-bs-theme="dark-flat"] { | ||||
|   body:not(.primary-light):not(.primary-dark) { | ||||
|     @include paperless-green-dark-mode; | ||||
|  | ||||
|     .navbar.bg-primary { | ||||
|       // navbar is og green in dark mode | ||||
|       @include paperless-green; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   @include dark-mode; | ||||
|  | ||||
|   .btn-outline-primary, .btn-outline-secondary { | ||||
|     border-color: var(--pngx-bg-alt) !important; | ||||
|     background-color: var(--pngx-bg-alt) !important; | ||||
|     color: var(--bs-body-color) !important; | ||||
|   } | ||||
|  | ||||
|   .btn-outline-secondary:hover, .btn-outline-secondary:focus, .btn-outline-secondary:active, .btn-outline-secondary.active { | ||||
|     background-color: var(--pngx-bg-darker) !important; | ||||
|     color: var(--pngx-body-color-accent) !important; | ||||
|   } | ||||
|  | ||||
|   .btn-outline-danger { | ||||
|     border-color: var(--pngx-bg-alt) !important; | ||||
|     background-color: var(--pngx-bg-alt) !important; | ||||
|     color: var(--bs-danger) !important; | ||||
|  | ||||
|     &:hover, &:focus, &.active, &:active { | ||||
|       background-color: var(--pngx-bg-darker) !important; | ||||
|       color: var(--bs-danger) !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .form-control:not(.btn), input, select, textarea, | ||||
|   .form-select:not(.is-invalid):not(:disabled), .form-check-input, | ||||
|   .ng-select .ng-select-container { | ||||
|     background-color: var(--pngx-bg-darker) !important; | ||||
|     color: var(--bs-body-color) !important; | ||||
|     border-color: var(--pngx-bg-alt) !important; | ||||
|   } | ||||
|  | ||||
|   .input-group .input-group-text { | ||||
|     background-color: var(--pngx-bg-alt); | ||||
|     color: var(--bs-body-color); | ||||
|     border-color: var(--pngx-bg-alt); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -324,7 +324,6 @@ INSTALLED_APPS = [ | ||||
|     "paperless_tesseract.apps.PaperlessTesseractConfig", | ||||
|     "paperless_text.apps.PaperlessTextConfig", | ||||
|     "paperless_mail.apps.PaperlessMailConfig", | ||||
|     "paperless_remote.apps.PaperlessRemoteParserConfig", | ||||
|     "django.contrib.admin", | ||||
|     "rest_framework", | ||||
|     "rest_framework.authtoken", | ||||
| @@ -1444,10 +1443,3 @@ WEBHOOKS_ALLOW_INTERNAL_REQUESTS = __get_boolean( | ||||
|     "PAPERLESS_WEBHOOKS_ALLOW_INTERNAL_REQUESTS", | ||||
|     "true", | ||||
| ) | ||||
|  | ||||
| ############################################################################### | ||||
| # Remote Parser                                                               # | ||||
| ############################################################################### | ||||
| REMOTE_OCR_ENGINE = os.getenv("PAPERLESS_REMOTE_OCR_ENGINE") | ||||
| REMOTE_OCR_API_KEY = os.getenv("PAPERLESS_REMOTE_OCR_API_KEY") | ||||
| REMOTE_OCR_ENDPOINT = os.getenv("PAPERLESS_REMOTE_OCR_ENDPOINT") | ||||
|   | ||||
| @@ -1,4 +0,0 @@ | ||||
| # this is here so that django finds the checks. | ||||
| from paperless_remote.checks import check_remote_parser_configured | ||||
|  | ||||
| __all__ = ["check_remote_parser_configured"] | ||||
| @@ -1,14 +0,0 @@ | ||||
| from django.apps import AppConfig | ||||
|  | ||||
| from paperless_remote.signals import remote_consumer_declaration | ||||
|  | ||||
|  | ||||
| class PaperlessRemoteParserConfig(AppConfig): | ||||
|     name = "paperless_remote" | ||||
|  | ||||
|     def ready(self): | ||||
|         from documents.signals import document_consumer_declaration | ||||
|  | ||||
|         document_consumer_declaration.connect(remote_consumer_declaration) | ||||
|  | ||||
|         AppConfig.ready(self) | ||||
| @@ -1,15 +0,0 @@ | ||||
| from django.conf import settings | ||||
| from django.core.checks import Error | ||||
| from django.core.checks import register | ||||
|  | ||||
|  | ||||
| @register() | ||||
| def check_remote_parser_configured(app_configs, **kwargs): | ||||
|     if settings.REMOTE_OCR_ENGINE == "azureai" and not settings.REMOTE_OCR_ENDPOINT: | ||||
|         return [ | ||||
|             Error( | ||||
|                 "Azure AI remote parser requires endpoint to be configured.", | ||||
|             ), | ||||
|         ] | ||||
|  | ||||
|     return [] | ||||
| @@ -1,113 +0,0 @@ | ||||
| from pathlib import Path | ||||
|  | ||||
| from django.conf import settings | ||||
|  | ||||
| from paperless_tesseract.parsers import RasterisedDocumentParser | ||||
|  | ||||
|  | ||||
| class RemoteEngineConfig: | ||||
|     def __init__( | ||||
|         self, | ||||
|         engine: str, | ||||
|         api_key: str | None = None, | ||||
|         endpoint: str | None = None, | ||||
|     ): | ||||
|         self.engine = engine | ||||
|         self.api_key = api_key | ||||
|         self.endpoint = endpoint | ||||
|  | ||||
|     def engine_is_valid(self): | ||||
|         valid = self.engine in ["azureai"] and self.api_key is not None | ||||
|         if self.engine == "azureai": | ||||
|             valid = valid and self.endpoint is not None | ||||
|         return valid | ||||
|  | ||||
|  | ||||
| class RemoteDocumentParser(RasterisedDocumentParser): | ||||
|     """ | ||||
|     This parser uses a remote OCR engine to parse documents. Currently, it supports Azure AI Vision | ||||
|     as this is the only service that provides a remote OCR API with text-embedded PDF output. | ||||
|     """ | ||||
|  | ||||
|     logging_name = "paperless.parsing.remote" | ||||
|  | ||||
|     def get_settings(self) -> RemoteEngineConfig: | ||||
|         """ | ||||
|         Returns the configuration for the remote OCR engine, loaded from Django settings. | ||||
|         """ | ||||
|         return RemoteEngineConfig( | ||||
|             engine=settings.REMOTE_OCR_ENGINE, | ||||
|             api_key=settings.REMOTE_OCR_API_KEY, | ||||
|             endpoint=settings.REMOTE_OCR_ENDPOINT, | ||||
|         ) | ||||
|  | ||||
|     def supported_mime_types(self): | ||||
|         if self.settings.engine_is_valid(): | ||||
|             return { | ||||
|                 "application/pdf": ".pdf", | ||||
|                 "image/png": ".png", | ||||
|                 "image/jpeg": ".jpg", | ||||
|                 "image/tiff": ".tiff", | ||||
|                 "image/bmp": ".bmp", | ||||
|                 "image/gif": ".gif", | ||||
|                 "image/webp": ".webp", | ||||
|             } | ||||
|         else: | ||||
|             return {} | ||||
|  | ||||
|     def azure_ai_vision_parse( | ||||
|         self, | ||||
|         file: Path, | ||||
|     ) -> str | None: | ||||
|         """ | ||||
|         Uses Azure AI Vision to parse the document and return the text content. | ||||
|         It requests a searchable PDF output with embedded text. | ||||
|         The PDF is saved to the archive_path attribute. | ||||
|         Returns the text content extracted from the document. | ||||
|         If the parsing fails, it returns None. | ||||
|         """ | ||||
|         from azure.ai.documentintelligence import DocumentIntelligenceClient | ||||
|         from azure.ai.documentintelligence.models import AnalyzeDocumentRequest | ||||
|         from azure.ai.documentintelligence.models import AnalyzeOutputOption | ||||
|         from azure.ai.documentintelligence.models import DocumentContentFormat | ||||
|         from azure.core.credentials import AzureKeyCredential | ||||
|  | ||||
|         client = DocumentIntelligenceClient( | ||||
|             endpoint=self.settings.endpoint, | ||||
|             credential=AzureKeyCredential(self.settings.api_key), | ||||
|         ) | ||||
|  | ||||
|         with file.open("rb") as f: | ||||
|             analyze_request = AnalyzeDocumentRequest(bytes_source=f.read()) | ||||
|             poller = client.begin_analyze_document( | ||||
|                 model_id="prebuilt-read", | ||||
|                 body=analyze_request, | ||||
|                 output_content_format=DocumentContentFormat.TEXT, | ||||
|                 output=[AnalyzeOutputOption.PDF],  # request searchable PDF output | ||||
|                 content_type="application/json", | ||||
|             ) | ||||
|  | ||||
|         poller.wait() | ||||
|         result_id = poller.details["operation_id"] | ||||
|         result = poller.result() | ||||
|  | ||||
|         # Download the PDF with embedded text | ||||
|         self.archive_path = Path(self.tempdir) / "archive.pdf" | ||||
|         with self.archive_path.open("wb") as f: | ||||
|             for chunk in client.get_analyze_result_pdf( | ||||
|                 model_id="prebuilt-read", | ||||
|                 result_id=result_id, | ||||
|             ): | ||||
|                 f.write(chunk) | ||||
|  | ||||
|         return result.content | ||||
|  | ||||
|     def parse(self, document_path: Path, mime_type, file_name=None): | ||||
|         if not self.settings.engine_is_valid(): | ||||
|             self.log.warning( | ||||
|                 "No valid remote parser engine is configured, content will be empty.", | ||||
|             ) | ||||
|             self.text = "" | ||||
|             return | ||||
|         elif self.settings.engine == "azureai": | ||||
|             self.text = self.azure_ai_vision_parse(document_path) | ||||
| @@ -1,18 +0,0 @@ | ||||
| def get_parser(*args, **kwargs): | ||||
|     from paperless_remote.parsers import RemoteDocumentParser | ||||
|  | ||||
|     return RemoteDocumentParser(*args, **kwargs) | ||||
|  | ||||
|  | ||||
| def get_supported_mime_types(): | ||||
|     from paperless_remote.parsers import RemoteDocumentParser | ||||
|  | ||||
|     return RemoteDocumentParser(None).supported_mime_types() | ||||
|  | ||||
|  | ||||
| def remote_consumer_declaration(sender, **kwargs): | ||||
|     return { | ||||
|         "parser": get_parser, | ||||
|         "weight": 5, | ||||
|         "mime_types": get_supported_mime_types(), | ||||
|     } | ||||
										
											Binary file not shown.
										
									
								
							| @@ -1,29 +0,0 @@ | ||||
| from django.test import TestCase | ||||
| from django.test import override_settings | ||||
|  | ||||
| from paperless_remote import check_remote_parser_configured | ||||
|  | ||||
|  | ||||
| class TestChecks(TestCase): | ||||
|     @override_settings(REMOTE_OCR_ENGINE=None) | ||||
|     def test_no_engine(self): | ||||
|         msgs = check_remote_parser_configured(None) | ||||
|         self.assertEqual(len(msgs), 0) | ||||
|  | ||||
|     @override_settings(REMOTE_OCR_ENGINE="azureai") | ||||
|     @override_settings(REMOTE_OCR_API_KEY="somekey") | ||||
|     @override_settings(REMOTE_OCR_ENDPOINT=None) | ||||
|     def test_azure_no_endpoint(self): | ||||
|         msgs = check_remote_parser_configured(None) | ||||
|         self.assertEqual(len(msgs), 1) | ||||
|         self.assertTrue( | ||||
|             msgs[0].msg.startswith( | ||||
|                 "Azure AI remote parser requires endpoint to be configured.", | ||||
|             ), | ||||
|         ) | ||||
|  | ||||
|     @override_settings(REMOTE_OCR_ENGINE="something") | ||||
|     @override_settings(REMOTE_OCR_API_KEY="somekey") | ||||
|     def test_valid_configuration(self): | ||||
|         msgs = check_remote_parser_configured(None) | ||||
|         self.assertEqual(len(msgs), 0) | ||||
| @@ -1,101 +0,0 @@ | ||||
| import uuid | ||||
| from pathlib import Path | ||||
| from unittest import mock | ||||
|  | ||||
| from django.test import TestCase | ||||
| from django.test import override_settings | ||||
|  | ||||
| from documents.tests.utils import DirectoriesMixin | ||||
| from documents.tests.utils import FileSystemAssertsMixin | ||||
| from paperless_remote.parsers import RemoteDocumentParser | ||||
| from paperless_remote.signals import get_parser | ||||
|  | ||||
|  | ||||
| class TestParser(DirectoriesMixin, FileSystemAssertsMixin, TestCase): | ||||
|     SAMPLE_FILES = Path(__file__).resolve().parent / "samples" | ||||
|  | ||||
|     def assertContainsStrings(self, content, strings): | ||||
|         # Asserts that all strings appear in content, in the given order. | ||||
|         indices = [] | ||||
|         for s in strings: | ||||
|             if s in content: | ||||
|                 indices.append(content.index(s)) | ||||
|             else: | ||||
|                 self.fail(f"'{s}' is not in '{content}'") | ||||
|         self.assertListEqual(indices, sorted(indices)) | ||||
|  | ||||
|     @mock.patch("paperless_tesseract.parsers.run_subprocess") | ||||
|     @mock.patch("azure.ai.documentintelligence.DocumentIntelligenceClient") | ||||
|     def test_get_text_with_azure(self, mock_client_cls, mock_subprocess): | ||||
|         # Arrange mock Azure client | ||||
|         mock_client = mock.Mock() | ||||
|         mock_client_cls.return_value = mock_client | ||||
|  | ||||
|         # Simulate poller result and its `.details` | ||||
|         mock_poller = mock.Mock() | ||||
|         mock_poller.wait.return_value = None | ||||
|         mock_poller.details = {"operation_id": "fake-op-id"} | ||||
|         mock_client.begin_analyze_document.return_value = mock_poller | ||||
|         mock_poller.result.return_value.content = "This is a test document." | ||||
|  | ||||
|         # Return dummy PDF bytes | ||||
|         mock_client.get_analyze_result_pdf.return_value = [ | ||||
|             b"%PDF-", | ||||
|             b"1.7 ", | ||||
|             b"FAKEPDF", | ||||
|         ] | ||||
|  | ||||
|         # Simulate pdftotext by writing dummy text to sidecar file | ||||
|         def fake_run(cmd, *args, **kwargs): | ||||
|             with Path(cmd[-1]).open("w", encoding="utf-8") as f: | ||||
|                 f.write("This is a test document.") | ||||
|  | ||||
|         mock_subprocess.side_effect = fake_run | ||||
|  | ||||
|         with override_settings( | ||||
|             REMOTE_OCR_ENGINE="azureai", | ||||
|             REMOTE_OCR_API_KEY="somekey", | ||||
|             REMOTE_OCR_ENDPOINT="https://endpoint.cognitiveservices.azure.com", | ||||
|         ): | ||||
|             parser = get_parser(uuid.uuid4()) | ||||
|             parser.parse( | ||||
|                 self.SAMPLE_FILES / "simple-digital.pdf", | ||||
|                 "application/pdf", | ||||
|             ) | ||||
|  | ||||
|             self.assertContainsStrings( | ||||
|                 parser.text.strip(), | ||||
|                 ["This is a test document."], | ||||
|             ) | ||||
|  | ||||
|     @override_settings( | ||||
|         REMOTE_OCR_ENGINE="azureai", | ||||
|         REMOTE_OCR_API_KEY="key", | ||||
|         REMOTE_OCR_ENDPOINT="https://endpoint.cognitiveservices.azure.com", | ||||
|     ) | ||||
|     def test_supported_mime_types_valid_config(self): | ||||
|         parser = RemoteDocumentParser(uuid.uuid4()) | ||||
|         expected_types = { | ||||
|             "application/pdf": ".pdf", | ||||
|             "image/png": ".png", | ||||
|             "image/jpeg": ".jpg", | ||||
|             "image/tiff": ".tiff", | ||||
|             "image/bmp": ".bmp", | ||||
|             "image/gif": ".gif", | ||||
|             "image/webp": ".webp", | ||||
|         } | ||||
|         self.assertEqual(parser.supported_mime_types(), expected_types) | ||||
|  | ||||
|     def test_supported_mime_types_invalid_config(self): | ||||
|         parser = get_parser(uuid.uuid4()) | ||||
|         self.assertEqual(parser.supported_mime_types(), {}) | ||||
|  | ||||
|     @override_settings( | ||||
|         REMOTE_OCR_ENGINE=None, | ||||
|         REMOTE_OCR_API_KEY=None, | ||||
|         REMOTE_OCR_ENDPOINT=None, | ||||
|     ) | ||||
|     def test_parse_with_invalid_config(self): | ||||
|         parser = get_parser(uuid.uuid4()) | ||||
|         parser.parse(self.SAMPLE_FILES / "simple-digital.pdf", "application/pdf") | ||||
|         self.assertEqual(parser.text, "") | ||||
							
								
								
									
										96
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										96
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,5 +1,5 @@ | ||||
| version = 1 | ||||
| revision = 2 | ||||
| revision = 3 | ||||
| requires-python = ">=3.10" | ||||
| resolution-markers = [ | ||||
|     "python_full_version >= '3.11' and sys_platform == 'darwin'", | ||||
| @@ -95,34 +95,6 @@ wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/af/cc/55a32a2c98022d88812b5986d2a92c4ff3ee087e83b712ebc703bba452bf/Automat-24.8.1-py3-none-any.whl", hash = "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a", size = 42585, upload-time = "2024-08-19T17:31:56.729Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "azure-ai-documentintelligence" | ||||
| version = "1.0.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "azure-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "isodate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/44/7b/8115cd713e2caa5e44def85f2b7ebd02a74ae74d7113ba20bdd41fd6dd80/azure_ai_documentintelligence-1.0.2.tar.gz", hash = "sha256:4d75a2513f2839365ebabc0e0e1772f5601b3a8c9a71e75da12440da13b63484", size = 170940 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/d9/75/c9ec040f23082f54ffb1977ff8f364c2d21c79a640a13d1c1809e7fd6b1a/azure_ai_documentintelligence-1.0.2-py3-none-any.whl", hash = "sha256:e1fb446abbdeccc9759d897898a0fe13141ed29f9ad11fc705f951925822ed59", size = 106005 }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "azure-core" | ||||
| version = "1.33.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/75/aa/7c9db8edd626f1a7d99d09ef7926f6f4fb34d5f9fa00dc394afdfe8e2a80/azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9", size = 295633 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/07/b7/76b7e144aa53bd206bf1ce34fa75350472c3f69bf30e5c8c18bc9881035d/azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f", size = 207071 }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "babel" | ||||
| version = "2.17.0" | ||||
| @@ -1430,15 +1402,6 @@ wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/c7/fc/4e5a141c3f7c7bed550ac1f69e599e92b6be449dd4677ec09f325cad0955/inotifyrecursive-0.3.5-py3-none-any.whl", hash = "sha256:7e5f4a2e1dc2bef0efa3b5f6b339c41fb4599055a2b54909d020e9e932cc8d2f", size = 8009, upload-time = "2020-11-20T12:38:46.981Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "isodate" | ||||
| version = "0.7.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "jinja2" | ||||
| version = "3.1.6" | ||||
| @@ -1724,11 +1687,12 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "mkdocs-material" | ||||
| version = "9.6.16" | ||||
| version = "9.6.17" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "backrefs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "markdown", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
| @@ -1739,9 +1703,9 @@ dependencies = [ | ||||
|     { name = "pymdown-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/dd/84/aec27a468c5e8c27689c71b516fb5a0d10b8fca45b9ad2dd9d6e43bc4296/mkdocs_material-9.6.16.tar.gz", hash = "sha256:d07011df4a5c02ee0877496d9f1bfc986cfb93d964799b032dd99fe34c0e9d19", size = 4028828, upload-time = "2025-07-26T15:53:47.542Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/47/02/51115cdda743e1551c5c13bdfaaf8c46b959acc57ba914d8ec479dd2fe1f/mkdocs_material-9.6.17.tar.gz", hash = "sha256:48ae7aec72a3f9f501a70be3fbd329c96ff5f5a385b67a1563e5ed5ce064affe", size = 4032898, upload-time = "2025-08-15T16:09:21.412Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/65/f4/90ad67125b4dd66e7884e4dbdfab82e3679eb92b751116f8bb25ccfe2f0c/mkdocs_material-9.6.16-py3-none-any.whl", hash = "sha256:8d1a1282b892fe1fdf77bfeb08c485ba3909dd743c9ba69a19a40f637c6ec18c", size = 9223743, upload-time = "2025-07-26T15:53:44.236Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3c/7c/0f0d44c92c8f3068930da495b752244bd59fd87b5b0f9571fa2d2a93aee7/mkdocs_material-9.6.17-py3-none-any.whl", hash = "sha256:221dd8b37a63f52e580bcab4a7e0290e4a6f59bd66190be9c3d40767e05f9417", size = 9229230, upload-time = "2025-08-15T16:09:18.301Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -2046,7 +2010,6 @@ name = "paperless-ngx" | ||||
| version = "2.18.1" | ||||
| source = { virtual = "." } | ||||
| dependencies = [ | ||||
|     { name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "celery", extra = ["redis"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
| @@ -2181,8 +2144,7 @@ typing = [ | ||||
|  | ||||
| [package.metadata] | ||||
| requires-dist = [ | ||||
|     { name = "azure-ai-documentintelligence", specifier = ">=1.0.2" }, | ||||
|     { name = "babel", specifier = ">=2.17.0" }, | ||||
|     { name = "babel", specifier = ">=2.17" }, | ||||
|     { name = "bleach", specifier = "~=6.2.0" }, | ||||
|     { name = "celery", extras = ["redis"], specifier = "~=5.5.1" }, | ||||
|     { name = "channels", specifier = "~=4.2" }, | ||||
| @@ -2254,7 +2216,7 @@ dev = [ | ||||
|     { name = "imagehash" }, | ||||
|     { name = "mkdocs-glightbox", specifier = "~=0.4.0" }, | ||||
|     { name = "mkdocs-material", specifier = "~=9.6.4" }, | ||||
|     { name = "pre-commit", specifier = "~=4.2.0" }, | ||||
|     { name = "pre-commit", specifier = "~=4.3.0" }, | ||||
|     { name = "pre-commit-uv", specifier = "~=4.1.3" }, | ||||
|     { name = "pytest", specifier = "~=8.4.1" }, | ||||
|     { name = "pytest-cov", specifier = "~=6.2.1" }, | ||||
| @@ -2272,7 +2234,7 @@ docs = [ | ||||
|     { name = "mkdocs-material", specifier = "~=9.6.4" }, | ||||
| ] | ||||
| lint = [ | ||||
|     { name = "pre-commit", specifier = "~=4.2.0" }, | ||||
|     { name = "pre-commit", specifier = "~=4.3.0" }, | ||||
|     { name = "pre-commit-uv", specifier = "~=4.1.3" }, | ||||
|     { name = "ruff", specifier = "~=0.12.2" }, | ||||
| ] | ||||
| @@ -2460,6 +2422,9 @@ wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, | ||||
| @@ -2535,7 +2500,7 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "pre-commit" | ||||
| version = "4.2.0" | ||||
| version = "4.3.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "cfgv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
| @@ -2544,9 +2509,9 @@ dependencies = [ | ||||
|     { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
|     { name = "virtualenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| @@ -3288,24 +3253,25 @@ wheels = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "ruff" | ||||
| version = "0.12.8" | ||||
| version = "0.12.9" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
|   | ||||
		Reference in New Issue
	
	Block a user