mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-19 21:03:21 -07:00
Merge branch 'nightly' into dependabot/pip/dnspython-2.6.1
This commit is contained in:
commit
5cbf0dfe7d
534 changed files with 63946 additions and 23064 deletions
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
|
@ -27,12 +27,12 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
config-file: ./.github/codeql-config.yml
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
|
4
.github/workflows/issues-stale.yml
vendored
4
.github/workflows/issues-stale.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale
|
||||
uses: actions/stale@v8
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: >
|
||||
This issue is stale because it has been open for 30 days with no activity.
|
||||
|
@ -30,7 +30,7 @@ jobs:
|
|||
days-before-close: 5
|
||||
|
||||
- name: Invalid Template
|
||||
uses: actions/stale@v8
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: >
|
||||
Invalid issues template.
|
||||
|
|
2
.github/workflows/issues.yml
vendored
2
.github/workflows/issues.yml
vendored
|
@ -10,6 +10,6 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Label Issues
|
||||
uses: dessant/label-actions@v3
|
||||
uses: dessant/label-actions@v4
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
|
|
17
.github/workflows/publish-docker.yml
vendored
17
.github/workflows/publish-docker.yml
vendored
|
@ -47,7 +47,7 @@ jobs:
|
|||
version: latest
|
||||
|
||||
- name: Cache Docker Layers
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
|
@ -94,23 +94,10 @@ jobs:
|
|||
if: always() && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
failures=(neutral, skipped, timed_out, action_required)
|
||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||
echo "status=failure" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ steps.status.outputs.status }}
|
||||
status: ${{ needs.build-docker.result == 'success' && 'success' || contains(needs.*.result, 'failure') && 'failure' || 'cancelled' }}
|
||||
title: ${{ github.workflow }}
|
||||
nofail: true
|
||||
|
|
47
.github/workflows/publish-installers.yml
vendored
47
.github/workflows/publish-installers.yml
vendored
|
@ -6,10 +6,13 @@ on:
|
|||
branches: [master, beta, nightly]
|
||||
tags: [v*]
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: '3.11'
|
||||
|
||||
jobs:
|
||||
build-installer:
|
||||
name: Build ${{ matrix.os_upper }} Installer
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
runs-on: ${{ matrix.os }}-${{ matrix.os_version }}
|
||||
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
@ -17,9 +20,13 @@ jobs:
|
|||
include:
|
||||
- os: 'windows'
|
||||
os_upper: 'Windows'
|
||||
os_version: 'latest'
|
||||
arch: 'x64'
|
||||
ext: 'exe'
|
||||
- os: 'macos'
|
||||
os_upper: 'MacOS'
|
||||
os_version: '14'
|
||||
arch: 'universal'
|
||||
ext: 'pkg'
|
||||
|
||||
steps:
|
||||
|
@ -52,16 +59,16 @@ jobs:
|
|||
echo $GITHUB_SHA > version.txt
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v4
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.9'
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: pip
|
||||
cache-dependency-path: '**/requirements*.txt'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r package/requirements-package.txt
|
||||
pip install -r package/requirements-package.txt --no-binary cffi
|
||||
|
||||
- name: Build Package
|
||||
run: |
|
||||
|
@ -74,7 +81,7 @@ jobs:
|
|||
script-file: ./package/Tautulli.nsi
|
||||
arguments: >
|
||||
/DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }}
|
||||
/DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
|
||||
/DINSTALLER_NAME=..\Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-${{ matrix.arch }}.${{ matrix.ext }}
|
||||
additional-plugin-paths: package/nsis-plugins
|
||||
|
||||
- name: Create MacOS Installer
|
||||
|
@ -85,13 +92,13 @@ jobs:
|
|||
--version ${{ steps.get_version.outputs.VERSION }} \
|
||||
--component ./dist/Tautulli.app \
|
||||
--scripts ./package/macos-scripts \
|
||||
Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
|
||||
Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-${{ matrix.arch }}.${{ matrix.ext }}
|
||||
|
||||
- name: Upload Installer
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Tautulli-${{ matrix.os }}-installer
|
||||
path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.${{ matrix.ext }}
|
||||
path: Tautulli-${{ matrix.os }}-${{ steps.get_version.outputs.RELEASE_VERSION }}-${{ matrix.arch }}.${{ matrix.ext }}
|
||||
|
||||
release:
|
||||
name: Release Installers
|
||||
|
@ -99,9 +106,6 @@ jobs:
|
|||
if: always() && startsWith(github.ref, 'refs/tags/') && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
@ -111,8 +115,8 @@ jobs:
|
|||
echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Download Installers
|
||||
if: env.WORKFLOW_CONCLUSION == 'success'
|
||||
uses: actions/download-artifact@v3
|
||||
if: needs.build-installer.result == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Get Changelog
|
||||
id: get_changelog
|
||||
|
@ -125,7 +129,7 @@ jobs:
|
|||
echo "$EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHACTIONS_TOKEN }}
|
||||
|
@ -147,23 +151,10 @@ jobs:
|
|||
if: always() && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
failures=(neutral, skipped, timed_out, action_required)
|
||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||
echo "status=failure" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ steps.status.outputs.status }}
|
||||
status: ${{ needs.build-installer.result == 'success' && 'success' || contains(needs.*.result, 'failure') && 'failure' || 'cancelled' }}
|
||||
title: ${{ github.workflow }}
|
||||
nofail: true
|
||||
|
|
17
.github/workflows/publish-snap.yml
vendored
17
.github/workflows/publish-snap.yml
vendored
|
@ -44,7 +44,7 @@ jobs:
|
|||
architecture: ${{ matrix.architecture }}
|
||||
|
||||
- name: Upload Snap Package
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Tautulli-snap-package-${{ matrix.architecture }}
|
||||
path: ${{ steps.build.outputs.snap }}
|
||||
|
@ -69,23 +69,10 @@ jobs:
|
|||
if: always() && !contains(github.event.head_commit.message, '[skip ci]')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get Build Job Status
|
||||
uses: technote-space/workflow-conclusion-action@v3
|
||||
|
||||
- name: Combine Job Status
|
||||
id: status
|
||||
run: |
|
||||
failures=(neutral, skipped, timed_out, action_required)
|
||||
if [[ ${array[@]} =~ $WORKFLOW_CONCLUSION ]]; then
|
||||
echo "status=failure" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=$WORKFLOW_CONCLUSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Post Status to Discord
|
||||
uses: sarisia/actions-status-discord@v1
|
||||
with:
|
||||
webhook: ${{ secrets.DISCORD_WEBHOOK }}
|
||||
status: ${{ steps.status.outputs.status }}
|
||||
status: ${{ needs.build-snap.result == 'success' && 'success' || contains(needs.*.result, 'failure') && 'failure' || 'cancelled' }}
|
||||
title: ${{ github.workflow }}
|
||||
nofail: true
|
||||
|
|
|
@ -123,11 +123,6 @@
|
|||
% else:
|
||||
<li><a href="graphs">Graphs</a></li>
|
||||
% endif
|
||||
% if title == "Synced Items":
|
||||
<li class="active"><a href="sync">Synced Items</a></li>
|
||||
% else:
|
||||
<li><a href="sync">Synced Items</a></li>
|
||||
% endif
|
||||
% if title == "Settings":
|
||||
<li class="dropdown active">
|
||||
% else:
|
||||
|
|
|
@ -369,7 +369,7 @@ DOCUMENTATION :: END
|
|||
% if data['media_type'] != 'photo':
|
||||
<div class="dashboard-activity-info-time">
|
||||
% if data['live']:
|
||||
<br /><span class="thumb-tooltip dashboard-activity-info-channel" data-toggle="popover" data-img="${data['channel_thumb']}" data-height="40" data-width="40">${data['channel_call_sign']} ${data['channel_identifier']}</span>
|
||||
<br /><span class="thumb-tooltip dashboard-activity-info-channel" data-toggle="popover" data-img="${data['channel_thumb']}" data-height="40" data-width="40">${data['channel_title'] or (data['channel_vcn'] + ' ' + data['channel_call_sign'])}</span>
|
||||
% elif data['view_offset']:
|
||||
ETA:
|
||||
<span id="stream-eta-${sk}">
|
||||
|
|
|
@ -408,8 +408,8 @@ DOCUMENTATION :: END
|
|||
% endif
|
||||
</div>
|
||||
<div class="summary-content-details-tag" id="channel-icon">
|
||||
% if media_info['channel_identifier']:
|
||||
Channel <strong> <span class="thumb-tooltip" data-toggle="popover" data-img="${media_info['channel_thumb']}" data-height="40" data-width="40">${media_info['channel_call_sign']} ${media_info['channel_identifier']}</span> </strong>
|
||||
% if media_info['channel_vcn']:
|
||||
Channel <strong> <span class="thumb-tooltip" data-toggle="popover" data-img="${media_info['channel_thumb']}" data-height="40" data-width="40">${media_info['channel_title'] or (media_info['channel_vcn'] + ' ' + media_info['channel_call_sign'])}</span> </strong>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
|
@ -878,7 +878,7 @@ DOCUMENTATION :: END
|
|||
transcode_decision: transcode_decision,
|
||||
user_id: "${history_user_id}",
|
||||
% if data['live']:
|
||||
guid: "${data['guid']}
|
||||
guid: "${data['guid']}"
|
||||
% elif data['media_type'] in ('show', 'artist'):
|
||||
grandparent_rating_key: "${data['rating_key']}"
|
||||
% elif data['media_type'] in ('season', 'album'):
|
||||
|
@ -947,8 +947,12 @@ DOCUMENTATION :: END
|
|||
url: 'item_watch_time_stats',
|
||||
async: true,
|
||||
data: {
|
||||
% if data['live']:
|
||||
guid: "${data['guid']}"
|
||||
% else:
|
||||
rating_key: "${data['rating_key']}",
|
||||
media_type: "${data['media_type']}"
|
||||
media_type: "${data['media_type']}"
|
||||
% endif
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#watch-time-stats").html(xhr.responseText);
|
||||
|
@ -959,8 +963,12 @@ DOCUMENTATION :: END
|
|||
url: 'item_user_stats',
|
||||
async: true,
|
||||
data: {
|
||||
% if data['live']:
|
||||
guid: "${data['guid']}"
|
||||
% else:
|
||||
rating_key: "${data['rating_key']}",
|
||||
media_type: "${data['media_type']}"
|
||||
media_type: "${data['media_type']}"
|
||||
% endif
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#user-stats").html(xhr.responseText);
|
||||
|
|
|
@ -76,7 +76,6 @@ DOCUMENTATION :: END
|
|||
% if _session['user_group'] == 'admin':
|
||||
<li><a id="nav-tabs-export" href="#tabs-export" role="tab" data-toggle="tab">Export</a></li>
|
||||
% endif
|
||||
<li><a id="nav-tabs-synceditems" href="#tabs-synceditems" role="tab" data-toggle="tab">Synced Items</a></li>
|
||||
<li><a id="nav-tabs-ipaddresses" href="#tabs-ipaddresses" role="tab" data-toggle="tab">IP Addresses</a></li>
|
||||
<li><a id="nav-tabs-tautullilogins" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
|
||||
</ul>
|
||||
|
@ -316,57 +315,6 @@ DOCUMENTATION :: END
|
|||
</div>
|
||||
</div>
|
||||
% endif
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-synceditems">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>
|
||||
<i class="fa fa-cloud-download"></i> Synced Items for <strong>
|
||||
<span class="set-username">${data['friendly_name']}</span>
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="sync-row-edit-mode">
|
||||
<i class="fa fa-trash-o"></i> Delete mode
|
||||
</button>
|
||||
</div>
|
||||
% endif
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark refresh-syncs-button" id="refresh-syncs-list"><i class="fa fa-refresh"></i> Refresh synced items</button>
|
||||
</div>
|
||||
<div class="btn-group colvis-button-bar" id="button-bar-sync"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display sync_table" id="sync_table-UID-${data['user_id']}" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left" id="delete_row">Delete</th>
|
||||
<th align="left" id="state">State</th>
|
||||
<th align="left" id="username">Username</th>
|
||||
<th align="left" id="sync_title">Title</th>
|
||||
<th align="left" id="type">Type</th>
|
||||
<th align="left" id="sync_platform">Platform</th>
|
||||
<th align="left" id="device">Device</th>
|
||||
<th align="left" id="size">Total Size</th>
|
||||
<th align="left" id="items">Total Items</th>
|
||||
<th align="left" id="converted">Converted</th>
|
||||
<th align="left" id="downloaded">Downloaded</th>
|
||||
<th align="left" id="sync_percent_complete">Complete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-ipaddresses">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
|
@ -642,12 +590,6 @@ DOCUMENTATION :: END
|
|||
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
|
||||
}
|
||||
|
||||
$('#nav-tabs-synceditems').on('shown.bs.tab', function() {
|
||||
if (typeof(sync_table) === 'undefined') {
|
||||
loadSyncTable(user_id);
|
||||
}
|
||||
});
|
||||
|
||||
$("#refresh-syncs-list").click(function() {
|
||||
sync_table.ajax.reload();
|
||||
});
|
||||
|
|
396
lib/annotated_types/__init__.py
Normal file
396
lib/annotated_types/__init__.py
Normal file
|
@ -0,0 +1,396 @@
|
|||
import math
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import timezone
|
||||
from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
from typing_extensions import Protocol, runtime_checkable
|
||||
else:
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated, Literal
|
||||
else:
|
||||
from typing import Annotated, Literal
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
EllipsisType = type(Ellipsis)
|
||||
KW_ONLY = {}
|
||||
SLOTS = {}
|
||||
else:
|
||||
from types import EllipsisType
|
||||
|
||||
KW_ONLY = {"kw_only": True}
|
||||
SLOTS = {"slots": True}
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BaseMetadata',
|
||||
'GroupedMetadata',
|
||||
'Gt',
|
||||
'Ge',
|
||||
'Lt',
|
||||
'Le',
|
||||
'Interval',
|
||||
'MultipleOf',
|
||||
'MinLen',
|
||||
'MaxLen',
|
||||
'Len',
|
||||
'Timezone',
|
||||
'Predicate',
|
||||
'LowerCase',
|
||||
'UpperCase',
|
||||
'IsDigits',
|
||||
'IsFinite',
|
||||
'IsNotFinite',
|
||||
'IsNan',
|
||||
'IsNotNan',
|
||||
'IsInfinite',
|
||||
'IsNotInfinite',
|
||||
'doc',
|
||||
'DocInfo',
|
||||
'__version__',
|
||||
)
|
||||
|
||||
__version__ = '0.6.0'
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
# arguments that start with __ are considered
|
||||
# positional only
|
||||
# see https://peps.python.org/pep-0484/#positional-only-arguments
|
||||
|
||||
|
||||
class SupportsGt(Protocol):
|
||||
def __gt__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsGe(Protocol):
|
||||
def __ge__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsLt(Protocol):
|
||||
def __lt__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsLe(Protocol):
|
||||
def __le__(self: T, __other: T) -> bool:
|
||||
...
|
||||
|
||||
|
||||
class SupportsMod(Protocol):
|
||||
def __mod__(self: T, __other: T) -> T:
|
||||
...
|
||||
|
||||
|
||||
class SupportsDiv(Protocol):
|
||||
def __div__(self: T, __other: T) -> T:
|
||||
...
|
||||
|
||||
|
||||
class BaseMetadata:
|
||||
"""Base class for all metadata.
|
||||
|
||||
This exists mainly so that implementers
|
||||
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Gt(BaseMetadata):
|
||||
"""Gt(gt=x) implies that the value must be greater than x.
|
||||
|
||||
It can be used with any type that supports the ``>`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
gt: SupportsGt
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Ge(BaseMetadata):
|
||||
"""Ge(ge=x) implies that the value must be greater than or equal to x.
|
||||
|
||||
It can be used with any type that supports the ``>=`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
ge: SupportsGe
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Lt(BaseMetadata):
|
||||
"""Lt(lt=x) implies that the value must be less than x.
|
||||
|
||||
It can be used with any type that supports the ``<`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
lt: SupportsLt
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Le(BaseMetadata):
|
||||
"""Le(le=x) implies that the value must be less than or equal to x.
|
||||
|
||||
It can be used with any type that supports the ``<=`` operator,
|
||||
including numbers, dates and times, strings, sets, and so on.
|
||||
"""
|
||||
|
||||
le: SupportsLe
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class GroupedMetadata(Protocol):
|
||||
"""A grouping of multiple BaseMetadata objects.
|
||||
|
||||
`GroupedMetadata` on its own is not metadata and has no meaning.
|
||||
All it the the constraint and metadata should be fully expressable
|
||||
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
|
||||
|
||||
Concrete implementations should override `GroupedMetadata.__iter__()`
|
||||
to add their own metadata.
|
||||
For example:
|
||||
|
||||
>>> @dataclass
|
||||
>>> class Field(GroupedMetadata):
|
||||
>>> gt: float | None = None
|
||||
>>> description: str | None = None
|
||||
...
|
||||
>>> def __iter__(self) -> Iterable[BaseMetadata]:
|
||||
>>> if self.gt is not None:
|
||||
>>> yield Gt(self.gt)
|
||||
>>> if self.description is not None:
|
||||
>>> yield Description(self.gt)
|
||||
|
||||
Also see the implementation of `Interval` below for an example.
|
||||
|
||||
Parsers should recognize this and unpack it so that it can be used
|
||||
both with and without unpacking:
|
||||
|
||||
- `Annotated[int, Field(...)]` (parser must unpack Field)
|
||||
- `Annotated[int, *Field(...)]` (PEP-646)
|
||||
""" # noqa: trailing-whitespace
|
||||
|
||||
@property
|
||||
def __is_annotated_types_grouped_metadata__(self) -> Literal[True]:
|
||||
return True
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
...
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
__slots__ = () # allow subclasses to use slots
|
||||
|
||||
def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
|
||||
# Basic ABC like functionality without the complexity of an ABC
|
||||
super().__init_subclass__(*args, **kwargs)
|
||||
if cls.__iter__ is GroupedMetadata.__iter__:
|
||||
raise TypeError("Can't subclass GroupedMetadata without implementing __iter__")
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]: # noqa: F811
|
||||
raise NotImplementedError # more helpful than "None has no attribute..." type errors
|
||||
|
||||
|
||||
@dataclass(frozen=True, **KW_ONLY, **SLOTS)
|
||||
class Interval(GroupedMetadata):
|
||||
"""Interval can express inclusive or exclusive bounds with a single object.
|
||||
|
||||
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
|
||||
are interpreted the same way as the single-bound constraints.
|
||||
"""
|
||||
|
||||
gt: Union[SupportsGt, None] = None
|
||||
ge: Union[SupportsGe, None] = None
|
||||
lt: Union[SupportsLt, None] = None
|
||||
le: Union[SupportsLe, None] = None
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
"""Unpack an Interval into zero or more single-bounds."""
|
||||
if self.gt is not None:
|
||||
yield Gt(self.gt)
|
||||
if self.ge is not None:
|
||||
yield Ge(self.ge)
|
||||
if self.lt is not None:
|
||||
yield Lt(self.lt)
|
||||
if self.le is not None:
|
||||
yield Le(self.le)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MultipleOf(BaseMetadata):
|
||||
"""MultipleOf(multiple_of=x) might be interpreted in two ways:
|
||||
|
||||
1. Python semantics, implying ``value % multiple_of == 0``, or
|
||||
2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of``
|
||||
|
||||
We encourage users to be aware of these two common interpretations,
|
||||
and libraries to carefully document which they implement.
|
||||
"""
|
||||
|
||||
multiple_of: Union[SupportsDiv, SupportsMod]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MinLen(BaseMetadata):
|
||||
"""
|
||||
MinLen() implies minimum inclusive length,
|
||||
e.g. ``len(value) >= min_length``.
|
||||
"""
|
||||
|
||||
min_length: Annotated[int, Ge(0)]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class MaxLen(BaseMetadata):
|
||||
"""
|
||||
MaxLen() implies maximum inclusive length,
|
||||
e.g. ``len(value) <= max_length``.
|
||||
"""
|
||||
|
||||
max_length: Annotated[int, Ge(0)]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Len(GroupedMetadata):
|
||||
"""
|
||||
Len() implies that ``min_length <= len(value) <= max_length``.
|
||||
|
||||
Upper bound may be omitted or ``None`` to indicate no upper length bound.
|
||||
"""
|
||||
|
||||
min_length: Annotated[int, Ge(0)] = 0
|
||||
max_length: Optional[Annotated[int, Ge(0)]] = None
|
||||
|
||||
def __iter__(self) -> Iterator[BaseMetadata]:
|
||||
"""Unpack a Len into zone or more single-bounds."""
|
||||
if self.min_length > 0:
|
||||
yield MinLen(self.min_length)
|
||||
if self.max_length is not None:
|
||||
yield MaxLen(self.max_length)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Timezone(BaseMetadata):
|
||||
"""Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive).
|
||||
|
||||
``Annotated[datetime, Timezone(None)]`` must be a naive datetime.
|
||||
``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be
|
||||
tz-aware but any timezone is allowed.
|
||||
|
||||
You may also pass a specific timezone string or timezone object such as
|
||||
``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that
|
||||
you only allow a specific timezone, though we note that this is often
|
||||
a symptom of poor design.
|
||||
"""
|
||||
|
||||
tz: Union[str, timezone, EllipsisType, None]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class Predicate(BaseMetadata):
|
||||
"""``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values.
|
||||
|
||||
Users should prefer statically inspectable metadata, but if you need the full
|
||||
power and flexibility of arbitrary runtime predicates... here it is.
|
||||
|
||||
We provide a few predefined predicates for common string constraints:
|
||||
``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and
|
||||
``IsDigit = Predicate(str.isdigit)``. Users are encouraged to use methods which
|
||||
can be given special handling, and avoid indirection like ``lambda s: s.lower()``.
|
||||
|
||||
Some libraries might have special logic to handle certain predicates, e.g. by
|
||||
checking for `str.isdigit` and using its presence to both call custom logic to
|
||||
enforce digit-only strings, and customise some generated external schema.
|
||||
|
||||
We do not specify what behaviour should be expected for predicates that raise
|
||||
an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently
|
||||
skip invalid constraints, or statically raise an error; or it might try calling it
|
||||
and then propogate or discard the resulting exception.
|
||||
"""
|
||||
|
||||
func: Callable[[Any], bool]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Not:
|
||||
func: Callable[[Any], bool]
|
||||
|
||||
def __call__(self, __v: Any) -> bool:
|
||||
return not self.func(__v)
|
||||
|
||||
|
||||
_StrType = TypeVar("_StrType", bound=str)
|
||||
|
||||
LowerCase = Annotated[_StrType, Predicate(str.islower)]
|
||||
"""
|
||||
Return True if the string is a lowercase string, False otherwise.
|
||||
|
||||
A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string.
|
||||
""" # noqa: E501
|
||||
UpperCase = Annotated[_StrType, Predicate(str.isupper)]
|
||||
"""
|
||||
Return True if the string is an uppercase string, False otherwise.
|
||||
|
||||
A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string.
|
||||
""" # noqa: E501
|
||||
IsDigits = Annotated[_StrType, Predicate(str.isdigit)]
|
||||
"""
|
||||
Return True if the string is a digit string, False otherwise.
|
||||
|
||||
A string is a digit string if all characters in the string are digits and there is at least one character in the string.
|
||||
""" # noqa: E501
|
||||
IsAscii = Annotated[_StrType, Predicate(str.isascii)]
|
||||
"""
|
||||
Return True if all characters in the string are ASCII, False otherwise.
|
||||
|
||||
ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too.
|
||||
"""
|
||||
|
||||
_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex])
|
||||
IsFinite = Annotated[_NumericType, Predicate(math.isfinite)]
|
||||
"""Return True if x is neither an infinity nor a NaN, and False otherwise."""
|
||||
IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))]
|
||||
"""Return True if x is one of infinity or NaN, and False otherwise"""
|
||||
IsNan = Annotated[_NumericType, Predicate(math.isnan)]
|
||||
"""Return True if x is a NaN (not a number), and False otherwise."""
|
||||
IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))]
|
||||
"""Return True if x is anything but NaN (not a number), and False otherwise."""
|
||||
IsInfinite = Annotated[_NumericType, Predicate(math.isinf)]
|
||||
"""Return True if x is a positive or negative infinity, and False otherwise."""
|
||||
IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))]
|
||||
"""Return True if x is neither a positive or negative infinity, and False otherwise."""
|
||||
|
||||
try:
|
||||
from typing_extensions import DocInfo, doc # type: ignore [attr-defined]
|
||||
except ImportError:
|
||||
|
||||
@dataclass(frozen=True, **SLOTS)
|
||||
class DocInfo: # type: ignore [no-redef]
|
||||
""" "
|
||||
The return value of doc(), mainly to be used by tools that want to extract the
|
||||
Annotated documentation at runtime.
|
||||
"""
|
||||
|
||||
documentation: str
|
||||
"""The documentation string passed to doc()."""
|
||||
|
||||
def doc(
|
||||
documentation: str,
|
||||
) -> DocInfo:
|
||||
"""
|
||||
Add documentation to a type annotation inside of Annotated.
|
||||
|
||||
For example:
|
||||
|
||||
>>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ...
|
||||
"""
|
||||
return DocInfo(documentation)
|
147
lib/annotated_types/test_cases.py
Normal file
147
lib/annotated_types/test_cases.py
Normal file
|
@ -0,0 +1,147 @@
|
|||
import math
|
||||
import sys
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
from typing_extensions import Annotated
|
||||
else:
|
||||
from typing import Annotated
|
||||
|
||||
import annotated_types as at
|
||||
|
||||
|
||||
class Case(NamedTuple):
|
||||
"""
|
||||
A test case for `annotated_types`.
|
||||
"""
|
||||
|
||||
annotation: Any
|
||||
valid_cases: Iterable[Any]
|
||||
invalid_cases: Iterable[Any]
|
||||
|
||||
|
||||
def cases() -> Iterable[Case]:
|
||||
# Gt, Ge, Lt, Le
|
||||
yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1))
|
||||
yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(date(2000, 1, 1))],
|
||||
[date(2000, 1, 2), date(2000, 1, 3)],
|
||||
[date(2000, 1, 1), date(1999, 12, 31)],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Gt(Decimal('1.123'))],
|
||||
[Decimal('1.1231'), Decimal('123')],
|
||||
[Decimal('1.123'), Decimal('0')],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1))
|
||||
yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Ge(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(1998, 1, 1), datetime(1999, 12, 31)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4))
|
||||
yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Lt(datetime(2000, 1, 1))],
|
||||
[datetime(1999, 12, 31), datetime(1999, 12, 31)],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000))
|
||||
yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Le(datetime(2000, 1, 1))],
|
||||
[datetime(2000, 1, 1), datetime(1999, 12, 31)],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
)
|
||||
|
||||
# Interval
|
||||
yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1))
|
||||
yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1))
|
||||
yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1))
|
||||
yield Case(
|
||||
Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))],
|
||||
[datetime(2000, 1, 2), datetime(2000, 1, 3)],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 4)],
|
||||
)
|
||||
|
||||
yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4))
|
||||
yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1))
|
||||
|
||||
# lengths
|
||||
|
||||
yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
|
||||
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
|
||||
yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
|
||||
yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
|
||||
|
||||
yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
|
||||
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
|
||||
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
|
||||
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
|
||||
|
||||
yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
|
||||
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))
|
||||
|
||||
yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
|
||||
yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
|
||||
yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))
|
||||
|
||||
# Timezone
|
||||
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)]
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)]
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone(timezone.utc)],
|
||||
[datetime(2000, 1, 1, tzinfo=timezone.utc)],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
|
||||
)
|
||||
yield Case(
|
||||
Annotated[datetime, at.Timezone('Europe/London')],
|
||||
[datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))],
|
||||
[datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))],
|
||||
)
|
||||
|
||||
# predicate types
|
||||
|
||||
yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom'])
|
||||
yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC'])
|
||||
yield Case(at.IsDigits[str], ['123'], ['', 'ab', 'a1b2'])
|
||||
yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀'])
|
||||
|
||||
yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5])
|
||||
|
||||
yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf])
|
||||
yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23])
|
||||
yield Case(at.IsNan[float], [math.nan], [1.23, math.inf])
|
||||
yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan])
|
||||
yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23])
|
||||
yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf])
|
||||
|
||||
# check stacked predicates
|
||||
yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan])
|
||||
|
||||
# doc
|
||||
yield Case(Annotated[int, at.doc("A number")], [1, 2], [])
|
||||
|
||||
# custom GroupedMetadata
|
||||
class MyCustomGroupedMetadata(at.GroupedMetadata):
|
||||
def __iter__(self) -> Iterator[at.Predicate]:
|
||||
yield at.Predicate(lambda x: float(x).is_integer())
|
||||
|
||||
yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5])
|
|
@ -1 +1 @@
|
|||
__version__ = "1.2.3"
|
||||
__version__ = "1.3.0"
|
||||
|
|
|
@ -168,9 +168,9 @@ class Arrow:
|
|||
isinstance(tzinfo, dt_tzinfo)
|
||||
and hasattr(tzinfo, "localize")
|
||||
and hasattr(tzinfo, "zone")
|
||||
and tzinfo.zone # type: ignore[attr-defined]
|
||||
and tzinfo.zone
|
||||
):
|
||||
tzinfo = parser.TzinfoParser.parse(tzinfo.zone) # type: ignore[attr-defined]
|
||||
tzinfo = parser.TzinfoParser.parse(tzinfo.zone)
|
||||
elif isinstance(tzinfo, str):
|
||||
tzinfo = parser.TzinfoParser.parse(tzinfo)
|
||||
|
||||
|
@ -495,7 +495,7 @@ class Arrow:
|
|||
yield current
|
||||
|
||||
values = [getattr(current, f) for f in cls._ATTRS]
|
||||
current = cls(*values, tzinfo=tzinfo).shift( # type: ignore
|
||||
current = cls(*values, tzinfo=tzinfo).shift( # type: ignore[misc]
|
||||
**{frame_relative: relative_steps}
|
||||
)
|
||||
|
||||
|
@ -578,7 +578,7 @@ class Arrow:
|
|||
for _ in range(3 - len(values)):
|
||||
values.append(1)
|
||||
|
||||
floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore
|
||||
floor = self.__class__(*values, tzinfo=self.tzinfo) # type: ignore[misc]
|
||||
|
||||
if frame_absolute == "week":
|
||||
# if week_start is greater than self.isoweekday() go back one week by setting delta = 7
|
||||
|
@ -792,7 +792,6 @@ class Arrow:
|
|||
return self._datetime.isoformat()
|
||||
|
||||
def __format__(self, formatstr: str) -> str:
|
||||
|
||||
if len(formatstr) > 0:
|
||||
return self.format(formatstr)
|
||||
|
||||
|
@ -804,7 +803,6 @@ class Arrow:
|
|||
# attributes and properties
|
||||
|
||||
def __getattr__(self, name: str) -> int:
|
||||
|
||||
if name == "week":
|
||||
return self.isocalendar()[1]
|
||||
|
||||
|
@ -965,7 +963,6 @@ class Arrow:
|
|||
absolute_kwargs = {}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
|
||||
if key in self._ATTRS:
|
||||
absolute_kwargs[key] = value
|
||||
elif key in ["week", "quarter"]:
|
||||
|
@ -1022,7 +1019,6 @@ class Arrow:
|
|||
additional_attrs = ["weeks", "quarters", "weekday"]
|
||||
|
||||
for key, value in kwargs.items():
|
||||
|
||||
if key in self._ATTRS_PLURAL or key in additional_attrs:
|
||||
relative_kwargs[key] = value
|
||||
else:
|
||||
|
@ -1259,11 +1255,10 @@ class Arrow:
|
|||
)
|
||||
|
||||
if trunc(abs(delta)) != 1:
|
||||
granularity += "s" # type: ignore
|
||||
granularity += "s" # type: ignore[assignment]
|
||||
return locale.describe(granularity, delta, only_distance=only_distance)
|
||||
|
||||
else:
|
||||
|
||||
if not granularity:
|
||||
raise ValueError(
|
||||
"Empty granularity list provided. "
|
||||
|
@ -1314,7 +1309,7 @@ class Arrow:
|
|||
|
||||
def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow":
|
||||
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, that represents
|
||||
the time difference relative to the attrbiutes of the
|
||||
the time difference relative to the attributes of the
|
||||
:class:`Arrow <arrow.arrow.Arrow>` object.
|
||||
|
||||
:param timestring: a ``str`` representing a humanized relative time.
|
||||
|
@ -1367,7 +1362,6 @@ class Arrow:
|
|||
|
||||
# Search input string for each time unit within locale
|
||||
for unit, unit_object in locale_obj.timeframes.items():
|
||||
|
||||
# Need to check the type of unit_object to create the correct dictionary
|
||||
if isinstance(unit_object, Mapping):
|
||||
strings_to_search = unit_object
|
||||
|
@ -1378,7 +1372,6 @@ class Arrow:
|
|||
# Needs to cycle all through strings as some locales have strings that
|
||||
# could overlap in a regex match, since input validation isn't being performed.
|
||||
for time_delta, time_string in strings_to_search.items():
|
||||
|
||||
# Replace {0} with regex \d representing digits
|
||||
search_string = str(time_string)
|
||||
search_string = search_string.format(r"\d+")
|
||||
|
@ -1419,7 +1412,7 @@ class Arrow:
|
|||
# Assert error if string does not modify any units
|
||||
if not any([True for k, v in unit_visited.items() if v]):
|
||||
raise ValueError(
|
||||
"Input string not valid. Note: Some locales do not support the week granulairty in Arrow. "
|
||||
"Input string not valid. Note: Some locales do not support the week granularity in Arrow. "
|
||||
"If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error."
|
||||
)
|
||||
|
||||
|
@ -1718,7 +1711,6 @@ class Arrow:
|
|||
# math
|
||||
|
||||
def __add__(self, other: Any) -> "Arrow":
|
||||
|
||||
if isinstance(other, (timedelta, relativedelta)):
|
||||
return self.fromdatetime(self._datetime + other, self._datetime.tzinfo)
|
||||
|
||||
|
@ -1736,7 +1728,6 @@ class Arrow:
|
|||
pass # pragma: no cover
|
||||
|
||||
def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]:
|
||||
|
||||
if isinstance(other, (timedelta, relativedelta)):
|
||||
return self.fromdatetime(self._datetime - other, self._datetime.tzinfo)
|
||||
|
||||
|
@ -1749,7 +1740,6 @@ class Arrow:
|
|||
return NotImplemented
|
||||
|
||||
def __rsub__(self, other: Any) -> timedelta:
|
||||
|
||||
if isinstance(other, dt_datetime):
|
||||
return other - self._datetime
|
||||
|
||||
|
@ -1758,42 +1748,36 @@ class Arrow:
|
|||
# comparisons
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
|
||||
if not isinstance(other, (Arrow, dt_datetime)):
|
||||
return False
|
||||
|
||||
return self._datetime == self._get_datetime(other)
|
||||
|
||||
def __ne__(self, other: Any) -> bool:
|
||||
|
||||
if not isinstance(other, (Arrow, dt_datetime)):
|
||||
return True
|
||||
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __gt__(self, other: Any) -> bool:
|
||||
|
||||
if not isinstance(other, (Arrow, dt_datetime)):
|
||||
return NotImplemented
|
||||
|
||||
return self._datetime > self._get_datetime(other)
|
||||
|
||||
def __ge__(self, other: Any) -> bool:
|
||||
|
||||
if not isinstance(other, (Arrow, dt_datetime)):
|
||||
return NotImplemented
|
||||
|
||||
return self._datetime >= self._get_datetime(other)
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
|
||||
if not isinstance(other, (Arrow, dt_datetime)):
|
||||
return NotImplemented
|
||||
|
||||
return self._datetime < self._get_datetime(other)
|
||||
|
||||
def __le__(self, other: Any) -> bool:
|
||||
|
||||
if not isinstance(other, (Arrow, dt_datetime)):
|
||||
return NotImplemented
|
||||
|
||||
|
@ -1865,7 +1849,6 @@ class Arrow:
|
|||
def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int]:
|
||||
"""Sets default end and limit values for range method."""
|
||||
if end is None:
|
||||
|
||||
if limit is None:
|
||||
raise ValueError("One of 'end' or 'limit' is required.")
|
||||
|
||||
|
|
|
@ -267,11 +267,9 @@ class ArrowFactory:
|
|||
raise TypeError(f"Cannot parse single argument of type {type(arg)!r}.")
|
||||
|
||||
elif arg_count == 2:
|
||||
|
||||
arg_1, arg_2 = args[0], args[1]
|
||||
|
||||
if isinstance(arg_1, datetime):
|
||||
|
||||
# (datetime, tzinfo/str) -> fromdatetime @ tzinfo
|
||||
if isinstance(arg_2, (dt_tzinfo, str)):
|
||||
return self.type.fromdatetime(arg_1, tzinfo=arg_2)
|
||||
|
@ -281,7 +279,6 @@ class ArrowFactory:
|
|||
)
|
||||
|
||||
elif isinstance(arg_1, date):
|
||||
|
||||
# (date, tzinfo/str) -> fromdate @ tzinfo
|
||||
if isinstance(arg_2, (dt_tzinfo, str)):
|
||||
return self.type.fromdate(arg_1, tzinfo=arg_2)
|
||||
|
|
|
@ -29,7 +29,6 @@ FORMAT_W3C: Final[str] = "YYYY-MM-DD HH:mm:ssZZ"
|
|||
|
||||
|
||||
class DateTimeFormatter:
|
||||
|
||||
# This pattern matches characters enclosed in square brackets are matched as
|
||||
# an atomic group. For more info on atomic groups and how to they are
|
||||
# emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578
|
||||
|
@ -41,18 +40,15 @@ class DateTimeFormatter:
|
|||
locale: locales.Locale
|
||||
|
||||
def __init__(self, locale: str = DEFAULT_LOCALE) -> None:
|
||||
|
||||
self.locale = locales.get_locale(locale)
|
||||
|
||||
def format(cls, dt: datetime, fmt: str) -> str:
|
||||
|
||||
# FIXME: _format_token() is nullable
|
||||
return cls._FORMAT_RE.sub(
|
||||
lambda m: cast(str, cls._format_token(dt, m.group(0))), fmt
|
||||
)
|
||||
|
||||
def _format_token(self, dt: datetime, token: Optional[str]) -> Optional[str]:
|
||||
|
||||
if token and token.startswith("[") and token.endswith("]"):
|
||||
return token[1:-1]
|
||||
|
||||
|
|
|
@ -129,7 +129,6 @@ class Locale:
|
|||
_locale_map[locale_name.lower().replace("_", "-")] = cls
|
||||
|
||||
def __init__(self) -> None:
|
||||
|
||||
self._month_name_to_ordinal = None
|
||||
|
||||
def describe(
|
||||
|
@ -174,7 +173,7 @@ class Locale:
|
|||
# Needed to determine the correct relative string to use
|
||||
timeframe_value = 0
|
||||
|
||||
for _unit_name, unit_value in timeframes:
|
||||
for _, unit_value in timeframes:
|
||||
if trunc(unit_value) != 0:
|
||||
timeframe_value = trunc(unit_value)
|
||||
break
|
||||
|
@ -285,7 +284,6 @@ class Locale:
|
|||
timeframe: TimeFrameLiteral,
|
||||
delta: Union[float, int],
|
||||
) -> str:
|
||||
|
||||
if timeframe == "now":
|
||||
return humanized
|
||||
|
||||
|
@ -425,7 +423,7 @@ class ItalianLocale(Locale):
|
|||
"hours": "{0} ore",
|
||||
"day": "un giorno",
|
||||
"days": "{0} giorni",
|
||||
"week": "una settimana,",
|
||||
"week": "una settimana",
|
||||
"weeks": "{0} settimane",
|
||||
"month": "un mese",
|
||||
"months": "{0} mesi",
|
||||
|
@ -867,14 +865,16 @@ class FinnishLocale(Locale):
|
|||
|
||||
timeframes: ClassVar[Mapping[TimeFrameLiteral, Union[str, Mapping[str, str]]]] = {
|
||||
"now": "juuri nyt",
|
||||
"second": "sekunti",
|
||||
"seconds": {"past": "{0} muutama sekunti", "future": "{0} muutaman sekunnin"},
|
||||
"second": {"past": "sekunti", "future": "sekunnin"},
|
||||
"seconds": {"past": "{0} sekuntia", "future": "{0} sekunnin"},
|
||||
"minute": {"past": "minuutti", "future": "minuutin"},
|
||||
"minutes": {"past": "{0} minuuttia", "future": "{0} minuutin"},
|
||||
"hour": {"past": "tunti", "future": "tunnin"},
|
||||
"hours": {"past": "{0} tuntia", "future": "{0} tunnin"},
|
||||
"day": "päivä",
|
||||
"day": {"past": "päivä", "future": "päivän"},
|
||||
"days": {"past": "{0} päivää", "future": "{0} päivän"},
|
||||
"week": {"past": "viikko", "future": "viikon"},
|
||||
"weeks": {"past": "{0} viikkoa", "future": "{0} viikon"},
|
||||
"month": {"past": "kuukausi", "future": "kuukauden"},
|
||||
"months": {"past": "{0} kuukautta", "future": "{0} kuukauden"},
|
||||
"year": {"past": "vuosi", "future": "vuoden"},
|
||||
|
@ -1887,7 +1887,7 @@ class GermanBaseLocale(Locale):
|
|||
future = "in {0}"
|
||||
and_word = "und"
|
||||
|
||||
timeframes = {
|
||||
timeframes: ClassVar[Dict[TimeFrameLiteral, str]] = {
|
||||
"now": "gerade eben",
|
||||
"second": "einer Sekunde",
|
||||
"seconds": "{0} Sekunden",
|
||||
|
@ -1982,7 +1982,9 @@ class GermanBaseLocale(Locale):
|
|||
return super().describe(timeframe, delta, only_distance)
|
||||
|
||||
# German uses a different case without 'in' or 'ago'
|
||||
humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta)))
|
||||
humanized: str = self.timeframes_only_distance[timeframe].format(
|
||||
trunc(abs(delta))
|
||||
)
|
||||
|
||||
return humanized
|
||||
|
||||
|
@ -2547,6 +2549,8 @@ class ArabicLocale(Locale):
|
|||
"hours": {"2": "ساعتين", "ten": "{0} ساعات", "higher": "{0} ساعة"},
|
||||
"day": "يوم",
|
||||
"days": {"2": "يومين", "ten": "{0} أيام", "higher": "{0} يوم"},
|
||||
"week": "اسبوع",
|
||||
"weeks": {"2": "اسبوعين", "ten": "{0} أسابيع", "higher": "{0} اسبوع"},
|
||||
"month": "شهر",
|
||||
"months": {"2": "شهرين", "ten": "{0} أشهر", "higher": "{0} شهر"},
|
||||
"year": "سنة",
|
||||
|
@ -3709,6 +3713,8 @@ class HungarianLocale(Locale):
|
|||
"hours": {"past": "{0} órával", "future": "{0} óra"},
|
||||
"day": {"past": "egy nappal", "future": "egy nap"},
|
||||
"days": {"past": "{0} nappal", "future": "{0} nap"},
|
||||
"week": {"past": "egy héttel", "future": "egy hét"},
|
||||
"weeks": {"past": "{0} héttel", "future": "{0} hét"},
|
||||
"month": {"past": "egy hónappal", "future": "egy hónap"},
|
||||
"months": {"past": "{0} hónappal", "future": "{0} hónap"},
|
||||
"year": {"past": "egy évvel", "future": "egy év"},
|
||||
|
@ -3934,7 +3940,6 @@ class ThaiLocale(Locale):
|
|||
|
||||
|
||||
class LaotianLocale(Locale):
|
||||
|
||||
names = ["lo", "lo-la"]
|
||||
|
||||
past = "{0} ກ່ອນຫນ້ານີ້"
|
||||
|
@ -4119,6 +4124,7 @@ class BengaliLocale(Locale):
|
|||
return f"{n}র্থ"
|
||||
if n == 6:
|
||||
return f"{n}ষ্ঠ"
|
||||
return ""
|
||||
|
||||
|
||||
class RomanshLocale(Locale):
|
||||
|
@ -4137,6 +4143,8 @@ class RomanshLocale(Locale):
|
|||
"hours": "{0} ura",
|
||||
"day": "in di",
|
||||
"days": "{0} dis",
|
||||
"week": "in'emna",
|
||||
"weeks": "{0} emnas",
|
||||
"month": "in mais",
|
||||
"months": "{0} mais",
|
||||
"year": "in onn",
|
||||
|
@ -5399,7 +5407,7 @@ class LuxembourgishLocale(Locale):
|
|||
future = "an {0}"
|
||||
and_word = "an"
|
||||
|
||||
timeframes = {
|
||||
timeframes: ClassVar[Dict[TimeFrameLiteral, str]] = {
|
||||
"now": "just elo",
|
||||
"second": "enger Sekonn",
|
||||
"seconds": "{0} Sekonnen",
|
||||
|
@ -5487,7 +5495,9 @@ class LuxembourgishLocale(Locale):
|
|||
return super().describe(timeframe, delta, only_distance)
|
||||
|
||||
# Luxembourgish uses a different case without 'in' or 'ago'
|
||||
humanized = self.timeframes_only_distance[timeframe].format(trunc(abs(delta)))
|
||||
humanized: str = self.timeframes_only_distance[timeframe].format(
|
||||
trunc(abs(delta))
|
||||
)
|
||||
|
||||
return humanized
|
||||
|
||||
|
|
|
@ -159,7 +159,6 @@ class DateTimeParser:
|
|||
_input_re_map: Dict[_FORMAT_TYPE, Pattern[str]]
|
||||
|
||||
def __init__(self, locale: str = DEFAULT_LOCALE, cache_size: int = 0) -> None:
|
||||
|
||||
self.locale = locales.get_locale(locale)
|
||||
self._input_re_map = self._BASE_INPUT_RE_MAP.copy()
|
||||
self._input_re_map.update(
|
||||
|
@ -196,7 +195,6 @@ class DateTimeParser:
|
|||
def parse_iso(
|
||||
self, datetime_string: str, normalize_whitespace: bool = False
|
||||
) -> datetime:
|
||||
|
||||
if normalize_whitespace:
|
||||
datetime_string = re.sub(r"\s+", " ", datetime_string.strip())
|
||||
|
||||
|
@ -236,13 +234,14 @@ class DateTimeParser:
|
|||
]
|
||||
|
||||
if has_time:
|
||||
|
||||
if has_space_divider:
|
||||
date_string, time_string = datetime_string.split(" ", 1)
|
||||
else:
|
||||
date_string, time_string = datetime_string.split("T", 1)
|
||||
|
||||
time_parts = re.split(r"[\+\-Z]", time_string, 1, re.IGNORECASE)
|
||||
time_parts = re.split(
|
||||
r"[\+\-Z]", time_string, maxsplit=1, flags=re.IGNORECASE
|
||||
)
|
||||
|
||||
time_components: Optional[Match[str]] = self._TIME_RE.match(time_parts[0])
|
||||
|
||||
|
@ -303,7 +302,6 @@ class DateTimeParser:
|
|||
fmt: Union[List[str], str],
|
||||
normalize_whitespace: bool = False,
|
||||
) -> datetime:
|
||||
|
||||
if normalize_whitespace:
|
||||
datetime_string = re.sub(r"\s+", " ", datetime_string)
|
||||
|
||||
|
@ -341,12 +339,11 @@ class DateTimeParser:
|
|||
f"Unable to find a match group for the specified token {token!r}."
|
||||
)
|
||||
|
||||
self._parse_token(token, value, parts) # type: ignore
|
||||
self._parse_token(token, value, parts) # type: ignore[arg-type]
|
||||
|
||||
return self._build_datetime(parts)
|
||||
|
||||
def _generate_pattern_re(self, fmt: str) -> Tuple[List[_FORMAT_TYPE], Pattern[str]]:
|
||||
|
||||
# fmt is a string of tokens like 'YYYY-MM-DD'
|
||||
# we construct a new string by replacing each
|
||||
# token by its pattern:
|
||||
|
@ -498,7 +495,6 @@ class DateTimeParser:
|
|||
value: Any,
|
||||
parts: _Parts,
|
||||
) -> None:
|
||||
|
||||
if token == "YYYY":
|
||||
parts["year"] = int(value)
|
||||
|
||||
|
@ -508,7 +504,7 @@ class DateTimeParser:
|
|||
|
||||
elif token in ["MMMM", "MMM"]:
|
||||
# FIXME: month_number() is nullable
|
||||
parts["month"] = self.locale.month_number(value.lower()) # type: ignore
|
||||
parts["month"] = self.locale.month_number(value.lower()) # type: ignore[typeddict-item]
|
||||
|
||||
elif token in ["MM", "M"]:
|
||||
parts["month"] = int(value)
|
||||
|
@ -588,7 +584,6 @@ class DateTimeParser:
|
|||
weekdate = parts.get("weekdate")
|
||||
|
||||
if weekdate is not None:
|
||||
|
||||
year, week = int(weekdate[0]), int(weekdate[1])
|
||||
|
||||
if weekdate[2] is not None:
|
||||
|
@ -712,7 +707,6 @@ class DateTimeParser:
|
|||
)
|
||||
|
||||
def _parse_multiformat(self, string: str, formats: Iterable[str]) -> datetime:
|
||||
|
||||
_datetime: Optional[datetime] = None
|
||||
|
||||
for fmt in formats:
|
||||
|
@ -740,12 +734,11 @@ class DateTimeParser:
|
|||
|
||||
class TzinfoParser:
|
||||
_TZINFO_RE: ClassVar[Pattern[str]] = re.compile(
|
||||
r"^([\+\-])?(\d{2})(?:\:?(\d{2}))?$"
|
||||
r"^(?:\(UTC)*([\+\-])?(\d{2})(?:\:?(\d{2}))?"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, tzinfo_string: str) -> dt_tzinfo:
|
||||
|
||||
tzinfo: Optional[dt_tzinfo] = None
|
||||
|
||||
if tzinfo_string == "local":
|
||||
|
@ -755,7 +748,6 @@ class TzinfoParser:
|
|||
tzinfo = tz.tzutc()
|
||||
|
||||
else:
|
||||
|
||||
iso_match = cls._TZINFO_RE.match(tzinfo_string)
|
||||
|
||||
if iso_match:
|
||||
|
|
|
@ -20,7 +20,7 @@ from functools import wraps
|
|||
from inspect import signature
|
||||
|
||||
|
||||
def _launch_forever_coro(coro, args, kwargs, loop):
|
||||
async def _run_forever_coro(coro, args, kwargs, loop):
|
||||
'''
|
||||
This helper function launches an async main function that was tagged with
|
||||
forever=True. There are two possibilities:
|
||||
|
@ -48,7 +48,7 @@ def _launch_forever_coro(coro, args, kwargs, loop):
|
|||
# forever=True feature from autoasync at some point in the future.
|
||||
thing = coro(*args, **kwargs)
|
||||
if iscoroutine(thing):
|
||||
loop.create_task(thing)
|
||||
await thing
|
||||
|
||||
|
||||
def autoasync(coro=None, *, loop=None, forever=False, pass_loop=False):
|
||||
|
@ -127,7 +127,9 @@ def autoasync(coro=None, *, loop=None, forever=False, pass_loop=False):
|
|||
args, kwargs = bound_args.args, bound_args.kwargs
|
||||
|
||||
if forever:
|
||||
_launch_forever_coro(coro, args, kwargs, local_loop)
|
||||
local_loop.create_task(_run_forever_coro(
|
||||
coro, args, kwargs, local_loop
|
||||
))
|
||||
local_loop.run_forever()
|
||||
else:
|
||||
return local_loop.run_until_complete(coro(*args, **kwargs))
|
||||
|
|
|
@ -26,6 +26,12 @@ def update_wrapper(
|
|||
|
||||
|
||||
class _HashedSeq(list):
|
||||
"""This class guarantees that hash() will be called no more than once
|
||||
per element. This is important because the lru_cache() will hash
|
||||
the key multiple times on a cache miss.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = 'hashvalue'
|
||||
|
||||
def __init__(self, tup, hash=hash):
|
||||
|
@ -41,45 +47,57 @@ def _make_key(
|
|||
kwds,
|
||||
typed,
|
||||
kwd_mark=(object(),),
|
||||
fasttypes=set([int, str, frozenset, type(None)]),
|
||||
sorted=sorted,
|
||||
fasttypes={int, str},
|
||||
tuple=tuple,
|
||||
type=type,
|
||||
len=len,
|
||||
):
|
||||
'Make a cache key from optionally typed positional and keyword arguments'
|
||||
"""Make a cache key from optionally typed positional and keyword arguments
|
||||
|
||||
The key is constructed in a way that is flat as possible rather than
|
||||
as a nested structure that would take more memory.
|
||||
|
||||
If there is only a single argument and its data type is known to cache
|
||||
its hash value, then that argument is returned without a wrapper. This
|
||||
saves space and improves lookup speed.
|
||||
|
||||
"""
|
||||
# All of code below relies on kwds preserving the order input by the user.
|
||||
# Formerly, we sorted() the kwds before looping. The new way is *much*
|
||||
# faster; however, it means that f(x=1, y=2) will now be treated as a
|
||||
# distinct call from f(y=2, x=1) which will be cached separately.
|
||||
key = args
|
||||
if kwds:
|
||||
sorted_items = sorted(kwds.items())
|
||||
key += kwd_mark
|
||||
for item in sorted_items:
|
||||
for item in kwds.items():
|
||||
key += item
|
||||
if typed:
|
||||
key += tuple(type(v) for v in args)
|
||||
if kwds:
|
||||
key += tuple(type(v) for k, v in sorted_items)
|
||||
key += tuple(type(v) for v in kwds.values())
|
||||
elif len(key) == 1 and type(key[0]) in fasttypes:
|
||||
return key[0]
|
||||
return _HashedSeq(key)
|
||||
|
||||
|
||||
def lru_cache(maxsize=100, typed=False): # noqa: C901
|
||||
def lru_cache(maxsize=128, typed=False):
|
||||
"""Least-recently-used cache decorator.
|
||||
|
||||
If *maxsize* is set to None, the LRU features are disabled and the cache
|
||||
can grow without bound.
|
||||
|
||||
If *typed* is True, arguments of different types will be cached separately.
|
||||
For example, f(3.0) and f(3) will be treated as distinct calls with
|
||||
distinct results.
|
||||
For example, f(decimal.Decimal("3.0")) and f(3.0) will be treated as
|
||||
distinct calls with distinct results. Some types such as str and int may
|
||||
be cached separately even when typed is false.
|
||||
|
||||
Arguments to the cached function must be hashable.
|
||||
|
||||
View the cache statistics named tuple (hits, misses, maxsize, currsize) with
|
||||
f.cache_info(). Clear the cache and statistics with f.cache_clear().
|
||||
View the cache statistics named tuple (hits, misses, maxsize, currsize)
|
||||
with f.cache_info(). Clear the cache and statistics with f.cache_clear().
|
||||
Access the underlying function with f.__wrapped__.
|
||||
|
||||
See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
|
||||
See: https://en.wikipedia.org/wiki/Cache_replacement_policies#Least_recently_used_(LRU)
|
||||
|
||||
"""
|
||||
|
||||
|
@ -88,108 +106,138 @@ def lru_cache(maxsize=100, typed=False): # noqa: C901
|
|||
# The internals of the lru_cache are encapsulated for thread safety and
|
||||
# to allow the implementation to change (including a possible C version).
|
||||
|
||||
if isinstance(maxsize, int):
|
||||
# Negative maxsize is treated as 0
|
||||
if maxsize < 0:
|
||||
maxsize = 0
|
||||
elif callable(maxsize) and isinstance(typed, bool):
|
||||
# The user_function was passed in directly via the maxsize argument
|
||||
user_function, maxsize = maxsize, 128
|
||||
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
|
||||
wrapper.cache_parameters = lambda: {'maxsize': maxsize, 'typed': typed}
|
||||
return update_wrapper(wrapper, user_function)
|
||||
elif maxsize is not None:
|
||||
raise TypeError('Expected first argument to be an integer, a callable, or None')
|
||||
|
||||
def decorating_function(user_function):
|
||||
cache = dict()
|
||||
stats = [0, 0] # make statistics updateable non-locally
|
||||
HITS, MISSES = 0, 1 # names for the stats fields
|
||||
make_key = _make_key
|
||||
cache_get = cache.get # bound method to lookup key or return None
|
||||
_len = len # localize the global len() function
|
||||
lock = RLock() # because linkedlist updates aren't threadsafe
|
||||
root = [] # root of the circular doubly linked list
|
||||
root[:] = [root, root, None, None] # initialize by pointing to self
|
||||
nonlocal_root = [root] # make updateable non-locally
|
||||
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
|
||||
|
||||
if maxsize == 0:
|
||||
|
||||
def wrapper(*args, **kwds):
|
||||
# no caching, just do a statistics update after a successful call
|
||||
result = user_function(*args, **kwds)
|
||||
stats[MISSES] += 1
|
||||
return result
|
||||
|
||||
elif maxsize is None:
|
||||
|
||||
def wrapper(*args, **kwds):
|
||||
# simple caching without ordering or size limit
|
||||
key = make_key(args, kwds, typed)
|
||||
result = cache_get(
|
||||
key, root
|
||||
) # root used here as a unique not-found sentinel
|
||||
if result is not root:
|
||||
stats[HITS] += 1
|
||||
return result
|
||||
result = user_function(*args, **kwds)
|
||||
cache[key] = result
|
||||
stats[MISSES] += 1
|
||||
return result
|
||||
|
||||
else:
|
||||
|
||||
def wrapper(*args, **kwds):
|
||||
# size limited caching that tracks accesses by recency
|
||||
key = make_key(args, kwds, typed) if kwds or typed else args
|
||||
with lock:
|
||||
link = cache_get(key)
|
||||
if link is not None:
|
||||
# record recent use of the key by moving it
|
||||
# to the front of the list
|
||||
(root,) = nonlocal_root
|
||||
link_prev, link_next, key, result = link
|
||||
link_prev[NEXT] = link_next
|
||||
link_next[PREV] = link_prev
|
||||
last = root[PREV]
|
||||
last[NEXT] = root[PREV] = link
|
||||
link[PREV] = last
|
||||
link[NEXT] = root
|
||||
stats[HITS] += 1
|
||||
return result
|
||||
result = user_function(*args, **kwds)
|
||||
with lock:
|
||||
(root,) = nonlocal_root
|
||||
if key in cache:
|
||||
# getting here means that this same key was added to the
|
||||
# cache while the lock was released. since the link
|
||||
# update is already done, we need only return the
|
||||
# computed result and update the count of misses.
|
||||
pass
|
||||
elif _len(cache) >= maxsize:
|
||||
# use the old root to store the new key and result
|
||||
oldroot = root
|
||||
oldroot[KEY] = key
|
||||
oldroot[RESULT] = result
|
||||
# empty the oldest link and make it the new root
|
||||
root = nonlocal_root[0] = oldroot[NEXT]
|
||||
oldkey = root[KEY]
|
||||
root[KEY] = root[RESULT] = None
|
||||
# now update the cache dictionary for the new links
|
||||
del cache[oldkey]
|
||||
cache[key] = oldroot
|
||||
else:
|
||||
# put result in a new link at the front of the list
|
||||
last = root[PREV]
|
||||
link = [last, root, key, result]
|
||||
last[NEXT] = root[PREV] = cache[key] = link
|
||||
stats[MISSES] += 1
|
||||
return result
|
||||
|
||||
def cache_info():
|
||||
"""Report cache statistics"""
|
||||
with lock:
|
||||
return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache))
|
||||
|
||||
def cache_clear():
|
||||
"""Clear the cache and cache statistics"""
|
||||
with lock:
|
||||
cache.clear()
|
||||
root = nonlocal_root[0]
|
||||
root[:] = [root, root, None, None]
|
||||
stats[:] = [0, 0]
|
||||
|
||||
wrapper.__wrapped__ = user_function
|
||||
wrapper.cache_info = cache_info
|
||||
wrapper.cache_clear = cache_clear
|
||||
wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo)
|
||||
wrapper.cache_parameters = lambda: {'maxsize': maxsize, 'typed': typed}
|
||||
return update_wrapper(wrapper, user_function)
|
||||
|
||||
return decorating_function
|
||||
|
||||
|
||||
def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo):
|
||||
# Constants shared by all lru cache instances:
|
||||
sentinel = object() # unique object used to signal cache misses
|
||||
make_key = _make_key # build a key from the function arguments
|
||||
PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields
|
||||
|
||||
cache = {}
|
||||
hits = misses = 0
|
||||
full = False
|
||||
cache_get = cache.get # bound method to lookup a key or return None
|
||||
cache_len = cache.__len__ # get cache size without calling len()
|
||||
lock = RLock() # because linkedlist updates aren't threadsafe
|
||||
root = [] # root of the circular doubly linked list
|
||||
root[:] = [root, root, None, None] # initialize by pointing to self
|
||||
|
||||
if maxsize == 0:
|
||||
|
||||
def wrapper(*args, **kwds):
|
||||
# No caching -- just a statistics update
|
||||
nonlocal misses
|
||||
misses += 1
|
||||
result = user_function(*args, **kwds)
|
||||
return result
|
||||
|
||||
elif maxsize is None:
|
||||
|
||||
def wrapper(*args, **kwds):
|
||||
# Simple caching without ordering or size limit
|
||||
nonlocal hits, misses
|
||||
key = make_key(args, kwds, typed)
|
||||
result = cache_get(key, sentinel)
|
||||
if result is not sentinel:
|
||||
hits += 1
|
||||
return result
|
||||
misses += 1
|
||||
result = user_function(*args, **kwds)
|
||||
cache[key] = result
|
||||
return result
|
||||
|
||||
else:
|
||||
|
||||
def wrapper(*args, **kwds):
|
||||
# Size limited caching that tracks accesses by recency
|
||||
nonlocal root, hits, misses, full
|
||||
key = make_key(args, kwds, typed)
|
||||
with lock:
|
||||
link = cache_get(key)
|
||||
if link is not None:
|
||||
# Move the link to the front of the circular queue
|
||||
link_prev, link_next, _key, result = link
|
||||
link_prev[NEXT] = link_next
|
||||
link_next[PREV] = link_prev
|
||||
last = root[PREV]
|
||||
last[NEXT] = root[PREV] = link
|
||||
link[PREV] = last
|
||||
link[NEXT] = root
|
||||
hits += 1
|
||||
return result
|
||||
misses += 1
|
||||
result = user_function(*args, **kwds)
|
||||
with lock:
|
||||
if key in cache:
|
||||
# Getting here means that this same key was added to the
|
||||
# cache while the lock was released. Since the link
|
||||
# update is already done, we need only return the
|
||||
# computed result and update the count of misses.
|
||||
pass
|
||||
elif full:
|
||||
# Use the old root to store the new key and result.
|
||||
oldroot = root
|
||||
oldroot[KEY] = key
|
||||
oldroot[RESULT] = result
|
||||
# Empty the oldest link and make it the new root.
|
||||
# Keep a reference to the old key and old result to
|
||||
# prevent their ref counts from going to zero during the
|
||||
# update. That will prevent potentially arbitrary object
|
||||
# clean-up code (i.e. __del__) from running while we're
|
||||
# still adjusting the links.
|
||||
root = oldroot[NEXT]
|
||||
oldkey = root[KEY]
|
||||
root[KEY] = root[RESULT] = None
|
||||
# Now update the cache dictionary.
|
||||
del cache[oldkey]
|
||||
# Save the potentially reentrant cache[key] assignment
|
||||
# for last, after the root and links have been put in
|
||||
# a consistent state.
|
||||
cache[key] = oldroot
|
||||
else:
|
||||
# Put result in a new link at the front of the queue.
|
||||
last = root[PREV]
|
||||
link = [last, root, key, result]
|
||||
last[NEXT] = root[PREV] = cache[key] = link
|
||||
# Use the cache_len bound method instead of the len() function
|
||||
# which could potentially be wrapped in an lru_cache itself.
|
||||
full = cache_len() >= maxsize
|
||||
return result
|
||||
|
||||
def cache_info():
|
||||
"""Report cache statistics"""
|
||||
with lock:
|
||||
return _CacheInfo(hits, misses, maxsize, cache_len())
|
||||
|
||||
def cache_clear():
|
||||
"""Clear the cache and cache statistics"""
|
||||
nonlocal hits, misses, full
|
||||
with lock:
|
||||
cache.clear()
|
||||
root[:] = [root, root, None, None]
|
||||
hits = misses = 0
|
||||
full = False
|
||||
|
||||
wrapper.cache_info = cache_info
|
||||
wrapper.cache_clear = cache_clear
|
||||
return wrapper
|
||||
|
|
|
@ -11,9 +11,9 @@ from bleach.sanitizer import (
|
|||
|
||||
|
||||
# yyyymmdd
|
||||
__releasedate__ = "20230123"
|
||||
__releasedate__ = "20231006"
|
||||
# x.y.z or x.y.z.dev0 -- semver
|
||||
__version__ = "6.0.0"
|
||||
__version__ = "6.1.0"
|
||||
|
||||
|
||||
__all__ = ["clean", "linkify"]
|
||||
|
|
|
@ -395,10 +395,17 @@ class BleachHTMLTokenizer(HTMLTokenizer):
|
|||
# followed by a series of characters. It's treated as a tag
|
||||
# name that abruptly ends, but we should treat that like
|
||||
# character data
|
||||
yield {
|
||||
"type": TAG_TOKEN_TYPE_CHARACTERS,
|
||||
"data": "<" + self.currentToken["name"],
|
||||
}
|
||||
yield {"type": TAG_TOKEN_TYPE_CHARACTERS, "data": self.stream.get_tag()}
|
||||
elif last_error_token["data"] in (
|
||||
"eof-in-attribute-name",
|
||||
"eof-in-attribute-value-no-quotes",
|
||||
):
|
||||
# Handle the case where the text being parsed ends with <
|
||||
# followed by a series of characters and then space and then
|
||||
# more characters. It's treated as a tag name followed by an
|
||||
# attribute that abruptly ends, but we should treat that like
|
||||
# character data.
|
||||
yield {"type": TAG_TOKEN_TYPE_CHARACTERS, "data": self.stream.get_tag()}
|
||||
else:
|
||||
yield last_error_token
|
||||
|
||||
|
|
|
@ -45,8 +45,8 @@ def build_url_re(tlds=TLDS, protocols=html5lib_shim.allowed_protocols):
|
|||
r"""\(* # Match any opening parentheses.
|
||||
\b(?<![@.])(?:(?:{0}):/{{0,3}}(?:(?:\w+:)?\w+@)?)? # http://
|
||||
([\w-]+\.)+(?:{1})(?:\:[0-9]+)?(?!\.\w)\b # xx.yy.tld(:##)?
|
||||
(?:[/?][^\s\{{\}}\|\\\^\[\]`<>"]*)?
|
||||
# /path/zz (excluding "unsafe" chars from RFC 1738,
|
||||
(?:[/?][^\s\{{\}}\|\\\^`<>"]*)?
|
||||
# /path/zz (excluding "unsafe" chars from RFC 3986,
|
||||
# except for # and ~, which happen in practice)
|
||||
""".format(
|
||||
"|".join(sorted(protocols)), "|".join(sorted(tlds))
|
||||
|
@ -591,7 +591,7 @@ class LinkifyFilter(html5lib_shim.Filter):
|
|||
in_a = False
|
||||
token_buffer = []
|
||||
else:
|
||||
token_buffer.append(token)
|
||||
token_buffer.extend(list(self.extract_entities(token)))
|
||||
continue
|
||||
|
||||
if token["type"] in ["StartTag", "EmptyTag"]:
|
||||
|
|
|
@ -15,8 +15,8 @@ documentation: http://www.crummy.com/software/BeautifulSoup/bs4/doc/
|
|||
"""
|
||||
|
||||
__author__ = "Leonard Richardson (leonardr@segfault.org)"
|
||||
__version__ = "4.12.2"
|
||||
__copyright__ = "Copyright (c) 2004-2023 Leonard Richardson"
|
||||
__version__ = "4.12.3"
|
||||
__copyright__ = "Copyright (c) 2004-2024 Leonard Richardson"
|
||||
# Use of this source code is governed by the MIT license.
|
||||
__license__ = "MIT"
|
||||
|
||||
|
|
|
@ -514,15 +514,19 @@ class DetectsXMLParsedAsHTML(object):
|
|||
XML_PREFIX_B = b'<?xml'
|
||||
|
||||
@classmethod
|
||||
def warn_if_markup_looks_like_xml(cls, markup):
|
||||
def warn_if_markup_looks_like_xml(cls, markup, stacklevel=3):
|
||||
"""Perform a check on some markup to see if it looks like XML
|
||||
that's not XHTML. If so, issue a warning.
|
||||
|
||||
This is much less reliable than doing the check while parsing,
|
||||
but some of the tree builders can't do that.
|
||||
|
||||
:param stacklevel: The stacklevel of the code calling this
|
||||
function.
|
||||
|
||||
:return: True if the markup looks like non-XHTML XML, False
|
||||
otherwise.
|
||||
|
||||
"""
|
||||
if isinstance(markup, bytes):
|
||||
prefix = cls.XML_PREFIX_B
|
||||
|
@ -535,15 +539,16 @@ class DetectsXMLParsedAsHTML(object):
|
|||
and markup.startswith(prefix)
|
||||
and not looks_like_html.search(markup[:500])
|
||||
):
|
||||
cls._warn()
|
||||
cls._warn(stacklevel=stacklevel+2)
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def _warn(cls):
|
||||
def _warn(cls, stacklevel=5):
|
||||
"""Issue a warning about XML being parsed as HTML."""
|
||||
warnings.warn(
|
||||
XMLParsedAsHTMLWarning.MESSAGE, XMLParsedAsHTMLWarning
|
||||
XMLParsedAsHTMLWarning.MESSAGE, XMLParsedAsHTMLWarning,
|
||||
stacklevel=stacklevel
|
||||
)
|
||||
|
||||
def _initialize_xml_detector(self):
|
||||
|
|
|
@ -77,7 +77,9 @@ class HTML5TreeBuilder(HTMLTreeBuilder):
|
|||
|
||||
# html5lib only parses HTML, so if it's given XML that's worth
|
||||
# noting.
|
||||
DetectsXMLParsedAsHTML.warn_if_markup_looks_like_xml(markup)
|
||||
DetectsXMLParsedAsHTML.warn_if_markup_looks_like_xml(
|
||||
markup, stacklevel=3
|
||||
)
|
||||
|
||||
yield (markup, None, None, False)
|
||||
|
||||
|
|
|
@ -378,10 +378,10 @@ class HTMLParserTreeBuilder(HTMLTreeBuilder):
|
|||
parser.soup = self.soup
|
||||
try:
|
||||
parser.feed(markup)
|
||||
parser.close()
|
||||
except AssertionError as e:
|
||||
# html.parser raises AssertionError in rare cases to
|
||||
# indicate a fatal problem with the markup, especially
|
||||
# when there's an error in the doctype declaration.
|
||||
raise ParserRejectedMarkup(e)
|
||||
parser.close()
|
||||
parser.already_closed_empty_element = []
|
||||
|
|
|
@ -179,7 +179,9 @@ class LXMLTreeBuilderForXML(TreeBuilder):
|
|||
self.processing_instruction_class = ProcessingInstruction
|
||||
# We're in HTML mode, so if we're given XML, that's worth
|
||||
# noting.
|
||||
DetectsXMLParsedAsHTML.warn_if_markup_looks_like_xml(markup)
|
||||
DetectsXMLParsedAsHTML.warn_if_markup_looks_like_xml(
|
||||
markup, stacklevel=3
|
||||
)
|
||||
else:
|
||||
self.processing_instruction_class = XMLProcessingInstruction
|
||||
|
||||
|
|
|
@ -1356,7 +1356,7 @@ class Tag(PageElement):
|
|||
This is the first step in the deepcopy process.
|
||||
"""
|
||||
clone = type(self)(
|
||||
None, self.builder, self.name, self.namespace,
|
||||
None, None, self.name, self.namespace,
|
||||
self.prefix, self.attrs, is_xml=self._is_xml,
|
||||
sourceline=self.sourceline, sourcepos=self.sourcepos,
|
||||
can_be_empty_element=self.can_be_empty_element,
|
||||
|
@ -1845,6 +1845,11 @@ class Tag(PageElement):
|
|||
return space_before + s + space_after
|
||||
|
||||
def _format_tag(self, eventual_encoding, formatter, opening):
|
||||
if self.hidden:
|
||||
# A hidden tag is invisible, although its contents
|
||||
# are visible.
|
||||
return ''
|
||||
|
||||
# A tag starts with the < character (see below).
|
||||
|
||||
# Then the / character, if this is a closing tag.
|
||||
|
|
|
@ -51,7 +51,7 @@ class Formatter(EntitySubstitution):
|
|||
void_element_close_prefix='/', cdata_containing_tags=None,
|
||||
empty_attributes_are_booleans=False, indent=1,
|
||||
):
|
||||
"""Constructor.
|
||||
r"""Constructor.
|
||||
|
||||
:param language: This should be Formatter.XML if you are formatting
|
||||
XML markup and Formatter.HTML if you are formatting HTML markup.
|
||||
|
@ -76,7 +76,7 @@ class Formatter(EntitySubstitution):
|
|||
negative, or "" will only insert newlines. Using a
|
||||
positive integer indent indents that many spaces per
|
||||
level. If indent is a string (such as "\t"), that string
|
||||
is used to indent each level. The default behavior to
|
||||
is used to indent each level. The default behavior is to
|
||||
indent one space per level.
|
||||
"""
|
||||
self.language = language
|
||||
|
|
|
@ -1105,7 +1105,7 @@ class XMLTreeBuilderSmokeTest(TreeBuilderSmokeTest):
|
|||
doc = """<?xml version="1.0" encoding="utf-8"?>
|
||||
<Document xmlns="http://example.com/ns0"
|
||||
xmlns:ns1="http://example.com/ns1"
|
||||
xmlns:ns2="http://example.com/ns2"
|
||||
xmlns:ns2="http://example.com/ns2">
|
||||
<ns1:tag>foo</ns1:tag>
|
||||
<ns1:tag>bar</ns1:tag>
|
||||
<ns2:tag key="value">baz</ns2:tag>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<20><> <20> <css
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
˙ ><applet></applet><applet></applet><apple|><applet><applet><appl›„><applet><applet></applet></applet></applet></applet><applet></applet><apple>t<applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet>et><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><azplet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><plet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet><applet></applet></applet></applet></applet></appt></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet></applet><<meta charset=utf-8>
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
- ˙˙ <math><select><mi><select><select>t
|
Binary file not shown.
|
@ -14,30 +14,75 @@ from bs4 import (
|
|||
BeautifulSoup,
|
||||
ParserRejectedMarkup,
|
||||
)
|
||||
try:
|
||||
from soupsieve.util import SelectorSyntaxError
|
||||
import lxml
|
||||
import html5lib
|
||||
fully_fuzzable = True
|
||||
except ImportError:
|
||||
fully_fuzzable = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(not fully_fuzzable, reason="Prerequisites for fuzz tests are not installed.")
|
||||
class TestFuzz(object):
|
||||
|
||||
# Test case markup files from fuzzers are given this extension so
|
||||
# they can be included in builds.
|
||||
TESTCASE_SUFFIX = ".testcase"
|
||||
|
||||
# Copied 20230512 from
|
||||
# https://github.com/google/oss-fuzz/blob/4ac6a645a197a695fe76532251feb5067076b3f3/projects/bs4/bs4_fuzzer.py
|
||||
#
|
||||
# Copying the code lets us precisely duplicate the behavior of
|
||||
# oss-fuzz. The downside is that this code changes over time, so
|
||||
# multiple copies of the code must be kept around to run against
|
||||
# older tests. I'm not sure what to do about this, but I may
|
||||
# retire old tests after a time.
|
||||
def fuzz_test_with_css(self, filename):
|
||||
data = self.__markup(filename)
|
||||
parsers = ['lxml-xml', 'html5lib', 'html.parser', 'lxml']
|
||||
try:
|
||||
idx = int(data[0]) % len(parsers)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
css_selector, data = data[1:10], data[10:]
|
||||
|
||||
try:
|
||||
soup = BeautifulSoup(data[1:], features=parsers[idx])
|
||||
except ParserRejectedMarkup:
|
||||
return
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
list(soup.find_all(True))
|
||||
try:
|
||||
soup.css.select(css_selector.decode('utf-8', 'replace'))
|
||||
except SelectorSyntaxError:
|
||||
return
|
||||
soup.prettify()
|
||||
|
||||
# This class of error has been fixed by catching a less helpful
|
||||
# exception from html.parser and raising ParserRejectedMarkup
|
||||
# instead.
|
||||
@pytest.mark.parametrize(
|
||||
"filename", [
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-5703933063462912",
|
||||
"crash-ffbdfa8a2b26f13537b68d3794b0478a4090ee4a",
|
||||
]
|
||||
)
|
||||
def test_rejected_markup(self, filename):
|
||||
markup = self.__markup(filename)
|
||||
with pytest.raises(ParserRejectedMarkup):
|
||||
BeautifulSoup(markup, 'html.parser')
|
||||
|
||||
|
||||
# This class of error has to do with very deeply nested documents
|
||||
# which overflow the Python call stack when the tree is converted
|
||||
# to a string. This is an issue with Beautiful Soup which was fixed
|
||||
# as part of [bug=1471755].
|
||||
#
|
||||
# These test cases are in the older format that doesn't specify
|
||||
# which parser to use or give a CSS selector.
|
||||
@pytest.mark.parametrize(
|
||||
"filename", [
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-5984173902397440",
|
||||
|
@ -46,18 +91,44 @@ class TestFuzz(object):
|
|||
"clusterfuzz-testcase-minimized-bs4_fuzzer-6450958476902400",
|
||||
]
|
||||
)
|
||||
def test_deeply_nested_document(self, filename):
|
||||
def test_deeply_nested_document_without_css(self, filename):
|
||||
# Parsing the document and encoding it back to a string is
|
||||
# sufficient to demonstrate that the overflow problem has
|
||||
# been fixed.
|
||||
markup = self.__markup(filename)
|
||||
BeautifulSoup(markup, 'html.parser').encode()
|
||||
|
||||
# This class of error has to do with very deeply nested documents
|
||||
# which overflow the Python call stack when the tree is converted
|
||||
# to a string. This is an issue with Beautiful Soup which was fixed
|
||||
# as part of [bug=1471755].
|
||||
@pytest.mark.parametrize(
|
||||
"filename", [
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-5000587759190016",
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-5375146639360000",
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-5492400320282624",
|
||||
]
|
||||
)
|
||||
def test_deeply_nested_document(self, filename):
|
||||
self.fuzz_test_with_css(filename)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename", [
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-4670634698080256",
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-5270998950477824",
|
||||
]
|
||||
)
|
||||
def test_soupsieve_errors(self, filename):
|
||||
self.fuzz_test_with_css(filename)
|
||||
|
||||
# This class of error represents problems with html5lib's parser,
|
||||
# not Beautiful Soup. I use
|
||||
# https://github.com/html5lib/html5lib-python/issues/568 to notify
|
||||
# the html5lib developers of these issues.
|
||||
@pytest.mark.skip("html5lib problems")
|
||||
#
|
||||
# These test cases are in the older format that doesn't specify
|
||||
# which parser to use or give a CSS selector.
|
||||
@pytest.mark.skip(reason="html5lib-specific problems")
|
||||
@pytest.mark.parametrize(
|
||||
"filename", [
|
||||
# b"""ÿ<!DOCTyPEV PUBLIC'''Ð'"""
|
||||
|
@ -68,7 +139,7 @@ class TestFuzz(object):
|
|||
|
||||
# b'-<math><sElect><mi><sElect><sElect>'
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-5843991618256896",
|
||||
|
||||
|
||||
# b'ñ<table><svg><html>'
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-6241471367348224",
|
||||
|
||||
|
@ -79,10 +150,24 @@ class TestFuzz(object):
|
|||
"crash-0d306a50c8ed8bcd0785b67000fcd5dea1d33f08"
|
||||
]
|
||||
)
|
||||
def test_html5lib_parse_errors(self, filename):
|
||||
def test_html5lib_parse_errors_without_css(self, filename):
|
||||
markup = self.__markup(filename)
|
||||
print(BeautifulSoup(markup, 'html5lib').encode())
|
||||
|
||||
# This class of error represents problems with html5lib's parser,
|
||||
# not Beautiful Soup. I use
|
||||
# https://github.com/html5lib/html5lib-python/issues/568 to notify
|
||||
# the html5lib developers of these issues.
|
||||
@pytest.mark.skip(reason="html5lib-specific problems")
|
||||
@pytest.mark.parametrize(
|
||||
"filename", [
|
||||
# b'- \xff\xff <math>\x10<select><mi><select><select>t'
|
||||
"clusterfuzz-testcase-minimized-bs4_fuzzer-6306874195312640",
|
||||
]
|
||||
)
|
||||
def test_html5lib_parse_errors(self, filename):
|
||||
self.fuzz_test_with_css(filename)
|
||||
|
||||
def __markup(self, filename):
|
||||
if not filename.endswith(self.TESTCASE_SUFFIX):
|
||||
filename += self.TESTCASE_SUFFIX
|
||||
|
|
|
@ -219,3 +219,16 @@ class TestMultiValuedAttributes(SoupTest):
|
|||
)
|
||||
assert soup.a['class'] == 'foo'
|
||||
assert soup.a['id'] == ['bar']
|
||||
|
||||
def test_hidden_tag_is_invisible(self):
|
||||
# Setting .hidden on a tag makes it invisible in output, but
|
||||
# leaves its contents visible.
|
||||
#
|
||||
# This is not a documented or supported feature of Beautiful
|
||||
# Soup (e.g. NavigableString doesn't support .hidden even
|
||||
# though it could), but some people use it and it's not
|
||||
# hurting anything to verify that it keeps working.
|
||||
#
|
||||
soup = self.soup('<div id="1"><span id="2">a string</span></div>')
|
||||
soup.span.hidden = True
|
||||
assert '<div id="1">a string</div>' == str(soup.div)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from .core import contents, where
|
||||
|
||||
__all__ = ["contents", "where"]
|
||||
__version__ = "2023.07.22"
|
||||
__version__ = "2024.02.02"
|
||||
|
|
|
@ -245,34 +245,6 @@ mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK
|
|||
4SVhM7JZG+Ju1zdXtg2pEto=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: O=SECOM Trust.net OU=Security Communication RootCA1
|
||||
# Subject: O=SECOM Trust.net OU=Security Communication RootCA1
|
||||
# Label: "Security Communication Root CA"
|
||||
# Serial: 0
|
||||
# MD5 Fingerprint: f1:bc:63:6a:54:e0:b5:27:f5:cd:e7:1a:e3:4d:6e:4a
|
||||
# SHA1 Fingerprint: 36:b1:2b:49:f9:81:9e:d7:4c:9e:bc:38:0f:c6:56:8f:5d:ac:b2:f7
|
||||
# SHA256 Fingerprint: e7:5e:72:ed:9f:56:0e:ec:6e:b4:80:00:73:a4:3f:c3:ad:19:19:5a:39:22:82:01:78:95:97:4a:99:02:6b:6c
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDWjCCAkKgAwIBAgIBADANBgkqhkiG9w0BAQUFADBQMQswCQYDVQQGEwJKUDEY
|
||||
MBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYDVQQLEx5TZWN1cml0eSBDb21t
|
||||
dW5pY2F0aW9uIFJvb3RDQTEwHhcNMDMwOTMwMDQyMDQ5WhcNMjMwOTMwMDQyMDQ5
|
||||
WjBQMQswCQYDVQQGEwJKUDEYMBYGA1UEChMPU0VDT00gVHJ1c3QubmV0MScwJQYD
|
||||
VQQLEx5TZWN1cml0eSBDb21tdW5pY2F0aW9uIFJvb3RDQTEwggEiMA0GCSqGSIb3
|
||||
DQEBAQUAA4IBDwAwggEKAoIBAQCzs/5/022x7xZ8V6UMbXaKL0u/ZPtM7orw8yl8
|
||||
9f/uKuDp6bpbZCKamm8sOiZpUQWZJtzVHGpxxpp9Hp3dfGzGjGdnSj74cbAZJ6kJ
|
||||
DKaVv0uMDPpVmDvY6CKhS3E4eayXkmmziX7qIWgGmBSWh9JhNrxtJ1aeV+7AwFb9
|
||||
Ms+k2Y7CI9eNqPPYJayX5HA49LY6tJ07lyZDo6G8SVlyTCMwhwFY9k6+HGhWZq/N
|
||||
QV3Is00qVUarH9oe4kA92819uZKAnDfdDJZkndwi92SL32HeFZRSFaB9UslLqCHJ
|
||||
xrHty8OVYNEP8Ktw+N/LTX7s1vqr2b1/VPKl6Xn62dZ2JChzAgMBAAGjPzA9MB0G
|
||||
A1UdDgQWBBSgc0mZaNyFW2XjmygvV5+9M7wHSDALBgNVHQ8EBAMCAQYwDwYDVR0T
|
||||
AQH/BAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEAaECpqLvkT115swW1F7NgE+vG
|
||||
kl3g0dNq/vu+m22/xwVtWSDEHPC32oRYAmP6SBbvT6UL90qY8j+eG61Ha2POCEfr
|
||||
Uj94nK9NrvjVT8+amCoQQTlSxN3Zmw7vkwGusi7KaEIkQmywszo+zenaSMQVy+n5
|
||||
Bw+SUEmK3TGXX8npN6o7WWWXlDLJs58+OmJYxUmtYg5xpTKqL8aJdkNAExNnPaJU
|
||||
JRDL8Try2frbSVa7pv6nQTXD4IhhyYjH3zYQIphZ6rBK+1YWc26sTfcioU+tHXot
|
||||
RSflMMFe8toTyyVCUZVHA4xsIcx0Qu1T/zOLjw9XARYvz6buyXAiFL39vmwLAw==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com
|
||||
# Subject: CN=XRamp Global Certification Authority O=XRamp Security Services Inc OU=www.xrampsecurity.com
|
||||
# Label: "XRamp Global CA Root"
|
||||
|
@ -881,49 +853,6 @@ Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
|
|||
WD9f
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068
|
||||
# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068
|
||||
# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068"
|
||||
# Serial: 6047274297262753887
|
||||
# MD5 Fingerprint: 73:3a:74:7a:ec:bb:a3:96:a6:c2:e4:e2:c8:9b:c0:c3
|
||||
# SHA1 Fingerprint: ae:c5:fb:3f:c8:e1:bf:c4:e5:4f:03:07:5a:9a:e8:00:b7:f7:b6:fa
|
||||
# SHA256 Fingerprint: 04:04:80:28:bf:1f:28:64:d4:8f:9a:d4:d8:32:94:36:6a:82:88:56:55:3f:3b:14:30:3f:90:14:7f:5d:40:ef
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
jLMi6Et8Vcad+qMUu2WFbm5PEn4KPJ2V
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Izenpe.com O=IZENPE S.A.
|
||||
# Subject: CN=Izenpe.com O=IZENPE S.A.
|
||||
# Label: "Izenpe.com"
|
||||
|
@ -4633,3 +4562,253 @@ o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5
|
|||
dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE
|
||||
oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc.
|
||||
# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc.
|
||||
# Label: "TrustAsia Global Root CA G3"
|
||||
# Serial: 576386314500428537169965010905813481816650257167
|
||||
# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04
|
||||
# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7
|
||||
# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
FGWsJwt0ivKH
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc.
|
||||
# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc.
|
||||
# Label: "TrustAsia Global Root CA G4"
|
||||
# Serial: 451799571007117016466790293371524403291602933463
|
||||
# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb
|
||||
# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a
|
||||
# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw
|
||||
WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs
|
||||
IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y
|
||||
MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD
|
||||
VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz
|
||||
dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx
|
||||
s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw
|
||||
LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij
|
||||
YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD
|
||||
pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE
|
||||
AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR
|
||||
UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj
|
||||
/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=CommScope Public Trust ECC Root-01 O=CommScope
|
||||
# Subject: CN=CommScope Public Trust ECC Root-01 O=CommScope
|
||||
# Label: "CommScope Public Trust ECC Root-01"
|
||||
# Serial: 385011430473757362783587124273108818652468453534
|
||||
# MD5 Fingerprint: 3a:40:a7:fc:03:8c:9c:38:79:2f:3a:a2:6c:b6:0a:16
|
||||
# SHA1 Fingerprint: 07:86:c0:d8:dd:8e:c0:80:98:06:98:d0:58:7a:ef:de:a6:cc:a2:5d
|
||||
# SHA256 Fingerprint: 11:43:7c:da:7b:b4:5e:41:36:5f:45:b3:9a:38:98:6b:0d:e0:0d:ef:34:8e:0c:7b:b0:87:36:33:80:0b:c3:8b
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICHTCCAaOgAwIBAgIUQ3CCd89NXTTxyq4yLzf39H91oJ4wCgYIKoZIzj0EAwMw
|
||||
TjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29t
|
||||
bVNjb3BlIFB1YmxpYyBUcnVzdCBFQ0MgUm9vdC0wMTAeFw0yMTA0MjgxNzM1NDNa
|
||||
Fw00NjA0MjgxNzM1NDJaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2Nv
|
||||
cGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDEw
|
||||
djAQBgcqhkjOPQIBBgUrgQQAIgNiAARLNumuV16ocNfQj3Rid8NeeqrltqLxeP0C
|
||||
flfdkXmcbLlSiFS8LwS+uM32ENEp7LXQoMPwiXAZu1FlxUOcw5tjnSCDPgYLpkJE
|
||||
hRGnSjot6dZoL0hOUysHP029uax3OVejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
|
||||
VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSOB2LAUN3GGQYARnQE9/OufXVNMDAKBggq
|
||||
hkjOPQQDAwNoADBlAjEAnDPfQeMjqEI2Jpc1XHvr20v4qotzVRVcrHgpD7oh2MSg
|
||||
2NED3W3ROT3Ek2DS43KyAjB8xX6I01D1HiXo+k515liWpDVfG2XqYZpwI7UNo5uS
|
||||
Um9poIyNStDuiw7LR47QjRE=
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=CommScope Public Trust ECC Root-02 O=CommScope
|
||||
# Subject: CN=CommScope Public Trust ECC Root-02 O=CommScope
|
||||
# Label: "CommScope Public Trust ECC Root-02"
|
||||
# Serial: 234015080301808452132356021271193974922492992893
|
||||
# MD5 Fingerprint: 59:b0:44:d5:65:4d:b8:5c:55:19:92:02:b6:d1:94:b2
|
||||
# SHA1 Fingerprint: 3c:3f:ef:57:0f:fe:65:93:86:9e:a0:fe:b0:f6:ed:8e:d1:13:c7:e5
|
||||
# SHA256 Fingerprint: 2f:fb:7f:81:3b:bb:b3:c8:9a:b4:e8:16:2d:0f:16:d7:15:09:a8:30:cc:9d:73:c2:62:e5:14:08:75:d1:ad:4a
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICHDCCAaOgAwIBAgIUKP2ZYEFHpgE6yhR7H+/5aAiDXX0wCgYIKoZIzj0EAwMw
|
||||
TjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29t
|
||||
bVNjb3BlIFB1YmxpYyBUcnVzdCBFQ0MgUm9vdC0wMjAeFw0yMTA0MjgxNzQ0NTRa
|
||||
Fw00NjA0MjgxNzQ0NTNaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2Nv
|
||||
cGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDIw
|
||||
djAQBgcqhkjOPQIBBgUrgQQAIgNiAAR4MIHoYx7l63FRD/cHB8o5mXxO1Q/MMDAL
|
||||
j2aTPs+9xYa9+bG3tD60B8jzljHz7aRP+KNOjSkVWLjVb3/ubCK1sK9IRQq9qEmU
|
||||
v4RDsNuESgMjGWdqb8FuvAY5N9GIIvejQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
|
||||
VR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTmGHX/72DehKT1RsfeSlXjMjZ59TAKBggq
|
||||
hkjOPQQDAwNnADBkAjAmc0l6tqvmSfR9Uj/UQQSugEODZXW5hYA4O9Zv5JOGq4/n
|
||||
ich/m35rChJVYaoR4HkCMHfoMXGsPHED1oQmHhS48zs73u1Z/GtMMH9ZzkXpc2AV
|
||||
mkzw5l4lIhVtwodZ0LKOag==
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=CommScope Public Trust RSA Root-01 O=CommScope
|
||||
# Subject: CN=CommScope Public Trust RSA Root-01 O=CommScope
|
||||
# Label: "CommScope Public Trust RSA Root-01"
|
||||
# Serial: 354030733275608256394402989253558293562031411421
|
||||
# MD5 Fingerprint: 0e:b4:15:bc:87:63:5d:5d:02:73:d4:26:38:68:73:d8
|
||||
# SHA1 Fingerprint: 6d:0a:5f:f7:b4:23:06:b4:85:b3:b7:97:64:fc:ac:75:f5:33:f2:93
|
||||
# SHA256 Fingerprint: 02:bd:f9:6e:2a:45:dd:9b:f1:8f:c7:e1:db:df:21:a0:37:9b:a3:c9:c2:61:03:44:cf:d8:d6:06:fe:c1:ed:81
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFbDCCA1SgAwIBAgIUPgNJgXUWdDGOTKvVxZAplsU5EN0wDQYJKoZIhvcNAQEL
|
||||
BQAwTjELMAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwi
|
||||
Q29tbVNjb3BlIFB1YmxpYyBUcnVzdCBSU0EgUm9vdC0wMTAeFw0yMTA0MjgxNjQ1
|
||||
NTRaFw00NjA0MjgxNjQ1NTNaME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21t
|
||||
U2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3Qt
|
||||
MDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwSGWjDR1C45FtnYSk
|
||||
YZYSwu3D2iM0GXb26v1VWvZVAVMP8syMl0+5UMuzAURWlv2bKOx7dAvnQmtVzslh
|
||||
suitQDy6uUEKBU8bJoWPQ7VAtYXR1HHcg0Hz9kXHgKKEUJdGzqAMxGBWBB0HW0al
|
||||
DrJLpA6lfO741GIDuZNqihS4cPgugkY4Iw50x2tBt9Apo52AsH53k2NC+zSDO3Oj
|
||||
WiE260f6GBfZumbCk6SP/F2krfxQapWsvCQz0b2If4b19bJzKo98rwjyGpg/qYFl
|
||||
P8GMicWWMJoKz/TUyDTtnS+8jTiGU+6Xn6myY5QXjQ/cZip8UlF1y5mO6D1cv547
|
||||
KI2DAg+pn3LiLCuz3GaXAEDQpFSOm117RTYm1nJD68/A6g3czhLmfTifBSeolz7p
|
||||
UcZsBSjBAg/pGG3svZwG1KdJ9FQFa2ww8esD1eo9anbCyxooSU1/ZOD6K9pzg4H/
|
||||
kQO9lLvkuI6cMmPNn7togbGEW682v3fuHX/3SZtS7NJ3Wn2RnU3COS3kuoL4b/JO
|
||||
Hg9O5j9ZpSPcPYeoKFgo0fEbNttPxP/hjFtyjMcmAyejOQoBqsCyMWCDIqFPEgkB
|
||||
Ea801M/XrmLTBQe0MXXgDW1XT2mH+VepuhX2yFJtocucH+X8eKg1mp9BFM6ltM6U
|
||||
CBwJrVbl2rZJmkrqYxhTnCwuwwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G
|
||||
A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUN12mmnQywsL5x6YVEFm45P3luG0wDQYJ
|
||||
KoZIhvcNAQELBQADggIBAK+nz97/4L1CjU3lIpbfaOp9TSp90K09FlxD533Ahuh6
|
||||
NWPxzIHIxgvoLlI1pKZJkGNRrDSsBTtXAOnTYtPZKdVUvhwQkZyybf5Z/Xn36lbQ
|
||||
nmhUQo8mUuJM3y+Xpi/SB5io82BdS5pYV4jvguX6r2yBS5KPQJqTRlnLX3gWsWc+
|
||||
QgvfKNmwrZggvkN80V4aCRckjXtdlemrwWCrWxhkgPut4AZ9HcpZuPN4KWfGVh2v
|
||||
trV0KnahP/t1MJ+UXjulYPPLXAziDslg+MkfFoom3ecnf+slpoq9uC02EJqxWE2a
|
||||
aE9gVOX2RhOOiKy8IUISrcZKiX2bwdgt6ZYD9KJ0DLwAHb/WNyVntHKLr4W96ioD
|
||||
j8z7PEQkguIBpQtZtjSNMgsSDesnwv1B10A8ckYpwIzqug/xBpMu95yo9GA+o/E4
|
||||
Xo4TwbM6l4c/ksp4qRyv0LAbJh6+cOx69TOY6lz/KwsETkPdY34Op054A5U+1C0w
|
||||
lREQKC6/oAI+/15Z0wUOlV9TRe9rh9VIzRamloPh37MG88EU26fsHItdkJANclHn
|
||||
YfkUyq+Dj7+vsQpZXdxc1+SWrVtgHdqul7I52Qb1dgAT+GhMIbA1xNxVssnBQVoc
|
||||
icCMb3SgazNNtQEo/a2tiRc7ppqEvOuM6sRxJKi6KfkIsidWNTJf6jn7MZrVGczw
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=CommScope Public Trust RSA Root-02 O=CommScope
|
||||
# Subject: CN=CommScope Public Trust RSA Root-02 O=CommScope
|
||||
# Label: "CommScope Public Trust RSA Root-02"
|
||||
# Serial: 480062499834624527752716769107743131258796508494
|
||||
# MD5 Fingerprint: e1:29:f9:62:7b:76:e2:96:6d:f3:d4:d7:0f:ae:1f:aa
|
||||
# SHA1 Fingerprint: ea:b0:e2:52:1b:89:93:4c:11:68:f2:d8:9a:ac:22:4c:a3:8a:57:ae
|
||||
# SHA256 Fingerprint: ff:e9:43:d7:93:42:4b:4f:7c:44:0c:1c:3d:64:8d:53:63:f3:4b:82:dc:87:aa:7a:9f:11:8f:c5:de:e1:01:f1
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH
|
||||
# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH
|
||||
# Label: "Telekom Security TLS ECC Root 2020"
|
||||
# Serial: 72082518505882327255703894282316633856
|
||||
# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd
|
||||
# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec
|
||||
# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw
|
||||
CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH
|
||||
bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw
|
||||
MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx
|
||||
JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE
|
||||
AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49
|
||||
AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O
|
||||
tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP
|
||||
f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f
|
||||
MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA
|
||||
MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di
|
||||
z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn
|
||||
27iQ7t0l
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH
|
||||
# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH
|
||||
# Label: "Telekom Security TLS RSA Root 2023"
|
||||
# Serial: 44676229530606711399881795178081572759
|
||||
# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2
|
||||
# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93
|
||||
# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
dTdmQRCsu/WU48IxK63nI1bMNSWSs1A=
|
||||
-----END CERTIFICATE-----
|
||||
|
|
|
@ -5,6 +5,10 @@ certifi.py
|
|||
This module returns the installation location of cacert.pem or its contents.
|
||||
"""
|
||||
import sys
|
||||
import atexit
|
||||
|
||||
def exit_cacert_ctx() -> None:
|
||||
_CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr]
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
|
@ -35,6 +39,7 @@ if sys.version_info >= (3, 11):
|
|||
# we will also store that at the global level as well.
|
||||
_CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem"))
|
||||
_CACERT_PATH = str(_CACERT_CTX.__enter__())
|
||||
atexit.register(exit_cacert_ctx)
|
||||
|
||||
return _CACERT_PATH
|
||||
|
||||
|
@ -70,6 +75,7 @@ elif sys.version_info >= (3, 7):
|
|||
# we will also store that at the global level as well.
|
||||
_CACERT_CTX = get_path("certifi", "cacert.pem")
|
||||
_CACERT_PATH = str(_CACERT_CTX.__enter__())
|
||||
atexit.register(exit_cacert_ctx)
|
||||
|
||||
return _CACERT_PATH
|
||||
|
||||
|
|
4
lib/charset_normalizer/__main__.py
Normal file
4
lib/charset_normalizer/__main__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from .cli import cli_detect
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli_detect()
|
File diff suppressed because it is too large
Load diff
|
@ -4,8 +4,13 @@ from collections import Counter
|
|||
from functools import lru_cache
|
||||
from typing import Counter as TypeCounter, Dict, List, Optional, Tuple
|
||||
|
||||
from .assets import FREQUENCIES
|
||||
from .constant import KO_NAMES, LANGUAGE_SUPPORTED_COUNT, TOO_SMALL_SEQUENCE, ZH_NAMES
|
||||
from .constant import (
|
||||
FREQUENCIES,
|
||||
KO_NAMES,
|
||||
LANGUAGE_SUPPORTED_COUNT,
|
||||
TOO_SMALL_SEQUENCE,
|
||||
ZH_NAMES,
|
||||
)
|
||||
from .md import is_suspiciously_successive_range
|
||||
from .models import CoherenceMatches
|
||||
from .utils import (
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
from .__main__ import cli_detect, query_yes_no
|
||||
|
||||
__all__ = (
|
||||
"cli_detect",
|
||||
"query_yes_no",
|
||||
)
|
File diff suppressed because it is too large
Load diff
|
@ -9,7 +9,8 @@ from .constant import (
|
|||
)
|
||||
from .utils import (
|
||||
is_accentuated,
|
||||
is_ascii,
|
||||
is_arabic,
|
||||
is_arabic_isolated_form,
|
||||
is_case_variable,
|
||||
is_cjk,
|
||||
is_emoticon,
|
||||
|
@ -128,8 +129,9 @@ class TooManyAccentuatedPlugin(MessDetectorPlugin):
|
|||
|
||||
@property
|
||||
def ratio(self) -> float:
|
||||
if self._character_count == 0 or self._character_count < 8:
|
||||
if self._character_count < 8:
|
||||
return 0.0
|
||||
|
||||
ratio_of_accentuation: float = self._accentuated_count / self._character_count
|
||||
return ratio_of_accentuation if ratio_of_accentuation >= 0.35 else 0.0
|
||||
|
||||
|
@ -234,16 +236,13 @@ class SuspiciousRange(MessDetectorPlugin):
|
|||
|
||||
@property
|
||||
def ratio(self) -> float:
|
||||
if self._character_count == 0:
|
||||
if self._character_count <= 24:
|
||||
return 0.0
|
||||
|
||||
ratio_of_suspicious_range_usage: float = (
|
||||
self._suspicious_successive_range_count * 2
|
||||
) / self._character_count
|
||||
|
||||
if ratio_of_suspicious_range_usage < 0.1:
|
||||
return 0.0
|
||||
|
||||
return ratio_of_suspicious_range_usage
|
||||
|
||||
|
||||
|
@ -296,7 +295,11 @@ class SuperWeirdWordPlugin(MessDetectorPlugin):
|
|||
self._is_current_word_bad = True
|
||||
# Word/Buffer ending with an upper case accentuated letter are so rare,
|
||||
# that we will consider them all as suspicious. Same weight as foreign_long suspicious.
|
||||
if is_accentuated(self._buffer[-1]) and self._buffer[-1].isupper():
|
||||
if (
|
||||
is_accentuated(self._buffer[-1])
|
||||
and self._buffer[-1].isupper()
|
||||
and all(_.isupper() for _ in self._buffer) is False
|
||||
):
|
||||
self._foreign_long_count += 1
|
||||
self._is_current_word_bad = True
|
||||
if buffer_length >= 24 and self._foreign_long_watch:
|
||||
|
@ -419,7 +422,7 @@ class ArchaicUpperLowerPlugin(MessDetectorPlugin):
|
|||
|
||||
return
|
||||
|
||||
if self._current_ascii_only is True and is_ascii(character) is False:
|
||||
if self._current_ascii_only is True and character.isascii() is False:
|
||||
self._current_ascii_only = False
|
||||
|
||||
if self._last_alpha_seen is not None:
|
||||
|
@ -455,6 +458,34 @@ class ArchaicUpperLowerPlugin(MessDetectorPlugin):
|
|||
return self._successive_upper_lower_count_final / self._character_count
|
||||
|
||||
|
||||
class ArabicIsolatedFormPlugin(MessDetectorPlugin):
|
||||
def __init__(self) -> None:
|
||||
self._character_count: int = 0
|
||||
self._isolated_form_count: int = 0
|
||||
|
||||
def reset(self) -> None: # pragma: no cover
|
||||
self._character_count = 0
|
||||
self._isolated_form_count = 0
|
||||
|
||||
def eligible(self, character: str) -> bool:
|
||||
return is_arabic(character)
|
||||
|
||||
def feed(self, character: str) -> None:
|
||||
self._character_count += 1
|
||||
|
||||
if is_arabic_isolated_form(character):
|
||||
self._isolated_form_count += 1
|
||||
|
||||
@property
|
||||
def ratio(self) -> float:
|
||||
if self._character_count < 8:
|
||||
return 0.0
|
||||
|
||||
isolated_form_usage: float = self._isolated_form_count / self._character_count
|
||||
|
||||
return isolated_form_usage
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def is_suspiciously_successive_range(
|
||||
unicode_range_a: Optional[str], unicode_range_b: Optional[str]
|
||||
|
@ -522,6 +553,8 @@ def is_suspiciously_successive_range(
|
|||
return False
|
||||
if "Forms" in unicode_range_a or "Forms" in unicode_range_b:
|
||||
return False
|
||||
if unicode_range_a == "Basic Latin" or unicode_range_b == "Basic Latin":
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
|
|
@ -54,16 +54,19 @@ class CharsetMatch:
|
|||
|
||||
# Below 1% difference --> Use Coherence
|
||||
if chaos_difference < 0.01 and coherence_difference > 0.02:
|
||||
# When having a tough decision, use the result that decoded as many multi-byte as possible.
|
||||
if chaos_difference == 0.0 and self.coherence == other.coherence:
|
||||
return self.multi_byte_usage > other.multi_byte_usage
|
||||
return self.coherence > other.coherence
|
||||
elif chaos_difference < 0.01 and coherence_difference <= 0.02:
|
||||
# When having a difficult decision, use the result that decoded as many multi-byte as possible.
|
||||
# preserve RAM usage!
|
||||
if len(self._payload) >= TOO_BIG_SEQUENCE:
|
||||
return self.chaos < other.chaos
|
||||
return self.multi_byte_usage > other.multi_byte_usage
|
||||
|
||||
return self.chaos < other.chaos
|
||||
|
||||
@property
|
||||
def multi_byte_usage(self) -> float:
|
||||
return 1.0 - len(str(self)) / len(self.raw)
|
||||
return 1.0 - (len(str(self)) / len(self.raw))
|
||||
|
||||
def __str__(self) -> str:
|
||||
# Lazy Str Loading
|
||||
|
|
|
@ -32,6 +32,8 @@ def is_accentuated(character: str) -> bool:
|
|||
or "WITH DIAERESIS" in description
|
||||
or "WITH CIRCUMFLEX" in description
|
||||
or "WITH TILDE" in description
|
||||
or "WITH MACRON" in description
|
||||
or "WITH RING ABOVE" in description
|
||||
)
|
||||
|
||||
|
||||
|
@ -69,15 +71,6 @@ def is_latin(character: str) -> bool:
|
|||
return "LATIN" in description
|
||||
|
||||
|
||||
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
|
||||
def is_ascii(character: str) -> bool:
|
||||
try:
|
||||
character.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
|
||||
def is_punctuation(character: str) -> bool:
|
||||
character_category: str = unicodedata.category(character)
|
||||
|
@ -105,7 +98,7 @@ def is_symbol(character: str) -> bool:
|
|||
if character_range is None:
|
||||
return False
|
||||
|
||||
return "Forms" in character_range
|
||||
return "Forms" in character_range and character_category != "Lo"
|
||||
|
||||
|
||||
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
|
||||
|
@ -115,7 +108,7 @@ def is_emoticon(character: str) -> bool:
|
|||
if character_range is None:
|
||||
return False
|
||||
|
||||
return "Emoticons" in character_range
|
||||
return "Emoticons" in character_range or "Pictographs" in character_range
|
||||
|
||||
|
||||
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
|
||||
|
@ -133,12 +126,6 @@ def is_case_variable(character: str) -> bool:
|
|||
return character.islower() != character.isupper()
|
||||
|
||||
|
||||
def is_private_use_only(character: str) -> bool:
|
||||
character_category: str = unicodedata.category(character)
|
||||
|
||||
return character_category == "Co"
|
||||
|
||||
|
||||
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
|
||||
def is_cjk(character: str) -> bool:
|
||||
try:
|
||||
|
@ -189,6 +176,26 @@ def is_thai(character: str) -> bool:
|
|||
return "THAI" in character_name
|
||||
|
||||
|
||||
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
|
||||
def is_arabic(character: str) -> bool:
|
||||
try:
|
||||
character_name = unicodedata.name(character)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return "ARABIC" in character_name
|
||||
|
||||
|
||||
@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION)
|
||||
def is_arabic_isolated_form(character: str) -> bool:
|
||||
try:
|
||||
character_name = unicodedata.name(character)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return "ARABIC" in character_name and "ISOLATED FORM" in character_name
|
||||
|
||||
|
||||
@lru_cache(maxsize=len(UNICODE_RANGES_COMBINED))
|
||||
def is_unicode_range_secondary(range_name: str) -> bool:
|
||||
return any(keyword in range_name for keyword in UNICODE_SECONDARY_RANGE_KEYWORD)
|
||||
|
@ -205,7 +212,7 @@ def is_unprintable(character: str) -> bool:
|
|||
)
|
||||
|
||||
|
||||
def any_specified_encoding(sequence: bytes, search_zone: int = 4096) -> Optional[str]:
|
||||
def any_specified_encoding(sequence: bytes, search_zone: int = 8192) -> Optional[str]:
|
||||
"""
|
||||
Extract using ASCII-only decoder any specified encoding in the first n-bytes.
|
||||
"""
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
Expose version
|
||||
"""
|
||||
|
||||
__version__ = "3.2.0"
|
||||
__version__ = "3.3.2"
|
||||
VERSION = __version__.split(".")
|
||||
|
|
|
@ -452,6 +452,6 @@ class WSGIErrorHandler(logging.Handler):
|
|||
|
||||
class LazyRfc3339UtcTime(object):
|
||||
def __str__(self):
|
||||
"""Return now() in RFC3339 UTC Format."""
|
||||
now = datetime.datetime.now()
|
||||
return now.isoformat('T') + 'Z'
|
||||
"""Return utcnow() in RFC3339 UTC Format."""
|
||||
iso_formatted_now = datetime.datetime.utcnow().isoformat('T')
|
||||
return f'{iso_formatted_now!s}Z'
|
||||
|
|
|
@ -622,13 +622,15 @@ def autovary(ignore=None, debug=False):
|
|||
|
||||
|
||||
def convert_params(exception=ValueError, error=400):
|
||||
"""Convert request params based on function annotations, with error handling.
|
||||
"""Convert request params based on function annotations.
|
||||
|
||||
exception
|
||||
Exception class to catch.
|
||||
This function also processes errors that are subclasses of ``exception``.
|
||||
|
||||
status
|
||||
The HTTP error code to return to the client on failure.
|
||||
:param BaseException exception: Exception class to catch.
|
||||
:type exception: BaseException
|
||||
|
||||
:param error: The HTTP status code to return to the client on failure.
|
||||
:type error: int
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
types = request.handler.callable.__annotations__
|
||||
|
|
|
@ -47,7 +47,9 @@ try:
|
|||
import pstats
|
||||
|
||||
def new_func_strip_path(func_name):
|
||||
"""Make profiler output more readable by adding `__init__` modules' parents
|
||||
"""Add ``__init__`` modules' parents.
|
||||
|
||||
This makes the profiler output more readable.
|
||||
"""
|
||||
filename, line, name = func_name
|
||||
if filename.endswith('__init__.py'):
|
||||
|
|
|
@ -188,7 +188,7 @@ class Parser(configparser.ConfigParser):
|
|||
|
||||
def dict_from_file(self, file):
|
||||
if hasattr(file, 'read'):
|
||||
self.readfp(file)
|
||||
self.read_file(file)
|
||||
else:
|
||||
self.read(file)
|
||||
return self.as_dict()
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
"""Module with helpers for serving static files."""
|
||||
|
||||
import mimetypes
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import stat
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
import unicodedata
|
||||
|
||||
import urllib.parse
|
||||
from email.generator import _make_boundary as make_boundary
|
||||
from io import UnsupportedOperation
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob
|
||||
from cherrypy.lib import cptools, httputil, file_generator_limited
|
||||
from cherrypy.lib import cptools, file_generator_limited, httputil
|
||||
|
||||
|
||||
def _setup_mimetypes():
|
||||
|
@ -185,7 +184,10 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
|
|||
|
||||
|
||||
def _serve_fileobj(fileobj, content_type, content_length, debug=False):
|
||||
"""Internal. Set response.body to the given file object, perhaps ranged."""
|
||||
"""Set ``response.body`` to the given file object, perhaps ranged.
|
||||
|
||||
Internal helper.
|
||||
"""
|
||||
response = cherrypy.serving.response
|
||||
|
||||
# HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
|
||||
|
|
|
@ -494,7 +494,7 @@ class Bus(object):
|
|||
"Cannot reconstruct command from '-c'. "
|
||||
'Ref: https://github.com/cherrypy/cherrypy/issues/1545')
|
||||
except AttributeError:
|
||||
"""It looks Py_GetArgcArgv is completely absent in some environments
|
||||
"""It looks Py_GetArgcArgv's completely absent in some environments
|
||||
|
||||
It is known, that there's no Py_GetArgcArgv in MS Windows and
|
||||
``ctypes`` module is completely absent in Google AppEngine
|
||||
|
|
|
@ -136,6 +136,9 @@ class HTTPTests(helper.CPWebCase):
|
|||
self.assertStatus(200)
|
||||
self.assertBody(b'Hello world!')
|
||||
|
||||
response.close()
|
||||
c.close()
|
||||
|
||||
# Now send a message that has no Content-Length, but does send a body.
|
||||
# Verify that CP times out the socket and responds
|
||||
# with 411 Length Required.
|
||||
|
@ -159,6 +162,9 @@ class HTTPTests(helper.CPWebCase):
|
|||
self.status = str(response.status)
|
||||
self.assertStatus(411)
|
||||
|
||||
response.close()
|
||||
c.close()
|
||||
|
||||
def test_post_multipart(self):
|
||||
alphabet = 'abcdefghijklmnopqrstuvwxyz'
|
||||
# generate file contents for a large post
|
||||
|
@ -184,6 +190,9 @@ class HTTPTests(helper.CPWebCase):
|
|||
parts = ['%s * 65536' % ch for ch in alphabet]
|
||||
self.assertBody(', '.join(parts))
|
||||
|
||||
response.close()
|
||||
c.close()
|
||||
|
||||
def test_post_filename_with_special_characters(self):
|
||||
"""Testing that we can handle filenames with special characters.
|
||||
|
||||
|
@ -217,6 +226,9 @@ class HTTPTests(helper.CPWebCase):
|
|||
self.assertStatus(200)
|
||||
self.assertBody(fname)
|
||||
|
||||
response.close()
|
||||
c.close()
|
||||
|
||||
def test_malformed_request_line(self):
|
||||
if getattr(cherrypy.server, 'using_apache', False):
|
||||
return self.skip('skipped due to known Apache differences...')
|
||||
|
@ -264,6 +276,9 @@ class HTTPTests(helper.CPWebCase):
|
|||
self.body = response.fp.read(20)
|
||||
self.assertBody('Illegal header line.')
|
||||
|
||||
response.close()
|
||||
c.close()
|
||||
|
||||
def test_http_over_https(self):
|
||||
if self.scheme != 'https':
|
||||
return self.skip('skipped (not running HTTPS)... ')
|
||||
|
|
|
@ -150,6 +150,8 @@ class IteratorTest(helper.CPWebCase):
|
|||
self.assertStatus(200)
|
||||
self.assertBody('0')
|
||||
|
||||
itr_conn.close()
|
||||
|
||||
# Now we do the same check with streaming - some classes will
|
||||
# be automatically closed, while others cannot.
|
||||
stream_counts = {}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
"""Basic tests for the CherryPy core: request handling."""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from cheroot.test import webtest
|
||||
|
@ -197,6 +198,33 @@ def test_custom_log_format(log_tracker, monkeypatch, server):
|
|||
)
|
||||
|
||||
|
||||
def test_utc_in_timez(monkeypatch):
|
||||
"""Test that ``LazyRfc3339UtcTime`` is rendered as ``str`` using UTC timestamp."""
|
||||
utcoffset8_local_time_in_naive_utc = (
|
||||
datetime.datetime(
|
||||
year=2020,
|
||||
month=1,
|
||||
day=1,
|
||||
hour=1,
|
||||
minute=23,
|
||||
second=45,
|
||||
tzinfo=datetime.timezone(datetime.timedelta(hours=8)),
|
||||
)
|
||||
.astimezone(datetime.timezone.utc)
|
||||
.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
class mock_datetime:
|
||||
@classmethod
|
||||
def utcnow(cls):
|
||||
return utcoffset8_local_time_in_naive_utc
|
||||
|
||||
monkeypatch.setattr('datetime.datetime', mock_datetime)
|
||||
rfc3339_utc_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
|
||||
expected_time = '2019-12-31T17:23:45Z'
|
||||
assert rfc3339_utc_time == expected_time
|
||||
|
||||
|
||||
def test_timez_log_format(log_tracker, monkeypatch, server):
|
||||
"""Test a customized access_log_format string, which is a
|
||||
feature of _cplogging.LogManager.access()."""
|
||||
|
|
|
@ -38,7 +38,7 @@ CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAA
|
|||
URI_SCHEME = "cloudinary"
|
||||
API_VERSION = "v1_1"
|
||||
|
||||
VERSION = "1.34.0"
|
||||
VERSION = "1.39.1"
|
||||
|
||||
_USER_PLATFORM_DETAILS = "; ".join((platform(), "Python {}".format(python_version())))
|
||||
|
||||
|
@ -741,7 +741,11 @@ class CloudinaryResource(object):
|
|||
:return: Video tag
|
||||
"""
|
||||
public_id = options.get('public_id', self.public_id)
|
||||
source = re.sub(r"\.({0})$".format("|".join(self.default_source_types())), '', public_id)
|
||||
use_fetch_format = options.get('use_fetch_format', config().use_fetch_format)
|
||||
if not use_fetch_format:
|
||||
source = re.sub(r"\.({0})$".format("|".join(self.default_source_types())), '', public_id)
|
||||
else:
|
||||
source = public_id
|
||||
|
||||
custom_attributes = options.pop("attributes", dict())
|
||||
|
||||
|
|
|
@ -14,7 +14,8 @@ from cloudinary import utils
|
|||
from cloudinary.api_client.call_api import (
|
||||
call_api,
|
||||
call_metadata_api,
|
||||
call_json_api
|
||||
call_json_api,
|
||||
_call_v2_api
|
||||
)
|
||||
from cloudinary.exceptions import (
|
||||
BadRequest,
|
||||
|
@ -54,6 +55,19 @@ def usage(**options):
|
|||
return call_api("get", uri, {}, **options)
|
||||
|
||||
|
||||
def config(**options):
|
||||
"""
|
||||
Get account config details.
|
||||
|
||||
:param options: Additional options.
|
||||
:type options: dict, optional
|
||||
:return: Detailed config information.
|
||||
:rtype: Response
|
||||
"""
|
||||
params = only(options, "settings")
|
||||
return call_api("get", ["config"], params, **options)
|
||||
|
||||
|
||||
def resource_types(**options):
|
||||
return call_api("get", ["resources"], {}, **options)
|
||||
|
||||
|
@ -64,24 +78,22 @@ def resources(**options):
|
|||
uri = ["resources", resource_type]
|
||||
if upload_type:
|
||||
uri.append(upload_type)
|
||||
params = only(options, "next_cursor", "max_results", "prefix", "tags",
|
||||
"context", "moderations", "direction", "start_at", "metadata")
|
||||
params = __list_resources_params(**options)
|
||||
params.update(only(options, "prefix", "start_at"))
|
||||
return call_api("get", uri, params, **options)
|
||||
|
||||
|
||||
def resources_by_tag(tag, **options):
|
||||
resource_type = options.pop("resource_type", "image")
|
||||
uri = ["resources", resource_type, "tags", tag]
|
||||
params = only(options, "next_cursor", "max_results", "tags",
|
||||
"context", "moderations", "direction", "metadata")
|
||||
params = __list_resources_params(**options)
|
||||
return call_api("get", uri, params, **options)
|
||||
|
||||
|
||||
def resources_by_moderation(kind, status, **options):
|
||||
resource_type = options.pop("resource_type", "image")
|
||||
uri = ["resources", resource_type, "moderations", kind, status]
|
||||
params = only(options, "next_cursor", "max_results", "tags",
|
||||
"context", "moderations", "direction", "metadata")
|
||||
params = __list_resources_params(**options)
|
||||
return call_api("get", uri, params, **options)
|
||||
|
||||
|
||||
|
@ -89,7 +101,7 @@ def resources_by_ids(public_ids, **options):
|
|||
resource_type = options.pop("resource_type", "image")
|
||||
upload_type = options.pop("type", "upload")
|
||||
uri = ["resources", resource_type, upload_type]
|
||||
params = dict(only(options, "tags", "moderations", "context"), public_ids=public_ids)
|
||||
params = dict(__resources_params(**options), public_ids=public_ids)
|
||||
return call_api("get", uri, params, **options)
|
||||
|
||||
|
||||
|
@ -105,7 +117,7 @@ def resources_by_asset_folder(asset_folder, **options):
|
|||
:rtype: Response
|
||||
"""
|
||||
uri = ["resources", "by_asset_folder"]
|
||||
params = only(options, "max_results", "tags", "moderations", "context", "next_cursor")
|
||||
params = __list_resources_params(**options)
|
||||
params["asset_folder"] = asset_folder
|
||||
return call_api("get", uri, params, **options)
|
||||
|
||||
|
@ -125,7 +137,7 @@ def resources_by_asset_ids(asset_ids, **options):
|
|||
:rtype: Response
|
||||
"""
|
||||
uri = ["resources", 'by_asset_ids']
|
||||
params = dict(only(options, "tags", "moderations", "context"), asset_ids=asset_ids)
|
||||
params = dict(__resources_params(**options), asset_ids=asset_ids)
|
||||
return call_api("get", uri, params, **options)
|
||||
|
||||
|
||||
|
@ -147,15 +159,43 @@ def resources_by_context(key, value=None, **options):
|
|||
"""
|
||||
resource_type = options.pop("resource_type", "image")
|
||||
uri = ["resources", resource_type, "context"]
|
||||
params = only(options, "next_cursor", "max_results", "tags",
|
||||
"context", "moderations", "direction", "metadata")
|
||||
params = __list_resources_params(**options)
|
||||
params["key"] = key
|
||||
if value is not None:
|
||||
params["value"] = value
|
||||
return call_api("get", uri, params, **options)
|
||||
|
||||
|
||||
def visual_search(image_url=None, image_asset_id=None, text=None, **options):
|
||||
def __resources_params(**options):
|
||||
"""
|
||||
Prepares optional parameters for resources_* API calls.
|
||||
|
||||
:param options: Additional options
|
||||
:return: Optional parameters
|
||||
|
||||
:internal
|
||||
"""
|
||||
params = only(options, "tags", "context", "metadata", "moderations")
|
||||
params["fields"] = options.get("fields") and utils.encode_list(utils.build_array(options["fields"]))
|
||||
return params
|
||||
|
||||
|
||||
def __list_resources_params(**options):
|
||||
"""
|
||||
Prepares optional parameters for resources_* API calls.
|
||||
|
||||
:param options: Additional options
|
||||
:return: Optional parameters
|
||||
|
||||
:internal
|
||||
"""
|
||||
resources_params = __resources_params(**options)
|
||||
resources_params.update(only(options, "next_cursor", "max_results", "direction"))
|
||||
|
||||
return resources_params
|
||||
|
||||
|
||||
def visual_search(image_url=None, image_asset_id=None, text=None, image_file=None, **options):
|
||||
"""
|
||||
Find images based on their visual content.
|
||||
|
||||
|
@ -165,14 +205,17 @@ def visual_search(image_url=None, image_asset_id=None, text=None, **options):
|
|||
:type image_asset_id: str
|
||||
:param text: A textual description, e.g., "cat"
|
||||
:type text: str
|
||||
:param image_file: The image file.
|
||||
:type image_file: str|callable|Path|bytes
|
||||
:param options: Additional options
|
||||
:type options: dict, optional
|
||||
:return: Resources (assets) that were found
|
||||
:rtype: Response
|
||||
"""
|
||||
uri = ["resources", "visual_search"]
|
||||
params = {"image_url": image_url, "image_asset_id": image_asset_id, "text": text}
|
||||
return call_api("get", uri, params, **options)
|
||||
params = {"image_url": image_url, "image_asset_id": image_asset_id, "text": text,
|
||||
"image_file": utils.handle_file_parameter(image_file, "file")}
|
||||
return call_api("post", uri, params, **options)
|
||||
|
||||
|
||||
def resource(public_id, **options):
|
||||
|
@ -224,11 +267,11 @@ def update(public_id, **options):
|
|||
if "tags" in options:
|
||||
params["tags"] = ",".join(utils.build_array(options["tags"]))
|
||||
if "face_coordinates" in options:
|
||||
params["face_coordinates"] = utils.encode_double_array(
|
||||
options.get("face_coordinates"))
|
||||
params["face_coordinates"] = utils.encode_double_array(options.get("face_coordinates"))
|
||||
if "custom_coordinates" in options:
|
||||
params["custom_coordinates"] = utils.encode_double_array(
|
||||
options.get("custom_coordinates"))
|
||||
params["custom_coordinates"] = utils.encode_double_array(options.get("custom_coordinates"))
|
||||
if "regions" in options:
|
||||
params["regions"] = utils.json_encode(options.get("regions"))
|
||||
if "context" in options:
|
||||
params["context"] = utils.encode_context(options.get("context"))
|
||||
if "metadata" in options:
|
||||
|
@ -656,9 +699,8 @@ def add_metadata_field(field, **options):
|
|||
|
||||
:rtype: Response
|
||||
"""
|
||||
params = only(field, "type", "external_id", "label", "mandatory",
|
||||
"default_value", "validation", "datasource")
|
||||
return call_metadata_api("post", [], params, **options)
|
||||
|
||||
return call_metadata_api("post", [], __metadata_field_params(field), **options)
|
||||
|
||||
|
||||
def update_metadata_field(field_external_id, field, **options):
|
||||
|
@ -677,8 +719,13 @@ def update_metadata_field(field_external_id, field, **options):
|
|||
:rtype: Response
|
||||
"""
|
||||
uri = [field_external_id]
|
||||
params = only(field, "label", "mandatory", "default_value", "validation")
|
||||
return call_metadata_api("put", uri, params, **options)
|
||||
|
||||
return call_metadata_api("put", uri, __metadata_field_params(field), **options)
|
||||
|
||||
|
||||
def __metadata_field_params(field):
|
||||
return only(field, "type", "external_id", "label", "mandatory", "restrictions",
|
||||
"default_value", "validation", "datasource")
|
||||
|
||||
|
||||
def delete_metadata_field(field_external_id, **options):
|
||||
|
@ -798,3 +845,18 @@ def reorder_metadata_fields(order_by, direction=None, **options):
|
|||
uri = ['order']
|
||||
params = {'order_by': order_by, 'direction': direction}
|
||||
return call_metadata_api('put', uri, params, **options)
|
||||
|
||||
|
||||
def analyze(input_type, analysis_type, uri=None, **options):
|
||||
"""Analyzes an asset with the requested analysis type.
|
||||
|
||||
:param input_type: The type of input for the asset to analyze ('uri').
|
||||
:param analysis_type: The type of analysis to run ('google_tagging', 'captioning', 'fashion').
|
||||
:param uri: The URI of the asset to analyze.
|
||||
:param options: Additional options.
|
||||
|
||||
:rtype: Response
|
||||
"""
|
||||
api_uri = ['analysis', 'analyze', input_type]
|
||||
params = {'analysis_type': analysis_type, 'uri': uri, 'parameters': options.get("parameters")}
|
||||
return _call_v2_api('post', api_uri, params, **options)
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import cloudinary
|
||||
from cloudinary.api_client.execute_request import execute_request
|
||||
from cloudinary.provisioning.account_config import account_config
|
||||
from cloudinary.utils import get_http_connector
|
||||
|
||||
from cloudinary.utils import get_http_connector, normalize_params
|
||||
|
||||
PROVISIONING_SUB_PATH = "provisioning"
|
||||
ACCOUNT_SUB_PATH = "accounts"
|
||||
|
@ -28,7 +27,7 @@ def _call_account_api(method, uri, params=None, headers=None, **options):
|
|||
|
||||
return execute_request(http_connector=_http,
|
||||
method=method,
|
||||
params=params,
|
||||
params=normalize_params(params),
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
api_url=provisioning_api_url,
|
||||
|
|
|
@ -2,8 +2,7 @@ import json
|
|||
|
||||
import cloudinary
|
||||
from cloudinary.api_client.execute_request import execute_request
|
||||
from cloudinary.utils import get_http_connector
|
||||
|
||||
from cloudinary.utils import get_http_connector, normalize_params
|
||||
|
||||
logger = cloudinary.logger
|
||||
_http = get_http_connector(cloudinary.config(), cloudinary.CERT_KWARGS)
|
||||
|
@ -27,6 +26,10 @@ def call_json_api(method, uri, json_body, **options):
|
|||
return _call_api(method, uri, body=data, headers={'Content-Type': 'application/json'}, **options)
|
||||
|
||||
|
||||
def _call_v2_api(method, uri, json_body, **options):
|
||||
return call_json_api(method, uri, json_body=json_body, api_version='v2', **options)
|
||||
|
||||
|
||||
def call_api(method, uri, params, **options):
|
||||
return _call_api(method, uri, params=params, **options)
|
||||
|
||||
|
@ -43,10 +46,11 @@ def _call_api(method, uri, params=None, body=None, headers=None, extra_headers=N
|
|||
oauth_token = options.pop("oauth_token", cloudinary.config().oauth_token)
|
||||
|
||||
_validate_authorization(api_key, api_secret, oauth_token)
|
||||
|
||||
api_url = "/".join([prefix, cloudinary.API_VERSION, cloud_name] + uri)
|
||||
auth = {"key": api_key, "secret": api_secret, "oauth_token": oauth_token}
|
||||
|
||||
api_version = options.pop("api_version", cloudinary.API_VERSION)
|
||||
api_url = "/".join([prefix, api_version, cloud_name] + uri)
|
||||
|
||||
if body is not None:
|
||||
options["body"] = body
|
||||
|
||||
|
@ -55,7 +59,7 @@ def _call_api(method, uri, params=None, body=None, headers=None, extra_headers=N
|
|||
|
||||
return execute_request(http_connector=_http,
|
||||
method=method,
|
||||
params=params,
|
||||
params=normalize_params(params),
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
api_url=api_url,
|
||||
|
|
|
@ -63,9 +63,8 @@ def execute_request(http_connector, method, params, headers, auth, api_url, **op
|
|||
processed_params = process_params(params)
|
||||
|
||||
api_url = smart_escape(unquote(api_url))
|
||||
|
||||
try:
|
||||
response = http_connector.request(method.upper(), api_url, processed_params, req_headers, **kw)
|
||||
response = http_connector.request(method=method.upper(), url=api_url, fields=processed_params, headers=req_headers, **kw)
|
||||
body = response.data
|
||||
except HTTPError as e:
|
||||
raise GeneralError("Unexpected error %s" % str(e))
|
||||
|
|
|
@ -24,7 +24,7 @@ class HttpClient:
|
|||
|
||||
def get_json(self, url):
|
||||
try:
|
||||
response = self._http_client.request("GET", url, timeout=self.timeout)
|
||||
response = self._http_client.request(method="GET", url=url, timeout=self.timeout)
|
||||
body = response.data
|
||||
except HTTPError as e:
|
||||
raise GeneralError("Unexpected error %s" % str(e))
|
||||
|
|
|
@ -2,4 +2,5 @@ from .account_config import AccountConfig, account_config, reset_config
|
|||
from .account import (sub_accounts, create_sub_account, delete_sub_account, sub_account, update_sub_account,
|
||||
user_groups, create_user_group, update_user_group, delete_user_group, user_group,
|
||||
add_user_to_group, remove_user_from_group, user_group_users, user_in_user_groups,
|
||||
users, create_user, delete_user, user, update_user, Role)
|
||||
users, create_user, delete_user, user, update_user, access_keys, generate_access_key,
|
||||
update_access_key, delete_access_key, Role)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from cloudinary.api_client.call_account_api import _call_account_api
|
||||
from cloudinary.utils import encode_list
|
||||
|
||||
|
||||
SUB_ACCOUNTS_SUB_PATH = "sub_accounts"
|
||||
USERS_SUB_PATH = "users"
|
||||
USER_GROUPS_SUB_PATH = "user_groups"
|
||||
ACCESS_KEYS = "access_keys"
|
||||
|
||||
|
||||
class Role(object):
|
||||
|
@ -123,7 +123,8 @@ def update_sub_account(sub_account_id, name=None, cloud_name=None, custom_attrib
|
|||
return _call_account_api("put", uri, params=params, **options)
|
||||
|
||||
|
||||
def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, **options):
|
||||
def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, last_login=None, from_date=None, to_date=None,
|
||||
**options):
|
||||
"""
|
||||
List all users
|
||||
:param user_ids: The ids of the users to fetch
|
||||
|
@ -136,6 +137,13 @@ def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, **optio
|
|||
:type pending: bool, optional
|
||||
:param prefix: User prefix
|
||||
:type prefix: str, optional
|
||||
:param last_login: Return only users that last logged in in the specified range of dates (true),
|
||||
users that didn't last logged in in that range (false), or all users (None).
|
||||
:type last_login: bool, optional
|
||||
:param from_date: Last login start date.
|
||||
:type from_date: datetime, optional
|
||||
:param to_date: Last login end date.
|
||||
:type to_date: datetime, optional
|
||||
:param options: Generic advanced options dict, see online documentation.
|
||||
:type options: dict, optional
|
||||
:return: List of users associated with the account
|
||||
|
@ -146,7 +154,10 @@ def users(user_ids=None, sub_account_id=None, pending=None, prefix=None, **optio
|
|||
params = {"ids": user_ids,
|
||||
"sub_account_id": sub_account_id,
|
||||
"pending": pending,
|
||||
"prefix": prefix}
|
||||
"prefix": prefix,
|
||||
"last_login": last_login,
|
||||
"from": from_date,
|
||||
"to": to_date}
|
||||
return _call_account_api("get", uri, params=params, **options)
|
||||
|
||||
|
||||
|
@ -351,7 +362,7 @@ def user_in_user_groups(user_id, **options):
|
|||
"""
|
||||
Get all user groups a user belongs to
|
||||
:param user_id: The id of user
|
||||
:param user_id: str
|
||||
:type user_id: str
|
||||
:param options: Generic advanced options dict, see online documentation
|
||||
:type options: dict, optional
|
||||
:return: List of groups user is in
|
||||
|
@ -359,3 +370,112 @@ def user_in_user_groups(user_id, **options):
|
|||
"""
|
||||
uri = [USER_GROUPS_SUB_PATH, user_id]
|
||||
return _call_account_api("get", uri, {}, **options)
|
||||
|
||||
|
||||
def access_keys(sub_account_id, page_size=None, page=None, sort_by=None, sort_order=None, **options):
|
||||
"""
|
||||
Get sub account access keys.
|
||||
|
||||
:param sub_account_id: The id of the sub account.
|
||||
:type sub_account_id: str
|
||||
:param page_size: How many entries to display on each page.
|
||||
:type page_size: int
|
||||
:param page: Which page to return (maximum pages: 100). **Default**: All pages are returned.
|
||||
:type page: int
|
||||
:param sort_by: Which response parameter to sort by.
|
||||
**Possible values**: `api_key`, `created_at`, `name`, `enabled`.
|
||||
:type sort_by: str
|
||||
:param sort_order: Control the order of returned keys. **Possible values**: `desc` (default), `asc`.
|
||||
:type sort_order: str
|
||||
:param options: Generic advanced options dict, see online documentation.
|
||||
:type options: dict, optional
|
||||
:return: List of access keys
|
||||
:rtype: dict
|
||||
"""
|
||||
uri = [SUB_ACCOUNTS_SUB_PATH, sub_account_id, ACCESS_KEYS]
|
||||
params = {
|
||||
"page_size": page_size,
|
||||
"page": page,
|
||||
"sort_by": sort_by,
|
||||
"sort_order": sort_order,
|
||||
}
|
||||
return _call_account_api("get", uri, params, **options)
|
||||
|
||||
|
||||
def generate_access_key(sub_account_id, name=None, enabled=None, **options):
|
||||
"""
|
||||
Generate a new access key.
|
||||
|
||||
:param sub_account_id: The id of the sub account.
|
||||
:type sub_account_id: str
|
||||
:param name: The name of the new access key.
|
||||
:type name: str
|
||||
:param enabled: Whether the new access key is enabled or disabled.
|
||||
:type enabled: bool
|
||||
:param options: Generic advanced options dict, see online documentation.
|
||||
:type options: dict, optional
|
||||
:return: Access key details
|
||||
:rtype: dict
|
||||
"""
|
||||
uri = [SUB_ACCOUNTS_SUB_PATH, sub_account_id, ACCESS_KEYS]
|
||||
params = {
|
||||
"name": name,
|
||||
"enabled": enabled,
|
||||
}
|
||||
return _call_account_api("post", uri, params, **options)
|
||||
|
||||
|
||||
def update_access_key(sub_account_id, api_key, name=None, enabled=None, dedicated_for=None, **options):
|
||||
"""
|
||||
Update the name and/or status of an existing access key.
|
||||
|
||||
:param sub_account_id: The id of the sub account.
|
||||
:type sub_account_id: str
|
||||
:param api_key: The API key of the access key.
|
||||
:type api_key: str|int
|
||||
:param name: The updated name of the access key.
|
||||
:type name: str
|
||||
:param enabled: Enable or disable the access key.
|
||||
:type enabled: bool
|
||||
:param dedicated_for: Designates the access key for a specific purpose while allowing it to be used for
|
||||
other purposes, as well. This action replaces any previously assigned key.
|
||||
**Possible values**: `webhooks`
|
||||
:type dedicated_for: str
|
||||
:param options: Generic advanced options dict, see online documentation.
|
||||
:type options: dict, optional
|
||||
:return: Access key details
|
||||
:rtype: dict
|
||||
"""
|
||||
uri = [SUB_ACCOUNTS_SUB_PATH, sub_account_id, ACCESS_KEYS, str(api_key)]
|
||||
params = {
|
||||
"name": name,
|
||||
"enabled": enabled,
|
||||
"dedicated_for": dedicated_for,
|
||||
}
|
||||
return _call_account_api("put", uri, params, **options)
|
||||
|
||||
|
||||
def delete_access_key(sub_account_id, api_key=None, name=None, **options):
|
||||
"""
|
||||
Delete an existing access key by api_key or by name.
|
||||
|
||||
:param sub_account_id: The id of the sub account.
|
||||
:type sub_account_id: str
|
||||
:param api_key: The API key of the access key.
|
||||
:type api_key: str|int
|
||||
:param name: The name of the access key.
|
||||
:type name: str
|
||||
:param options: Generic advanced options dict, see online documentation.
|
||||
:type options: dict, optional
|
||||
:return: Operation status.
|
||||
:rtype: dict
|
||||
"""
|
||||
uri = [SUB_ACCOUNTS_SUB_PATH, sub_account_id, ACCESS_KEYS]
|
||||
|
||||
if api_key is not None:
|
||||
uri.append(str(api_key))
|
||||
|
||||
params = {
|
||||
"name": name
|
||||
}
|
||||
return _call_account_api("delete", uri, params, **options)
|
||||
|
|
|
@ -3,8 +3,8 @@ import json
|
|||
|
||||
import cloudinary
|
||||
from cloudinary.api_client.call_api import call_json_api
|
||||
from cloudinary.utils import unique, unsigned_download_url_prefix, build_distribution_domain, base64url_encode, \
|
||||
json_encode, compute_hex_hash, SIGNATURE_SHA256
|
||||
from cloudinary.utils import (unique, build_distribution_domain, base64url_encode, json_encode, compute_hex_hash,
|
||||
SIGNATURE_SHA256, build_array)
|
||||
|
||||
|
||||
class Search(object):
|
||||
|
@ -16,6 +16,7 @@ class Search(object):
|
|||
'sort_by': lambda x: next(iter(x)),
|
||||
'aggregate': None,
|
||||
'with_field': None,
|
||||
'fields': None,
|
||||
}
|
||||
|
||||
_ttl = 300 # Used for search URLs
|
||||
|
@ -57,6 +58,11 @@ class Search(object):
|
|||
self._add("with_field", value)
|
||||
return self
|
||||
|
||||
def fields(self, value):
|
||||
"""Request which fields to return in the result set."""
|
||||
self._add("fields", value)
|
||||
return self
|
||||
|
||||
def ttl(self, ttl):
|
||||
"""
|
||||
Sets the time to live of the search URL.
|
||||
|
@ -133,5 +139,5 @@ class Search(object):
|
|||
def _add(self, name, value):
|
||||
if name not in self.query:
|
||||
self.query[name] = []
|
||||
self.query[name].append(value)
|
||||
self.query[name].extend(build_array(value))
|
||||
return self
|
||||
|
|
|
@ -23,11 +23,6 @@ try: # Python 2.7+
|
|||
except ImportError:
|
||||
from urllib3.packages.ordered_dict import OrderedDict
|
||||
|
||||
try: # Python 3.4+
|
||||
from pathlib import Path as PathLibPathType
|
||||
except ImportError:
|
||||
PathLibPathType = None
|
||||
|
||||
if is_appengine_sandbox():
|
||||
# AppEngineManager uses AppEngine's URLFetch API behind the scenes
|
||||
_http = AppEngineManager()
|
||||
|
@ -503,32 +498,7 @@ def call_api(action, params, http_headers=None, return_error=False, unsigned=Fal
|
|||
|
||||
if file:
|
||||
filename = options.get("filename") # Custom filename provided by user (relevant only for streams and files)
|
||||
|
||||
if PathLibPathType and isinstance(file, PathLibPathType):
|
||||
name = filename or file.name
|
||||
data = file.read_bytes()
|
||||
elif isinstance(file, string_types):
|
||||
if utils.is_remote_url(file):
|
||||
# URL
|
||||
name = None
|
||||
data = file
|
||||
else:
|
||||
# file path
|
||||
name = filename or file
|
||||
with open(file, "rb") as opened:
|
||||
data = opened.read()
|
||||
elif hasattr(file, 'read') and callable(file.read):
|
||||
# stream
|
||||
data = file.read()
|
||||
name = filename or (file.name if hasattr(file, 'name') and isinstance(file.name, str) else "stream")
|
||||
elif isinstance(file, tuple):
|
||||
name, data = file
|
||||
else:
|
||||
# Not a string, not a stream
|
||||
name = filename or "file"
|
||||
data = file
|
||||
|
||||
param_list.append(("file", (name, data) if name else data))
|
||||
param_list.append(("file", utils.handle_file_parameter(file, filename)))
|
||||
|
||||
kw = {}
|
||||
if timeout is not None:
|
||||
|
@ -536,7 +506,7 @@ def call_api(action, params, http_headers=None, return_error=False, unsigned=Fal
|
|||
|
||||
code = 200
|
||||
try:
|
||||
response = _http.request("POST", api_url, param_list, headers, **kw)
|
||||
response = _http.request(method="POST", url=api_url, fields=param_list, headers=headers, **kw)
|
||||
except HTTPError as e:
|
||||
raise Error("Unexpected error - {0!r}".format(e))
|
||||
except socket.error as e:
|
||||
|
|
|
@ -25,6 +25,11 @@ from cloudinary import auth_token
|
|||
from cloudinary.api_client.tcp_keep_alive_manager import TCPKeepAlivePoolManager, TCPKeepAliveProxyManager
|
||||
from cloudinary.compat import PY3, to_bytes, to_bytearray, to_string, string_types, urlparse
|
||||
|
||||
try: # Python 3.4+
|
||||
from pathlib import Path as PathLibPathType
|
||||
except ImportError:
|
||||
PathLibPathType = None
|
||||
|
||||
VAR_NAME_RE = r'(\$\([a-zA-Z]\w+\))'
|
||||
|
||||
urlencode = six.moves.urllib.parse.urlencode
|
||||
|
@ -127,6 +132,7 @@ __SERIALIZED_UPLOAD_PARAMS = [
|
|||
"allowed_formats",
|
||||
"face_coordinates",
|
||||
"custom_coordinates",
|
||||
"regions",
|
||||
"context",
|
||||
"auto_tagging",
|
||||
"responsive_breakpoints",
|
||||
|
@ -181,12 +187,11 @@ def compute_hex_hash(s, algorithm=SIGNATURE_SHA1):
|
|||
|
||||
|
||||
def build_array(arg):
|
||||
if isinstance(arg, list):
|
||||
if isinstance(arg, (list, tuple)):
|
||||
return arg
|
||||
elif arg is None:
|
||||
return []
|
||||
else:
|
||||
return [arg]
|
||||
return [arg]
|
||||
|
||||
|
||||
def build_list_of_dicts(val):
|
||||
|
@ -235,8 +240,7 @@ def encode_double_array(array):
|
|||
array = build_array(array)
|
||||
if len(array) > 0 and isinstance(array[0], list):
|
||||
return "|".join([",".join([str(i) for i in build_array(inner)]) for inner in array])
|
||||
else:
|
||||
return encode_list([str(i) for i in array])
|
||||
return encode_list([str(i) for i in array])
|
||||
|
||||
|
||||
def encode_dict(arg):
|
||||
|
@ -246,8 +250,7 @@ def encode_dict(arg):
|
|||
else:
|
||||
items = arg.iteritems()
|
||||
return "|".join((k + "=" + v) for k, v in items)
|
||||
else:
|
||||
return arg
|
||||
return arg
|
||||
|
||||
|
||||
def normalize_context_value(value):
|
||||
|
@ -288,9 +291,14 @@ def json_encode(value, sort_keys=False):
|
|||
Converts value to a json encoded string
|
||||
|
||||
:param value: value to be encoded
|
||||
:param sort_keys: whether to sort keys
|
||||
|
||||
:return: JSON encoded string
|
||||
"""
|
||||
|
||||
if isinstance(value, str) or value is None:
|
||||
return value
|
||||
|
||||
return json.dumps(value, default=__json_serializer, separators=(',', ':'), sort_keys=sort_keys)
|
||||
|
||||
|
||||
|
@ -309,11 +317,13 @@ def patch_fetch_format(options):
|
|||
"""
|
||||
When upload type is fetch, remove the format options.
|
||||
In addition, set the fetch_format options to the format value unless it was already set.
|
||||
Mutates the options parameter!
|
||||
Mutates the "options" parameter!
|
||||
|
||||
:param options: URL and transformation options
|
||||
"""
|
||||
if options.get("type", "upload") != "fetch":
|
||||
use_fetch_format = options.pop("use_fetch_format", cloudinary.config().use_fetch_format)
|
||||
|
||||
if options.get("type", "upload") != "fetch" and not use_fetch_format:
|
||||
return
|
||||
|
||||
resource_format = options.pop("format", None)
|
||||
|
@ -351,8 +361,7 @@ def generate_transformation_string(**options):
|
|||
def recurse(bs):
|
||||
if isinstance(bs, dict):
|
||||
return generate_transformation_string(**bs)[0]
|
||||
else:
|
||||
return generate_transformation_string(transformation=bs)[0]
|
||||
return generate_transformation_string(transformation=bs)[0]
|
||||
|
||||
base_transformations = list(map(recurse, base_transformations))
|
||||
named_transformation = None
|
||||
|
@ -375,7 +384,7 @@ def generate_transformation_string(**options):
|
|||
flags = ".".join(build_array(options.pop("flags", None)))
|
||||
dpr = options.pop("dpr", cloudinary.config().dpr)
|
||||
duration = norm_range_value(options.pop("duration", None))
|
||||
|
||||
|
||||
so_raw = options.pop("start_offset", None)
|
||||
start_offset = norm_auto_range_value(so_raw)
|
||||
if start_offset == None:
|
||||
|
@ -513,8 +522,7 @@ def split_range(range):
|
|||
return [range[0], range[-1]]
|
||||
elif isinstance(range, string_types) and re.match(RANGE_RE, range):
|
||||
return range.split("..", 1)
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def norm_range_value(value):
|
||||
|
@ -570,6 +578,9 @@ def process_params(params):
|
|||
processed_params = {}
|
||||
for key, value in params.items():
|
||||
if isinstance(value, list) or isinstance(value, tuple):
|
||||
if len(value) == 2 and value[0] == "file": # keep file parameter as is.
|
||||
processed_params[key] = value
|
||||
continue
|
||||
value_list = {"{}[{}]".format(key, i): i_value for i, i_value in enumerate(value)}
|
||||
processed_params.update(value_list)
|
||||
elif value is not None:
|
||||
|
@ -578,9 +589,28 @@ def process_params(params):
|
|||
|
||||
|
||||
def cleanup_params(params):
|
||||
"""
|
||||
Cleans and normalizes parameters when calculating signature in Upload API.
|
||||
|
||||
:param params:
|
||||
:return:
|
||||
"""
|
||||
return dict([(k, __safe_value(v)) for (k, v) in params.items() if v is not None and not v == ""])
|
||||
|
||||
|
||||
def normalize_params(params):
|
||||
"""
|
||||
Normalizes Admin API parameters.
|
||||
|
||||
:param params:
|
||||
:return:
|
||||
"""
|
||||
if not params or not isinstance(params, dict):
|
||||
return params
|
||||
|
||||
return dict([(k, __bool_string(v)) for (k, v) in params.items() if v is not None and not v == ""])
|
||||
|
||||
|
||||
def sign_request(params, options):
|
||||
api_key = options.get("api_key", cloudinary.config().api_key)
|
||||
if not api_key:
|
||||
|
@ -1086,6 +1116,7 @@ def build_upload_params(**options):
|
|||
"allowed_formats": options.get("allowed_formats") and encode_list(build_array(options["allowed_formats"])),
|
||||
"face_coordinates": encode_double_array(options.get("face_coordinates")),
|
||||
"custom_coordinates": encode_double_array(options.get("custom_coordinates")),
|
||||
"regions": json_encode(options.get("regions")),
|
||||
"context": encode_context(options.get("context")),
|
||||
"auto_tagging": options.get("auto_tagging") and str(options.get("auto_tagging")),
|
||||
"responsive_breakpoints": generate_responsive_breakpoints_string(options.get("responsive_breakpoints")),
|
||||
|
@ -1101,6 +1132,37 @@ def build_upload_params(**options):
|
|||
return params
|
||||
|
||||
|
||||
def handle_file_parameter(file, filename):
|
||||
if not file:
|
||||
return None
|
||||
|
||||
if PathLibPathType and isinstance(file, PathLibPathType):
|
||||
name = filename or file.name
|
||||
data = file.read_bytes()
|
||||
elif isinstance(file, string_types):
|
||||
if is_remote_url(file):
|
||||
# URL
|
||||
name = None
|
||||
data = file
|
||||
else:
|
||||
# file path
|
||||
name = filename or file
|
||||
with open(file, "rb") as opened:
|
||||
data = opened.read()
|
||||
elif hasattr(file, 'read') and callable(file.read):
|
||||
# stream
|
||||
data = file.read()
|
||||
name = filename or (file.name if hasattr(file, 'name') and isinstance(file.name, str) else "stream")
|
||||
elif isinstance(file, tuple):
|
||||
name, data = file
|
||||
else:
|
||||
# Not a string, not a stream
|
||||
name = filename or "file"
|
||||
data = file
|
||||
|
||||
return (name, data) if name else data
|
||||
|
||||
|
||||
def build_multi_and_sprite_params(**options):
|
||||
"""
|
||||
Build params for multi, download_multi, generate_sprite, and download_generated_sprite methods
|
||||
|
@ -1166,8 +1228,21 @@ def __process_text_options(layer, layer_parameter):
|
|||
|
||||
|
||||
def process_layer(layer, layer_parameter):
|
||||
if isinstance(layer, string_types) and layer.startswith("fetch:"):
|
||||
layer = {"url": layer[len('fetch:'):]}
|
||||
if isinstance(layer, string_types):
|
||||
resource_type = None
|
||||
if layer.startswith("fetch:"):
|
||||
url = layer[len('fetch:'):]
|
||||
elif layer.find(":fetch:", 0, 12) != -1:
|
||||
resource_type, _, url = layer.split(":", 2)
|
||||
else:
|
||||
# nothing to process, a raw string, keep as is.
|
||||
return layer
|
||||
|
||||
# handle remote fetch URL
|
||||
layer = {"url": url, "type": "fetch"}
|
||||
if resource_type:
|
||||
layer["resource_type"] = resource_type
|
||||
|
||||
if not isinstance(layer, dict):
|
||||
return layer
|
||||
|
||||
|
@ -1176,19 +1251,19 @@ def process_layer(layer, layer_parameter):
|
|||
type = layer.get("type")
|
||||
public_id = layer.get("public_id")
|
||||
format = layer.get("format")
|
||||
fetch = layer.get("url")
|
||||
fetch_url = layer.get("url")
|
||||
components = list()
|
||||
|
||||
if text is not None and resource_type is None:
|
||||
resource_type = "text"
|
||||
|
||||
if fetch and resource_type is None:
|
||||
resource_type = "fetch"
|
||||
if fetch_url and type is None:
|
||||
type = "fetch"
|
||||
|
||||
if public_id is not None and format is not None:
|
||||
public_id = public_id + "." + format
|
||||
|
||||
if public_id is None and resource_type != "text" and resource_type != "fetch":
|
||||
if public_id is None and resource_type != "text" and type != "fetch":
|
||||
raise ValueError("Must supply public_id for for non-text " + layer_parameter)
|
||||
|
||||
if resource_type is not None and resource_type != "image":
|
||||
|
@ -1212,8 +1287,6 @@ def process_layer(layer, layer_parameter):
|
|||
|
||||
if text is not None:
|
||||
var_pattern = VAR_NAME_RE
|
||||
match = re.findall(var_pattern, text)
|
||||
|
||||
parts = filter(lambda p: p is not None, re.split(var_pattern, text))
|
||||
encoded_text = []
|
||||
for part in parts:
|
||||
|
@ -1223,11 +1296,9 @@ def process_layer(layer, layer_parameter):
|
|||
encoded_text.append(smart_escape(smart_escape(part, r"([,/])")))
|
||||
|
||||
text = ''.join(encoded_text)
|
||||
# text = text.replace("%2C", "%252C")
|
||||
# text = text.replace("/", "%252F")
|
||||
components.append(text)
|
||||
elif resource_type == "fetch":
|
||||
b64 = base64_encode_url(fetch)
|
||||
elif type == "fetch":
|
||||
b64 = base64url_encode(fetch_url)
|
||||
components.append(b64)
|
||||
else:
|
||||
public_id = public_id.replace("/", ':')
|
||||
|
@ -1359,8 +1430,7 @@ def normalize_expression(expression):
|
|||
result = re.sub(replaceRE, translate_if, result)
|
||||
result = re.sub('[ _]+', '_', result)
|
||||
return result
|
||||
else:
|
||||
return expression
|
||||
return expression
|
||||
|
||||
|
||||
def __join_pair(key, value):
|
||||
|
@ -1368,8 +1438,7 @@ def __join_pair(key, value):
|
|||
return None
|
||||
elif value is True:
|
||||
return key
|
||||
else:
|
||||
return u"{0}=\"{1}\"".format(key, value)
|
||||
return u"{0}=\"{1}\"".format(key, value)
|
||||
|
||||
|
||||
def html_attrs(attrs, only=None):
|
||||
|
@ -1379,10 +1448,15 @@ def html_attrs(attrs, only=None):
|
|||
def __safe_value(v):
|
||||
if isinstance(v, bool):
|
||||
return "1" if v else "0"
|
||||
else:
|
||||
return v
|
||||
return v
|
||||
|
||||
|
||||
def __bool_string(v):
|
||||
if isinstance(v, bool):
|
||||
return "true" if v else "false"
|
||||
|
||||
return v
|
||||
|
||||
def __crc(source):
|
||||
return str((zlib.crc32(to_bytearray(source)) & 0xffffffff) % 5 + 1)
|
||||
|
||||
|
|
6
lib/dateutil-stubs/METADATA.toml
Normal file
6
lib/dateutil-stubs/METADATA.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
version = "2.9.*"
|
||||
upstream_repository = "https://github.com/dateutil/dateutil"
|
||||
partial_stub = true
|
||||
|
||||
[tool.stubtest]
|
||||
ignore_missing_stub = true
|
9
lib/dateutil-stubs/_common.pyi
Normal file
9
lib/dateutil-stubs/_common.pyi
Normal file
|
@ -0,0 +1,9 @@
|
|||
from typing_extensions import Self
|
||||
|
||||
class weekday:
|
||||
def __init__(self, weekday: int, n: int | None = None) -> None: ...
|
||||
def __call__(self, n: int) -> Self: ...
|
||||
def __eq__(self, other: object) -> bool: ...
|
||||
def __hash__(self) -> int: ...
|
||||
weekday: int
|
||||
n: int
|
8
lib/dateutil-stubs/easter.pyi
Normal file
8
lib/dateutil-stubs/easter.pyi
Normal file
|
@ -0,0 +1,8 @@
|
|||
from datetime import date
|
||||
from typing import Literal
|
||||
|
||||
EASTER_JULIAN: Literal[1]
|
||||
EASTER_ORTHODOX: Literal[2]
|
||||
EASTER_WESTERN: Literal[3]
|
||||
|
||||
def easter(year: int, method: Literal[1, 2, 3] = 3) -> date: ...
|
67
lib/dateutil-stubs/parser/__init__.pyi
Normal file
67
lib/dateutil-stubs/parser/__init__.pyi
Normal file
|
@ -0,0 +1,67 @@
|
|||
from collections.abc import Callable, Mapping
|
||||
from datetime import datetime, tzinfo
|
||||
from typing import IO, Any
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from .isoparser import isoparse as isoparse, isoparser as isoparser
|
||||
|
||||
_FileOrStr: TypeAlias = bytes | str | IO[str] | IO[Any]
|
||||
_TzData: TypeAlias = tzinfo | int | str | None
|
||||
_TzInfo: TypeAlias = Mapping[str, _TzData] | Callable[[str, int], _TzData]
|
||||
|
||||
class parserinfo:
|
||||
JUMP: list[str]
|
||||
WEEKDAYS: list[tuple[str, ...]]
|
||||
MONTHS: list[tuple[str, ...]]
|
||||
HMS: list[tuple[str, str, str]]
|
||||
AMPM: list[tuple[str, str]]
|
||||
UTCZONE: list[str]
|
||||
PERTAIN: list[str]
|
||||
TZOFFSET: dict[str, int]
|
||||
def __init__(self, dayfirst: bool = False, yearfirst: bool = False) -> None: ...
|
||||
def jump(self, name: str) -> bool: ...
|
||||
def weekday(self, name: str) -> int | None: ...
|
||||
def month(self, name: str) -> int | None: ...
|
||||
def hms(self, name: str) -> int | None: ...
|
||||
def ampm(self, name: str) -> int | None: ...
|
||||
def pertain(self, name: str) -> bool: ...
|
||||
def utczone(self, name: str) -> bool: ...
|
||||
def tzoffset(self, name: str) -> int | None: ...
|
||||
def convertyear(self, year: int) -> int: ...
|
||||
def validate(self, res: datetime) -> bool: ...
|
||||
|
||||
class parser:
|
||||
def __init__(self, info: parserinfo | None = None) -> None: ...
|
||||
def parse(
|
||||
self,
|
||||
timestr: _FileOrStr,
|
||||
default: datetime | None = None,
|
||||
ignoretz: bool = False,
|
||||
tzinfos: _TzInfo | None = None,
|
||||
*,
|
||||
dayfirst: bool | None = ...,
|
||||
yearfirst: bool | None = ...,
|
||||
fuzzy: bool = ...,
|
||||
fuzzy_with_tokens: bool = ...,
|
||||
) -> datetime: ...
|
||||
|
||||
DEFAULTPARSER: parser
|
||||
|
||||
def parse(
|
||||
timestr: _FileOrStr,
|
||||
parserinfo: parserinfo | None = None,
|
||||
*,
|
||||
dayfirst: bool | None = ...,
|
||||
yearfirst: bool | None = ...,
|
||||
ignoretz: bool = ...,
|
||||
fuzzy: bool = ...,
|
||||
fuzzy_with_tokens: bool = ...,
|
||||
default: datetime | None = ...,
|
||||
tzinfos: _TzInfo | None = ...,
|
||||
) -> datetime: ...
|
||||
|
||||
class _tzparser: ...
|
||||
|
||||
DEFAULTTZPARSER: _tzparser
|
||||
|
||||
class ParserError(ValueError): ...
|
15
lib/dateutil-stubs/parser/isoparser.pyi
Normal file
15
lib/dateutil-stubs/parser/isoparser.pyi
Normal file
|
@ -0,0 +1,15 @@
|
|||
from _typeshed import SupportsRead
|
||||
from datetime import date, datetime, time, tzinfo
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
_Readable: TypeAlias = SupportsRead[str | bytes]
|
||||
_TakesAscii: TypeAlias = str | bytes | _Readable
|
||||
|
||||
class isoparser:
|
||||
def __init__(self, sep: str | bytes | None = None): ...
|
||||
def isoparse(self, dt_str: _TakesAscii) -> datetime: ...
|
||||
def parse_isodate(self, datestr: _TakesAscii) -> date: ...
|
||||
def parse_isotime(self, timestr: _TakesAscii) -> time: ...
|
||||
def parse_tzstr(self, tzstr: _TakesAscii, zero_as_utc: bool = True) -> tzinfo: ...
|
||||
|
||||
def isoparse(dt_str: _TakesAscii) -> datetime: ...
|
1
lib/dateutil-stubs/py.typed
Normal file
1
lib/dateutil-stubs/py.typed
Normal file
|
@ -0,0 +1 @@
|
|||
partial
|
97
lib/dateutil-stubs/relativedelta.pyi
Normal file
97
lib/dateutil-stubs/relativedelta.pyi
Normal file
|
@ -0,0 +1,97 @@
|
|||
from datetime import date, timedelta
|
||||
from typing import SupportsFloat, TypeVar, overload
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
# See #9817 for why we reexport this here
|
||||
from ._common import weekday as weekday
|
||||
|
||||
_DateT = TypeVar("_DateT", bound=date)
|
||||
# Work around attribute and type having the same name.
|
||||
_Weekday: TypeAlias = weekday
|
||||
|
||||
MO: weekday
|
||||
TU: weekday
|
||||
WE: weekday
|
||||
TH: weekday
|
||||
FR: weekday
|
||||
SA: weekday
|
||||
SU: weekday
|
||||
|
||||
class relativedelta:
|
||||
years: int
|
||||
months: int
|
||||
days: int
|
||||
leapdays: int
|
||||
hours: int
|
||||
minutes: int
|
||||
seconds: int
|
||||
microseconds: int
|
||||
year: int | None
|
||||
month: int | None
|
||||
weekday: _Weekday | None
|
||||
day: int | None
|
||||
hour: int | None
|
||||
minute: int | None
|
||||
second: int | None
|
||||
microsecond: int | None
|
||||
def __init__(
|
||||
self,
|
||||
dt1: date | None = None,
|
||||
dt2: date | None = None,
|
||||
years: int | None = 0,
|
||||
months: int | None = 0,
|
||||
days: int | None = 0,
|
||||
leapdays: int | None = 0,
|
||||
weeks: int | None = 0,
|
||||
hours: int | None = 0,
|
||||
minutes: int | None = 0,
|
||||
seconds: int | None = 0,
|
||||
microseconds: int | None = 0,
|
||||
year: int | None = None,
|
||||
month: int | None = None,
|
||||
day: int | None = None,
|
||||
weekday: int | _Weekday | None = None,
|
||||
yearday: int | None = None,
|
||||
nlyearday: int | None = None,
|
||||
hour: int | None = None,
|
||||
minute: int | None = None,
|
||||
second: int | None = None,
|
||||
microsecond: int | None = None,
|
||||
) -> None: ...
|
||||
@property
|
||||
def weeks(self) -> int: ...
|
||||
@weeks.setter
|
||||
def weeks(self, value: int) -> None: ...
|
||||
def normalized(self) -> Self: ...
|
||||
# TODO: use Union when mypy will handle it properly in overloaded operator
|
||||
# methods (#2129, #1442, #1264 in mypy)
|
||||
@overload
|
||||
def __add__(self, other: relativedelta) -> Self: ...
|
||||
@overload
|
||||
def __add__(self, other: timedelta) -> Self: ...
|
||||
@overload
|
||||
def __add__(self, other: _DateT) -> _DateT: ...
|
||||
@overload
|
||||
def __radd__(self, other: relativedelta) -> Self: ...
|
||||
@overload
|
||||
def __radd__(self, other: timedelta) -> Self: ...
|
||||
@overload
|
||||
def __radd__(self, other: _DateT) -> _DateT: ...
|
||||
@overload
|
||||
def __rsub__(self, other: relativedelta) -> Self: ...
|
||||
@overload
|
||||
def __rsub__(self, other: timedelta) -> Self: ...
|
||||
@overload
|
||||
def __rsub__(self, other: _DateT) -> _DateT: ...
|
||||
def __sub__(self, other: relativedelta) -> Self: ...
|
||||
def __neg__(self) -> Self: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
||||
def __mul__(self, other: SupportsFloat) -> Self: ...
|
||||
def __rmul__(self, other: SupportsFloat) -> Self: ...
|
||||
def __eq__(self, other: object) -> bool: ...
|
||||
def __ne__(self, other: object) -> bool: ...
|
||||
def __div__(self, other: SupportsFloat) -> Self: ...
|
||||
def __truediv__(self, other: SupportsFloat) -> Self: ...
|
||||
def __abs__(self) -> Self: ...
|
||||
def __hash__(self) -> int: ...
|
111
lib/dateutil-stubs/rrule.pyi
Normal file
111
lib/dateutil-stubs/rrule.pyi
Normal file
|
@ -0,0 +1,111 @@
|
|||
import datetime
|
||||
from _typeshed import Incomplete
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from ._common import weekday as weekdaybase
|
||||
|
||||
YEARLY: int
|
||||
MONTHLY: int
|
||||
WEEKLY: int
|
||||
DAILY: int
|
||||
HOURLY: int
|
||||
MINUTELY: int
|
||||
SECONDLY: int
|
||||
|
||||
class weekday(weekdaybase): ...
|
||||
|
||||
weekdays: tuple[weekday, weekday, weekday, weekday, weekday, weekday, weekday]
|
||||
MO: weekday
|
||||
TU: weekday
|
||||
WE: weekday
|
||||
TH: weekday
|
||||
FR: weekday
|
||||
SA: weekday
|
||||
SU: weekday
|
||||
|
||||
class rrulebase:
|
||||
def __init__(self, cache: bool = False) -> None: ...
|
||||
def __iter__(self) -> Iterator[datetime.datetime]: ...
|
||||
def __getitem__(self, item): ...
|
||||
def __contains__(self, item): ...
|
||||
def count(self): ...
|
||||
def before(self, dt, inc: bool = False): ...
|
||||
def after(self, dt, inc: bool = False): ...
|
||||
def xafter(self, dt, count: Incomplete | None = None, inc: bool = False): ...
|
||||
def between(self, after, before, inc: bool = False, count: int = 1): ...
|
||||
|
||||
class rrule(rrulebase):
|
||||
def __init__(
|
||||
self,
|
||||
freq,
|
||||
dtstart: datetime.date | None = None,
|
||||
interval: int = 1,
|
||||
wkst: weekday | int | None = None,
|
||||
count: int | None = None,
|
||||
until: datetime.date | int | None = None,
|
||||
bysetpos: int | Iterable[int] | None = None,
|
||||
bymonth: int | Iterable[int] | None = None,
|
||||
bymonthday: int | Iterable[int] | None = None,
|
||||
byyearday: int | Iterable[int] | None = None,
|
||||
byeaster: int | Iterable[int] | None = None,
|
||||
byweekno: int | Iterable[int] | None = None,
|
||||
byweekday: int | weekday | Iterable[int] | Iterable[weekday] | None = None,
|
||||
byhour: int | Iterable[int] | None = None,
|
||||
byminute: int | Iterable[int] | None = None,
|
||||
bysecond: int | Iterable[int] | None = None,
|
||||
cache: bool = False,
|
||||
) -> None: ...
|
||||
def replace(self, **kwargs): ...
|
||||
|
||||
_RRule: TypeAlias = rrule
|
||||
|
||||
class _iterinfo:
|
||||
rrule: _RRule
|
||||
def __init__(self, rrule: _RRule) -> None: ...
|
||||
yearlen: int | None
|
||||
nextyearlen: int | None
|
||||
yearordinal: int | None
|
||||
yearweekday: int | None
|
||||
mmask: Sequence[int] | None
|
||||
mdaymask: Sequence[int] | None
|
||||
nmdaymask: Sequence[int] | None
|
||||
wdaymask: Sequence[int] | None
|
||||
mrange: Sequence[int] | None
|
||||
wnomask: Sequence[int] | None
|
||||
nwdaymask: Sequence[int] | None
|
||||
eastermask: Sequence[int] | None
|
||||
lastyear: int | None
|
||||
lastmonth: int | None
|
||||
def rebuild(self, year, month): ...
|
||||
def ydayset(self, year, month, day): ...
|
||||
def mdayset(self, year, month, day): ...
|
||||
def wdayset(self, year, month, day): ...
|
||||
def ddayset(self, year, month, day): ...
|
||||
def htimeset(self, hour, minute, second): ...
|
||||
def mtimeset(self, hour, minute, second): ...
|
||||
def stimeset(self, hour, minute, second): ...
|
||||
|
||||
class rruleset(rrulebase):
|
||||
class _genitem:
|
||||
dt: Incomplete
|
||||
genlist: list[Incomplete]
|
||||
gen: Incomplete
|
||||
def __init__(self, genlist, gen) -> None: ...
|
||||
def __next__(self) -> None: ...
|
||||
next = __next__
|
||||
def __lt__(self, other) -> bool: ...
|
||||
def __gt__(self, other) -> bool: ...
|
||||
def __eq__(self, other) -> bool: ...
|
||||
def __ne__(self, other) -> bool: ...
|
||||
|
||||
def __init__(self, cache: bool = False) -> None: ...
|
||||
def rrule(self, rrule: _RRule): ...
|
||||
def rdate(self, rdate): ...
|
||||
def exrule(self, exrule): ...
|
||||
def exdate(self, exdate): ...
|
||||
|
||||
class _rrulestr:
|
||||
def __call__(self, s, **kwargs) -> rrule | rruleset: ...
|
||||
|
||||
rrulestr: _rrulestr
|
15
lib/dateutil-stubs/tz/__init__.pyi
Normal file
15
lib/dateutil-stubs/tz/__init__.pyi
Normal file
|
@ -0,0 +1,15 @@
|
|||
from .tz import (
|
||||
datetime_ambiguous as datetime_ambiguous,
|
||||
datetime_exists as datetime_exists,
|
||||
gettz as gettz,
|
||||
resolve_imaginary as resolve_imaginary,
|
||||
tzfile as tzfile,
|
||||
tzical as tzical,
|
||||
tzlocal as tzlocal,
|
||||
tzoffset as tzoffset,
|
||||
tzrange as tzrange,
|
||||
tzstr as tzstr,
|
||||
tzutc as tzutc,
|
||||
)
|
||||
|
||||
UTC: tzutc
|
28
lib/dateutil-stubs/tz/_common.pyi
Normal file
28
lib/dateutil-stubs/tz/_common.pyi
Normal file
|
@ -0,0 +1,28 @@
|
|||
import abc
|
||||
from datetime import datetime, timedelta, tzinfo
|
||||
from typing import ClassVar
|
||||
|
||||
def tzname_in_python2(namefunc): ...
|
||||
def enfold(dt: datetime, fold: int = 1): ...
|
||||
|
||||
class _DatetimeWithFold(datetime):
|
||||
@property
|
||||
def fold(self): ...
|
||||
|
||||
# Doesn't actually have ABCMeta as the metaclass at runtime,
|
||||
# but mypy complains if we don't have it in the stub.
|
||||
# See discussion in #8908
|
||||
class _tzinfo(tzinfo, metaclass=abc.ABCMeta):
|
||||
def is_ambiguous(self, dt: datetime) -> bool: ...
|
||||
def fromutc(self, dt: datetime) -> datetime: ...
|
||||
|
||||
class tzrangebase(_tzinfo):
|
||||
def __init__(self) -> None: ...
|
||||
def utcoffset(self, dt: datetime | None) -> timedelta | None: ...
|
||||
def dst(self, dt: datetime | None) -> timedelta | None: ...
|
||||
def tzname(self, dt: datetime | None) -> str: ...
|
||||
def fromutc(self, dt: datetime) -> datetime: ...
|
||||
def is_ambiguous(self, dt: datetime) -> bool: ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
def __ne__(self, other): ...
|
||||
__reduce__ = object.__reduce__
|
115
lib/dateutil-stubs/tz/tz.pyi
Normal file
115
lib/dateutil-stubs/tz/tz.pyi
Normal file
|
@ -0,0 +1,115 @@
|
|||
import datetime
|
||||
from _typeshed import Incomplete
|
||||
from typing import ClassVar, Literal, Protocol, TypeVar
|
||||
|
||||
from ..relativedelta import relativedelta
|
||||
from ._common import _tzinfo as _tzinfo, enfold as enfold, tzname_in_python2 as tzname_in_python2, tzrangebase as tzrangebase
|
||||
|
||||
_DT = TypeVar("_DT", bound=datetime.datetime)
|
||||
|
||||
ZERO: datetime.timedelta
|
||||
EPOCH: datetime.datetime
|
||||
EPOCHORDINAL: int
|
||||
|
||||
class tzutc(datetime.tzinfo):
|
||||
def utcoffset(self, dt: datetime.datetime | None) -> datetime.timedelta | None: ...
|
||||
def dst(self, dt: datetime.datetime | None) -> datetime.timedelta | None: ...
|
||||
def tzname(self, dt: datetime.datetime | None) -> str: ...
|
||||
def is_ambiguous(self, dt: datetime.datetime | None) -> bool: ...
|
||||
def fromutc(self, dt: _DT) -> _DT: ...
|
||||
def __eq__(self, other): ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
def __ne__(self, other): ...
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
class tzoffset(datetime.tzinfo):
|
||||
def __init__(self, name, offset) -> None: ...
|
||||
def utcoffset(self, dt: datetime.datetime | None) -> datetime.timedelta | None: ...
|
||||
def dst(self, dt: datetime.datetime | None) -> datetime.timedelta | None: ...
|
||||
def is_ambiguous(self, dt: datetime.datetime | None) -> bool: ...
|
||||
def tzname(self, dt: datetime.datetime | None) -> str: ...
|
||||
def fromutc(self, dt: _DT) -> _DT: ...
|
||||
def __eq__(self, other): ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
def __ne__(self, other): ...
|
||||
__reduce__ = object.__reduce__
|
||||
@classmethod
|
||||
def instance(cls, name, offset) -> tzoffset: ...
|
||||
|
||||
class tzlocal(_tzinfo):
|
||||
def __init__(self) -> None: ...
|
||||
def utcoffset(self, dt: datetime.datetime | None) -> datetime.timedelta | None: ...
|
||||
def dst(self, dt: datetime.datetime | None) -> datetime.timedelta | None: ...
|
||||
def tzname(self, dt: datetime.datetime | None) -> str: ...
|
||||
def is_ambiguous(self, dt: datetime.datetime | None) -> bool: ...
|
||||
def __eq__(self, other): ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
def __ne__(self, other): ...
|
||||
__reduce__ = object.__reduce__
|
||||
|
||||
class _ttinfo:
|
||||
def __init__(self) -> None: ...
|
||||
def __eq__(self, other): ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
def __ne__(self, other): ...
|
||||
|
||||
class _TZFileReader(Protocol):
|
||||
# optional attribute:
|
||||
# name: str
|
||||
def read(self, size: int, /) -> bytes: ...
|
||||
def seek(self, target: int, whence: Literal[1], /) -> object: ...
|
||||
|
||||
class tzfile(_tzinfo):
|
||||
def __init__(self, fileobj: str | _TZFileReader, filename: str | None = None) -> None: ...
|
||||
def is_ambiguous(self, dt: datetime.datetime | None, idx: int | None = None) -> bool: ...
|
||||
def utcoffset(self, dt: datetime.datetime | None) -> datetime.timedelta | None: ...
|
||||
def dst(self, dt: datetime.datetime | None) -> datetime.timedelta | None: ...
|
||||
def tzname(self, dt: datetime.datetime | None) -> str: ...
|
||||
def __eq__(self, other): ...
|
||||
__hash__: ClassVar[None] # type: ignore[assignment]
|
||||
def __ne__(self, other): ...
|
||||
def __reduce__(self): ...
|
||||
def __reduce_ex__(self, protocol): ...
|
||||
|
||||
class tzrange(tzrangebase):
|
||||
hasdst: bool
|
||||
def __init__(
|
||||
self,
|
||||
stdabbr: str,
|
||||
stdoffset: int | datetime.timedelta | None = None,
|
||||
dstabbr: str | None = None,
|
||||
dstoffset: int | datetime.timedelta | None = None,
|
||||
start: relativedelta | None = None,
|
||||
end: relativedelta | None = None,
|
||||
) -> None: ...
|
||||
def transitions(self, year: int) -> tuple[datetime.datetime, datetime.datetime]: ...
|
||||
def __eq__(self, other): ...
|
||||
|
||||
class tzstr(tzrange):
|
||||
hasdst: bool
|
||||
def __init__(self, s: str, posix_offset: bool = False) -> None: ...
|
||||
@classmethod
|
||||
def instance(cls, name, offset) -> tzoffset: ...
|
||||
|
||||
class _ICalReader(Protocol):
|
||||
# optional attribute:
|
||||
# name: str
|
||||
def read(self) -> str: ...
|
||||
|
||||
class tzical:
|
||||
def __init__(self, fileobj: str | _ICalReader) -> None: ...
|
||||
def keys(self): ...
|
||||
def get(self, tzid: Incomplete | None = None): ...
|
||||
|
||||
TZFILES: list[str]
|
||||
TZPATHS: list[str]
|
||||
|
||||
def datetime_exists(dt: datetime.datetime, tz: datetime.tzinfo | None = None) -> bool: ...
|
||||
def datetime_ambiguous(dt: datetime.datetime, tz: datetime.tzinfo | None = None) -> bool: ...
|
||||
def resolve_imaginary(dt: datetime.datetime) -> datetime.datetime: ...
|
||||
|
||||
class _GetTZ:
|
||||
def __call__(self, name: str | None = ...) -> datetime.tzinfo | None: ...
|
||||
def nocache(self, name: str | None) -> datetime.tzinfo | None: ...
|
||||
|
||||
gettz: _GetTZ
|
5
lib/dateutil-stubs/utils.pyi
Normal file
5
lib/dateutil-stubs/utils.pyi
Normal file
|
@ -0,0 +1,5 @@
|
|||
from datetime import datetime, timedelta, tzinfo
|
||||
|
||||
def default_tzinfo(dt: datetime, tzinfo: tzinfo) -> datetime: ...
|
||||
def today(tzinfo: tzinfo | None = None) -> datetime: ...
|
||||
def within_delta(dt1: datetime, dt2: datetime, delta: timedelta) -> bool: ...
|
17
lib/dateutil-stubs/zoneinfo/__init__.pyi
Normal file
17
lib/dateutil-stubs/zoneinfo/__init__.pyi
Normal file
|
@ -0,0 +1,17 @@
|
|||
from _typeshed import Incomplete
|
||||
from typing import IO
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||
|
||||
_MetadataType: TypeAlias = dict[str, Incomplete]
|
||||
|
||||
class ZoneInfoFile:
|
||||
zones: dict[Incomplete, Incomplete]
|
||||
metadata: _MetadataType | None
|
||||
def __init__(self, zonefile_stream: IO[bytes] | None = None) -> None: ...
|
||||
def get(self, name, default: Incomplete | None = None): ...
|
||||
|
||||
def get_zonefile_instance(new_instance: bool = False) -> ZoneInfoFile: ...
|
||||
def gettz(name): ...
|
||||
def gettz_db_metadata() -> _MetadataType: ...
|
11
lib/dateutil-stubs/zoneinfo/rebuild.pyi
Normal file
11
lib/dateutil-stubs/zoneinfo/rebuild.pyi
Normal file
|
@ -0,0 +1,11 @@
|
|||
from _typeshed import Incomplete, StrOrBytesPath
|
||||
from collections.abc import Sequence
|
||||
from tarfile import TarInfo
|
||||
|
||||
def rebuild(
|
||||
filename: StrOrBytesPath,
|
||||
tag: Incomplete | None = None,
|
||||
format: str = "gz",
|
||||
zonegroups: Sequence[str | TarInfo] = [],
|
||||
metadata: Incomplete | None = None,
|
||||
) -> None: ...
|
|
@ -1,4 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
|
@ -6,3 +8,17 @@ except ImportError:
|
|||
|
||||
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||
'utils', 'zoneinfo']
|
||||
|
||||
def __getattr__(name):
|
||||
import importlib
|
||||
|
||||
if name in __all__:
|
||||
return importlib.import_module("." + name, __name__)
|
||||
raise AttributeError(
|
||||
"module {!r} has not attribute {!r}".format(__name__, name)
|
||||
)
|
||||
|
||||
|
||||
def __dir__():
|
||||
# __dir__ should include all the lazy-importable modules as well.
|
||||
return [x for x in globals() if x not in sys.modules] + __all__
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
# coding: utf-8
|
||||
# file generated by setuptools_scm
|
||||
# don't change, don't track in version control
|
||||
version = '2.8.2'
|
||||
version_tuple = (2, 8, 2)
|
||||
__version__ = version = '2.9.0.post0'
|
||||
__version_tuple__ = version_tuple = (2, 9, 0)
|
||||
|
|
|
@ -72,7 +72,7 @@ class isoparser(object):
|
|||
Common:
|
||||
|
||||
- ``YYYY``
|
||||
- ``YYYY-MM`` or ``YYYYMM``
|
||||
- ``YYYY-MM``
|
||||
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
||||
|
||||
Uncommon:
|
||||
|
|
|
@ -48,7 +48,7 @@ class relativedelta(object):
|
|||
the corresponding arithmetic operation on the original datetime value
|
||||
with the information in the relativedelta.
|
||||
|
||||
weekday:
|
||||
weekday:
|
||||
One of the weekday instances (MO, TU, etc) available in the
|
||||
relativedelta module. These instances may receive a parameter N,
|
||||
specifying the Nth weekday, which could be positive or negative
|
||||
|
|
|
@ -182,7 +182,7 @@ class rrulebase(object):
|
|||
# __len__() introduces a large performance penalty.
|
||||
def count(self):
|
||||
""" Returns the number of recurrences in this set. It will have go
|
||||
trough the whole recurrence, if this hasn't been done before. """
|
||||
through the whole recurrence, if this hasn't been done before. """
|
||||
if self._len is None:
|
||||
for x in self:
|
||||
pass
|
||||
|
|
|
@ -34,7 +34,7 @@ except ImportError:
|
|||
from warnings import warn
|
||||
|
||||
ZERO = datetime.timedelta(0)
|
||||
EPOCH = datetime.datetime.utcfromtimestamp(0)
|
||||
EPOCH = datetime.datetime(1970, 1, 1, 0, 0)
|
||||
EPOCHORDINAL = EPOCH.toordinal()
|
||||
|
||||
|
||||
|
|
Binary file not shown.
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# Copyright 2015,2016,2017 Nir Cohen
|
||||
# Copyright 2015-2021 Nir Cohen
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
@ -55,7 +55,7 @@ except ImportError:
|
|||
# Python 3.7
|
||||
TypedDict = dict
|
||||
|
||||
__version__ = "1.8.0"
|
||||
__version__ = "1.9.0"
|
||||
|
||||
|
||||
class VersionDict(TypedDict):
|
||||
|
@ -125,6 +125,7 @@ _DISTRO_RELEASE_BASENAME_PATTERN = re.compile(r"(\w+)[-_](release|version)$")
|
|||
# Base file names to be looked up for if _UNIXCONFDIR is not readable.
|
||||
_DISTRO_RELEASE_BASENAMES = [
|
||||
"SuSE-release",
|
||||
"altlinux-release",
|
||||
"arch-release",
|
||||
"base-release",
|
||||
"centos-release",
|
||||
|
@ -151,6 +152,8 @@ _DISTRO_RELEASE_IGNORE_BASENAMES = (
|
|||
"system-release",
|
||||
"plesk-release",
|
||||
"iredmail-release",
|
||||
"board-release",
|
||||
"ec2_version",
|
||||
)
|
||||
|
||||
|
||||
|
@ -243,6 +246,7 @@ def id() -> str:
|
|||
"rocky" Rocky Linux
|
||||
"aix" AIX
|
||||
"guix" Guix System
|
||||
"altlinux" ALT Linux
|
||||
============== =========================================
|
||||
|
||||
If you have a need to get distros for reliable IDs added into this set,
|
||||
|
@ -991,10 +995,10 @@ class LinuxDistribution:
|
|||
|
||||
For details, see :func:`distro.info`.
|
||||
"""
|
||||
return dict(
|
||||
return InfoDict(
|
||||
id=self.id(),
|
||||
version=self.version(pretty, best),
|
||||
version_parts=dict(
|
||||
version_parts=VersionDict(
|
||||
major=self.major_version(best),
|
||||
minor=self.minor_version(best),
|
||||
build_number=self.build_number(best),
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from os import urandom
|
||||
from base64 import b64encode, b64decode
|
||||
from hashlib import pbkdf2_hmac
|
||||
|
@ -30,7 +29,7 @@ HASH_FUNCTION = 'sha256' # Must be in hashlib.
|
|||
# Linear to the hashing time. Adjust to be high but take a reasonable
|
||||
# amount of time on your server. Measure with:
|
||||
# python -m timeit -s 'import passwords as p' 'p.make_hash("something")'
|
||||
COST_FACTOR = 10000
|
||||
COST_FACTOR = 600000
|
||||
|
||||
|
||||
def make_hash(password):
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from .core import encode, decode, alabel, ulabel, IDNAError
|
||||
import codecs
|
||||
import re
|
||||
from typing import Tuple, Optional
|
||||
from typing import Any, Tuple, Optional
|
||||
|
||||
_unicode_dots_re = re.compile('[\u002e\u3002\uff0e\uff61]')
|
||||
|
||||
|
@ -26,24 +26,24 @@ class Codec(codecs.Codec):
|
|||
return decode(data), len(data)
|
||||
|
||||
class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
|
||||
def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[str, int]: # type: ignore
|
||||
def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[bytes, int]:
|
||||
if errors != 'strict':
|
||||
raise IDNAError('Unsupported error handling \"{}\"'.format(errors))
|
||||
|
||||
if not data:
|
||||
return "", 0
|
||||
return b'', 0
|
||||
|
||||
labels = _unicode_dots_re.split(data)
|
||||
trailing_dot = ''
|
||||
trailing_dot = b''
|
||||
if labels:
|
||||
if not labels[-1]:
|
||||
trailing_dot = '.'
|
||||
trailing_dot = b'.'
|
||||
del labels[-1]
|
||||
elif not final:
|
||||
# Keep potentially unfinished label until the next call
|
||||
del labels[-1]
|
||||
if labels:
|
||||
trailing_dot = '.'
|
||||
trailing_dot = b'.'
|
||||
|
||||
result = []
|
||||
size = 0
|
||||
|
@ -54,18 +54,21 @@ class IncrementalEncoder(codecs.BufferedIncrementalEncoder):
|
|||
size += len(label)
|
||||
|
||||
# Join with U+002E
|
||||
result_str = '.'.join(result) + trailing_dot # type: ignore
|
||||
result_bytes = b'.'.join(result) + trailing_dot
|
||||
size += len(trailing_dot)
|
||||
return result_str, size
|
||||
return result_bytes, size
|
||||
|
||||
class IncrementalDecoder(codecs.BufferedIncrementalDecoder):
|
||||
def _buffer_decode(self, data: str, errors: str, final: bool) -> Tuple[str, int]: # type: ignore
|
||||
def _buffer_decode(self, data: Any, errors: str, final: bool) -> Tuple[str, int]:
|
||||
if errors != 'strict':
|
||||
raise IDNAError('Unsupported error handling \"{}\"'.format(errors))
|
||||
|
||||
if not data:
|
||||
return ('', 0)
|
||||
|
||||
if not isinstance(data, str):
|
||||
data = str(data, 'ascii')
|
||||
|
||||
labels = _unicode_dots_re.split(data)
|
||||
trailing_dot = ''
|
||||
if labels:
|
||||
|
@ -99,14 +102,17 @@ class StreamReader(Codec, codecs.StreamReader):
|
|||
pass
|
||||
|
||||
|
||||
def getregentry() -> codecs.CodecInfo:
|
||||
# Compatibility as a search_function for codecs.register()
|
||||
def search_function(name: str) -> Optional[codecs.CodecInfo]:
|
||||
if name != 'idna2008':
|
||||
return None
|
||||
return codecs.CodecInfo(
|
||||
name='idna',
|
||||
encode=Codec().encode, # type: ignore
|
||||
decode=Codec().decode, # type: ignore
|
||||
name=name,
|
||||
encode=Codec().encode,
|
||||
decode=Codec().decode,
|
||||
incrementalencoder=IncrementalEncoder,
|
||||
incrementaldecoder=IncrementalDecoder,
|
||||
streamwriter=StreamWriter,
|
||||
streamreader=StreamReader,
|
||||
)
|
||||
|
||||
codecs.register(search_function)
|
||||
|
|
|
@ -150,9 +150,11 @@ def valid_contextj(label: str, pos: int) -> bool:
|
|||
joining_type = idnadata.joining_types.get(ord(label[i]))
|
||||
if joining_type == ord('T'):
|
||||
continue
|
||||
if joining_type in [ord('L'), ord('D')]:
|
||||
elif joining_type in [ord('L'), ord('D')]:
|
||||
ok = True
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
if not ok:
|
||||
return False
|
||||
|
@ -162,9 +164,11 @@ def valid_contextj(label: str, pos: int) -> bool:
|
|||
joining_type = idnadata.joining_types.get(ord(label[i]))
|
||||
if joining_type == ord('T'):
|
||||
continue
|
||||
if joining_type in [ord('R'), ord('D')]:
|
||||
elif joining_type in [ord('R'), ord('D')]:
|
||||
ok = True
|
||||
break
|
||||
else:
|
||||
break
|
||||
return ok
|
||||
|
||||
if cp_value == 0x200d:
|
||||
|
@ -236,12 +240,8 @@ def check_label(label: Union[str, bytes, bytearray]) -> None:
|
|||
if intranges_contain(cp_value, idnadata.codepoint_classes['PVALID']):
|
||||
continue
|
||||
elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTJ']):
|
||||
try:
|
||||
if not valid_contextj(label, pos):
|
||||
raise InvalidCodepointContext('Joiner {} not allowed at position {} in {}'.format(
|
||||
_unot(cp_value), pos+1, repr(label)))
|
||||
except ValueError:
|
||||
raise IDNAError('Unknown codepoint adjacent to joiner {} at position {} in {}'.format(
|
||||
if not valid_contextj(label, pos):
|
||||
raise InvalidCodepointContext('Joiner {} not allowed at position {} in {}'.format(
|
||||
_unot(cp_value), pos+1, repr(label)))
|
||||
elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTO']):
|
||||
if not valid_contexto(label, pos):
|
||||
|
@ -262,13 +262,8 @@ def alabel(label: str) -> bytes:
|
|||
except UnicodeEncodeError:
|
||||
pass
|
||||
|
||||
if not label:
|
||||
raise IDNAError('No Input')
|
||||
|
||||
label = str(label)
|
||||
check_label(label)
|
||||
label_bytes = _punycode(label)
|
||||
label_bytes = _alabel_prefix + label_bytes
|
||||
label_bytes = _alabel_prefix + _punycode(label)
|
||||
|
||||
if not valid_label_length(label_bytes):
|
||||
raise IDNAError('Label too long')
|
||||
|
@ -318,7 +313,7 @@ def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False
|
|||
status = uts46row[1]
|
||||
replacement = None # type: Optional[str]
|
||||
if len(uts46row) == 3:
|
||||
replacement = uts46row[2] # type: ignore
|
||||
replacement = uts46row[2]
|
||||
if (status == 'V' or
|
||||
(status == 'D' and not transitional) or
|
||||
(status == '3' and not std3_rules and replacement is None)):
|
||||
|
@ -338,9 +333,9 @@ def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False
|
|||
|
||||
|
||||
def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False, transitional: bool = False) -> bytes:
|
||||
if isinstance(s, (bytes, bytearray)):
|
||||
if not isinstance(s, str):
|
||||
try:
|
||||
s = s.decode('ascii')
|
||||
s = str(s, 'ascii')
|
||||
except UnicodeDecodeError:
|
||||
raise IDNAError('should pass a unicode string to the function rather than a byte string.')
|
||||
if uts46:
|
||||
|
@ -372,8 +367,8 @@ def encode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool =
|
|||
|
||||
def decode(s: Union[str, bytes, bytearray], strict: bool = False, uts46: bool = False, std3_rules: bool = False) -> str:
|
||||
try:
|
||||
if isinstance(s, (bytes, bytearray)):
|
||||
s = s.decode('ascii')
|
||||
if not isinstance(s, str):
|
||||
s = str(s, 'ascii')
|
||||
except UnicodeDecodeError:
|
||||
raise IDNAError('Invalid ASCII in A-label')
|
||||
if uts46:
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue