Initial commit

This commit is contained in:
Todd Sales
2025-06-23 21:01:55 -04:00
commit d42b70325a
2000 changed files with 183840 additions and 0 deletions

View File

@ -0,0 +1 @@
pip

View File

@ -0,0 +1,27 @@
Copyright (c) Django Software Foundation and individual contributors.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of Django nor the names of its contributors may be used
to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,246 @@
Metadata-Version: 2.1
Name: asgiref
Version: 3.8.1
Summary: ASGI specs, helper code, and adapters
Home-page: https://github.com/django/asgiref/
Author: Django Software Foundation
Author-email: foundation@djangoproject.com
License: BSD-3-Clause
Project-URL: Documentation, https://asgi.readthedocs.io/
Project-URL: Further Documentation, https://docs.djangoproject.com/en/stable/topics/async/#async-adapter-functions
Project-URL: Changelog, https://github.com/django/asgiref/blob/master/CHANGELOG.txt
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.8
License-File: LICENSE
Requires-Dist: typing-extensions >=4 ; python_version < "3.11"
Provides-Extra: tests
Requires-Dist: pytest ; extra == 'tests'
Requires-Dist: pytest-asyncio ; extra == 'tests'
Requires-Dist: mypy >=0.800 ; extra == 'tests'
asgiref
=======
.. image:: https://github.com/django/asgiref/actions/workflows/tests.yml/badge.svg
:target: https://github.com/django/asgiref/actions/workflows/tests.yml
.. image:: https://img.shields.io/pypi/v/asgiref.svg
:target: https://pypi.python.org/pypi/asgiref
ASGI is a standard for Python asynchronous web apps and servers to communicate
with each other, and positioned as an asynchronous successor to WSGI. You can
read more at https://asgi.readthedocs.io/en/latest/
This package includes ASGI base libraries, such as:
* Sync-to-async and async-to-sync function wrappers, ``asgiref.sync``
* Server base classes, ``asgiref.server``
* A WSGI-to-ASGI adapter, in ``asgiref.wsgi``
Function wrappers
-----------------
These allow you to wrap or decorate async or sync functions to call them from
the other style (so you can call async functions from a synchronous thread,
or vice-versa).
In particular:
* AsyncToSync lets a synchronous subthread stop and wait while the async
function is called on the main thread's event loop, and then control is
returned to the thread when the async function is finished.
* SyncToAsync lets async code call a synchronous function, which is run in
a threadpool and control returned to the async coroutine when the synchronous
function completes.
The idea is to make it easier to call synchronous APIs from async code and
asynchronous APIs from synchronous code so it's easier to transition code from
one style to the other. In the case of Channels, we wrap the (synchronous)
Django view system with SyncToAsync to allow it to run inside the (asynchronous)
ASGI server.
Note that exactly what threads things run in is very specific, and aimed to
keep maximum compatibility with old synchronous code. See
"Synchronous code & Threads" below for a full explanation. By default,
``sync_to_async`` will run all synchronous code in the program in the same
thread for safety reasons; you can disable this for more performance with
``@sync_to_async(thread_sensitive=False)``, but make sure that your code does
not rely on anything bound to threads (like database connections) when you do.
Threadlocal replacement
-----------------------
This is a drop-in replacement for ``threading.local`` that works with both
threads and asyncio Tasks. Even better, it will proxy values through from a
task-local context to a thread-local context when you use ``sync_to_async``
to run things in a threadpool, and vice-versa for ``async_to_sync``.
If you instead want true thread- and task-safety, you can set
``thread_critical`` on the Local object to ensure this instead.
Server base classes
-------------------
Includes a ``StatelessServer`` class which provides all the hard work of
writing a stateless server (as in, does not handle direct incoming sockets
but instead consumes external streams or sockets to work out what is happening).
An example of such a server would be a chatbot server that connects out to
a central chat server and provides a "connection scope" per user chatting to
it. There's only one actual connection, but the server has to separate things
into several scopes for easier writing of the code.
You can see an example of this being used in `frequensgi <https://github.com/andrewgodwin/frequensgi>`_.
WSGI-to-ASGI adapter
--------------------
Allows you to wrap a WSGI application so it appears as a valid ASGI application.
Simply wrap it around your WSGI application like so::
asgi_application = WsgiToAsgi(wsgi_application)
The WSGI application will be run in a synchronous threadpool, and the wrapped
ASGI application will be one that accepts ``http`` class messages.
Please note that not all extended features of WSGI may be supported (such as
file handles for incoming POST bodies).
Dependencies
------------
``asgiref`` requires Python 3.8 or higher.
Contributing
------------
Please refer to the
`main Channels contributing docs <https://github.com/django/channels/blob/master/CONTRIBUTING.rst>`_.
Testing
'''''''
To run tests, make sure you have installed the ``tests`` extra with the package::
cd asgiref/
pip install -e .[tests]
pytest
Building the documentation
''''''''''''''''''''''''''
The documentation uses `Sphinx <http://www.sphinx-doc.org>`_::
cd asgiref/docs/
pip install sphinx
To build the docs, you can use the default tools::
sphinx-build -b html . _build/html # or `make html`, if you've got make set up
cd _build/html
python -m http.server
...or you can use ``sphinx-autobuild`` to run a server and rebuild/reload
your documentation changes automatically::
pip install sphinx-autobuild
sphinx-autobuild . _build/html
Releasing
'''''''''
To release, first add details to CHANGELOG.txt and update the version number in ``asgiref/__init__.py``.
Then, build and push the packages::
python -m build
twine upload dist/*
rm -r build/ dist/
Implementation Details
----------------------
Synchronous code & threads
''''''''''''''''''''''''''
The ``asgiref.sync`` module provides two wrappers that let you go between
asynchronous and synchronous code at will, while taking care of the rough edges
for you.
Unfortunately, the rough edges are numerous, and the code has to work especially
hard to keep things in the same thread as much as possible. Notably, the
restrictions we are working with are:
* All synchronous code called through ``SyncToAsync`` and marked with
``thread_sensitive`` should run in the same thread as each other (and if the
outer layer of the program is synchronous, the main thread)
* If a thread already has a running async loop, ``AsyncToSync`` can't run things
on that loop if it's blocked on synchronous code that is above you in the
call stack.
The first compromise you get to might be that ``thread_sensitive`` code should
just run in the same thread and not spawn in a sub-thread, fulfilling the first
restriction, but that immediately runs you into the second restriction.
The only real solution is to essentially have a variant of ThreadPoolExecutor
that executes any ``thread_sensitive`` code on the outermost synchronous
thread - either the main thread, or a single spawned subthread.
This means you now have two basic states:
* If the outermost layer of your program is synchronous, then all async code
run through ``AsyncToSync`` will run in a per-call event loop in arbitrary
sub-threads, while all ``thread_sensitive`` code will run in the main thread.
* If the outermost layer of your program is asynchronous, then all async code
runs on the main thread's event loop, and all ``thread_sensitive`` synchronous
code will run in a single shared sub-thread.
Crucially, this means that in both cases there is a thread which is a shared
resource that all ``thread_sensitive`` code must run on, and there is a chance
that this thread is currently blocked on its own ``AsyncToSync`` call. Thus,
``AsyncToSync`` needs to act as an executor for thread code while it's blocking.
The ``CurrentThreadExecutor`` class provides this functionality; rather than
simply waiting on a Future, you can call its ``run_until_future`` method and
it will run submitted code until that Future is done. This means that code
inside the call can then run code on your thread.
Maintenance and Security
------------------------
To report security issues, please contact security@djangoproject.com. For GPG
signatures and more security process information, see
https://docs.djangoproject.com/en/dev/internals/security/.
To report bugs or request new features, please open a new GitHub issue.
This repository is part of the Channels project. For the shepherd and maintenance team, please see the
`main Channels readme <https://github.com/django/channels/blob/master/README.rst>`_.

View File

@ -0,0 +1,27 @@
asgiref-3.8.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
asgiref-3.8.1.dist-info/LICENSE,sha256=uEZBXRtRTpwd_xSiLeuQbXlLxUbKYSn5UKGM0JHipmk,1552
asgiref-3.8.1.dist-info/METADATA,sha256=Cbu67XPstSkMxAdA4puvY-FAzN9OrT_AasH7IuK6DaM,9259
asgiref-3.8.1.dist-info/RECORD,,
asgiref-3.8.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
asgiref-3.8.1.dist-info/top_level.txt,sha256=bokQjCzwwERhdBiPdvYEZa4cHxT4NCeAffQNUqJ8ssg,8
asgiref/__init__.py,sha256=kZzGpxWKY4rWDQrrrlM7bN7YKRAjy17Wv4w__djvVYU,22
asgiref/__pycache__/__init__.cpython-312.pyc,,
asgiref/__pycache__/compatibility.cpython-312.pyc,,
asgiref/__pycache__/current_thread_executor.cpython-312.pyc,,
asgiref/__pycache__/local.cpython-312.pyc,,
asgiref/__pycache__/server.cpython-312.pyc,,
asgiref/__pycache__/sync.cpython-312.pyc,,
asgiref/__pycache__/testing.cpython-312.pyc,,
asgiref/__pycache__/timeout.cpython-312.pyc,,
asgiref/__pycache__/typing.cpython-312.pyc,,
asgiref/__pycache__/wsgi.cpython-312.pyc,,
asgiref/compatibility.py,sha256=DhY1SOpOvOw0Y1lSEjCqg-znRUQKecG3LTaV48MZi68,1606
asgiref/current_thread_executor.py,sha256=EuowbT0oL_P4Fq8KTXNUyEgk3-k4Yh4E8F_anEVdeBI,3977
asgiref/local.py,sha256=bNeER_QIfw2-PAPYanqAZq6yAAEJ-aio7e9o8Up-mgI,4808
asgiref/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
asgiref/server.py,sha256=egTQhZo1k4G0F7SSBQNp_VOekpGcjBJZU2kkCoiGC_M,6005
asgiref/sync.py,sha256=Why0YQV84vSp7IBBr-JDbxYCua-InLgBjuiCMlj9WgI,21444
asgiref/testing.py,sha256=QgZgXKrwdq5xzhZqynr1msWOiTS3Kpastj7wHU2ePRY,3481
asgiref/timeout.py,sha256=LtGL-xQpG8JHprdsEUCMErJ0kNWj4qwWZhEHJ3iKu4s,3627
asgiref/typing.py,sha256=rLF3y_9OgvlQMaDm8yMw8QTgsO9Mv9YAc6Cj8xjvWo0,6264
asgiref/wsgi.py,sha256=fxBLgUE_0PEVgcp13ticz6GHf3q-aKWcB5eFPhd6yxo,6753

View File

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.43.0)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@ -0,0 +1 @@
asgiref

View File

@ -0,0 +1 @@
__version__ = "3.8.1"

View File

@ -0,0 +1,48 @@
import inspect
from .sync import iscoroutinefunction
def is_double_callable(application):
"""
Tests to see if an application is a legacy-style (double-callable) application.
"""
# Look for a hint on the object first
if getattr(application, "_asgi_single_callable", False):
return False
if getattr(application, "_asgi_double_callable", False):
return True
# Uninstanted classes are double-callable
if inspect.isclass(application):
return True
# Instanted classes depend on their __call__
if hasattr(application, "__call__"):
# We only check to see if its __call__ is a coroutine function -
# if it's not, it still might be a coroutine function itself.
if iscoroutinefunction(application.__call__):
return False
# Non-classes we just check directly
return not iscoroutinefunction(application)
def double_to_single_callable(application):
"""
Transforms a double-callable ASGI application into a single-callable one.
"""
async def new_application(scope, receive, send):
instance = application(scope)
return await instance(receive, send)
return new_application
def guarantee_single_callable(application):
"""
Takes either a single- or double-callable application and always returns it
in single-callable style. Use this to add backwards compatibility for ASGI
2.0 applications to your server/test harness/etc.
"""
if is_double_callable(application):
application = double_to_single_callable(application)
return application

View File

@ -0,0 +1,115 @@
import queue
import sys
import threading
from concurrent.futures import Executor, Future
from typing import TYPE_CHECKING, Any, Callable, TypeVar, Union
if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
from typing_extensions import ParamSpec
_T = TypeVar("_T")
_P = ParamSpec("_P")
_R = TypeVar("_R")
class _WorkItem:
"""
Represents an item needing to be run in the executor.
Copied from ThreadPoolExecutor (but it's private, so we're not going to rely on importing it)
"""
def __init__(
self,
future: "Future[_R]",
fn: Callable[_P, _R],
*args: _P.args,
**kwargs: _P.kwargs,
):
self.future = future
self.fn = fn
self.args = args
self.kwargs = kwargs
def run(self) -> None:
__traceback_hide__ = True # noqa: F841
if not self.future.set_running_or_notify_cancel():
return
try:
result = self.fn(*self.args, **self.kwargs)
except BaseException as exc:
self.future.set_exception(exc)
# Break a reference cycle with the exception 'exc'
self = None # type: ignore[assignment]
else:
self.future.set_result(result)
class CurrentThreadExecutor(Executor):
"""
An Executor that actually runs code in the thread it is instantiated in.
Passed to other threads running async code, so they can run sync code in
the thread they came from.
"""
def __init__(self) -> None:
self._work_thread = threading.current_thread()
self._work_queue: queue.Queue[Union[_WorkItem, "Future[Any]"]] = queue.Queue()
self._broken = False
def run_until_future(self, future: "Future[Any]") -> None:
"""
Runs the code in the work queue until a result is available from the future.
Should be run from the thread the executor is initialised in.
"""
# Check we're in the right thread
if threading.current_thread() != self._work_thread:
raise RuntimeError(
"You cannot run CurrentThreadExecutor from a different thread"
)
future.add_done_callback(self._work_queue.put)
# Keep getting and running work items until we get the future we're waiting for
# back via the future's done callback.
try:
while True:
# Get a work item and run it
work_item = self._work_queue.get()
if work_item is future:
return
assert isinstance(work_item, _WorkItem)
work_item.run()
del work_item
finally:
self._broken = True
def _submit(
self,
fn: Callable[_P, _R],
*args: _P.args,
**kwargs: _P.kwargs,
) -> "Future[_R]":
# Check they're not submitting from the same thread
if threading.current_thread() == self._work_thread:
raise RuntimeError(
"You cannot submit onto CurrentThreadExecutor from its own thread"
)
# Check they're not too late or the executor errored
if self._broken:
raise RuntimeError("CurrentThreadExecutor already quit or is broken")
# Add to work queue
f: "Future[_R]" = Future()
work_item = _WorkItem(f, fn, *args, **kwargs)
self._work_queue.put(work_item)
# Return the future
return f
# Python 3.9+ has a new signature for submit with a "/" after `fn`, to enforce
# it to be a positional argument. If we ignore[override] mypy on 3.9+ will be
# happy but 3.8 will say that the ignore comment is unused, even when
# defining them differently based on sys.version_info.
# We should be able to remove this when we drop support for 3.8.
if not TYPE_CHECKING:
def submit(self, fn, *args, **kwargs):
return self._submit(fn, *args, **kwargs)

View File

@ -0,0 +1,128 @@
import asyncio
import contextlib
import contextvars
import threading
from typing import Any, Dict, Union
class _CVar:
"""Storage utility for Local."""
def __init__(self) -> None:
self._data: "contextvars.ContextVar[Dict[str, Any]]" = contextvars.ContextVar(
"asgiref.local"
)
def __getattr__(self, key):
storage_object = self._data.get({})
try:
return storage_object[key]
except KeyError:
raise AttributeError(f"{self!r} object has no attribute {key!r}")
def __setattr__(self, key: str, value: Any) -> None:
if key == "_data":
return super().__setattr__(key, value)
storage_object = self._data.get({})
storage_object[key] = value
self._data.set(storage_object)
def __delattr__(self, key: str) -> None:
storage_object = self._data.get({})
if key in storage_object:
del storage_object[key]
self._data.set(storage_object)
else:
raise AttributeError(f"{self!r} object has no attribute {key!r}")
class Local:
"""Local storage for async tasks.
This is a namespace object (similar to `threading.local`) where data is
also local to the current async task (if there is one).
In async threads, local means in the same sense as the `contextvars`
module - i.e. a value set in an async frame will be visible:
- to other async code `await`-ed from this frame.
- to tasks spawned using `asyncio` utilities (`create_task`, `wait_for`,
`gather` and probably others).
- to code scheduled in a sync thread using `sync_to_async`
In "sync" threads (a thread with no async event loop running), the
data is thread-local, but additionally shared with async code executed
via the `async_to_sync` utility, which schedules async code in a new thread
and copies context across to that thread.
If `thread_critical` is True, then the local will only be visible per-thread,
behaving exactly like `threading.local` if the thread is sync, and as
`contextvars` if the thread is async. This allows genuinely thread-sensitive
code (such as DB handles) to be kept stricly to their initial thread and
disable the sharing across `sync_to_async` and `async_to_sync` wrapped calls.
Unlike plain `contextvars` objects, this utility is threadsafe.
"""
def __init__(self, thread_critical: bool = False) -> None:
self._thread_critical = thread_critical
self._thread_lock = threading.RLock()
self._storage: "Union[threading.local, _CVar]"
if thread_critical:
# Thread-local storage
self._storage = threading.local()
else:
# Contextvar storage
self._storage = _CVar()
@contextlib.contextmanager
def _lock_storage(self):
# Thread safe access to storage
if self._thread_critical:
try:
# this is a test for are we in a async or sync
# thread - will raise RuntimeError if there is
# no current loop
asyncio.get_running_loop()
except RuntimeError:
# We are in a sync thread, the storage is
# just the plain thread local (i.e, "global within
# this thread" - it doesn't matter where you are
# in a call stack you see the same storage)
yield self._storage
else:
# We are in an async thread - storage is still
# local to this thread, but additionally should
# behave like a context var (is only visible with
# the same async call stack)
# Ensure context exists in the current thread
if not hasattr(self._storage, "cvar"):
self._storage.cvar = _CVar()
# self._storage is a thread local, so the members
# can't be accessed in another thread (we don't
# need any locks)
yield self._storage.cvar
else:
# Lock for thread_critical=False as other threads
# can access the exact same storage object
with self._thread_lock:
yield self._storage
def __getattr__(self, key):
with self._lock_storage() as storage:
return getattr(storage, key)
def __setattr__(self, key, value):
if key in ("_local", "_storage", "_thread_critical", "_thread_lock"):
return super().__setattr__(key, value)
with self._lock_storage() as storage:
setattr(storage, key, value)
def __delattr__(self, key):
with self._lock_storage() as storage:
delattr(storage, key)

View File

View File

@ -0,0 +1,157 @@
import asyncio
import logging
import time
import traceback
from .compatibility import guarantee_single_callable
logger = logging.getLogger(__name__)
class StatelessServer:
"""
Base server class that handles basic concepts like application instance
creation/pooling, exception handling, and similar, for stateless protocols
(i.e. ones without actual incoming connections to the process)
Your code should override the handle() method, doing whatever it needs to,
and calling get_or_create_application_instance with a unique `scope_id`
and `scope` for the scope it wants to get.
If an application instance is found with the same `scope_id`, you are
given its input queue, otherwise one is made for you with the scope provided
and you are given that fresh new input queue. Either way, you should do
something like:
input_queue = self.get_or_create_application_instance(
"user-123456",
{"type": "testprotocol", "user_id": "123456", "username": "andrew"},
)
input_queue.put_nowait(message)
If you try and create an application instance and there are already
`max_application` instances, the oldest/least recently used one will be
reclaimed and shut down to make space.
Application coroutines that error will be found periodically (every 100ms
by default) and have their exceptions printed to the console. Override
application_exception() if you want to do more when this happens.
If you override run(), make sure you handle things like launching the
application checker.
"""
application_checker_interval = 0.1
def __init__(self, application, max_applications=1000):
# Parameters
self.application = application
self.max_applications = max_applications
# Initialisation
self.application_instances = {}
### Mainloop and handling
def run(self):
"""
Runs the asyncio event loop with our handler loop.
"""
event_loop = asyncio.get_event_loop()
asyncio.ensure_future(self.application_checker())
try:
event_loop.run_until_complete(self.handle())
except KeyboardInterrupt:
logger.info("Exiting due to Ctrl-C/interrupt")
async def handle(self):
raise NotImplementedError("You must implement handle()")
async def application_send(self, scope, message):
"""
Receives outbound sends from applications and handles them.
"""
raise NotImplementedError("You must implement application_send()")
### Application instance management
def get_or_create_application_instance(self, scope_id, scope):
"""
Creates an application instance and returns its queue.
"""
if scope_id in self.application_instances:
self.application_instances[scope_id]["last_used"] = time.time()
return self.application_instances[scope_id]["input_queue"]
# See if we need to delete an old one
while len(self.application_instances) > self.max_applications:
self.delete_oldest_application_instance()
# Make an instance of the application
input_queue = asyncio.Queue()
application_instance = guarantee_single_callable(self.application)
# Run it, and stash the future for later checking
future = asyncio.ensure_future(
application_instance(
scope=scope,
receive=input_queue.get,
send=lambda message: self.application_send(scope, message),
),
)
self.application_instances[scope_id] = {
"input_queue": input_queue,
"future": future,
"scope": scope,
"last_used": time.time(),
}
return input_queue
def delete_oldest_application_instance(self):
"""
Finds and deletes the oldest application instance
"""
oldest_time = min(
details["last_used"] for details in self.application_instances.values()
)
for scope_id, details in self.application_instances.items():
if details["last_used"] == oldest_time:
self.delete_application_instance(scope_id)
# Return to make sure we only delete one in case two have
# the same oldest time
return
def delete_application_instance(self, scope_id):
"""
Removes an application instance (makes sure its task is stopped,
then removes it from the current set)
"""
details = self.application_instances[scope_id]
del self.application_instances[scope_id]
if not details["future"].done():
details["future"].cancel()
async def application_checker(self):
"""
Goes through the set of current application instance Futures and cleans up
any that are done/prints exceptions for any that errored.
"""
while True:
await asyncio.sleep(self.application_checker_interval)
for scope_id, details in list(self.application_instances.items()):
if details["future"].done():
exception = details["future"].exception()
if exception:
await self.application_exception(exception, details)
try:
del self.application_instances[scope_id]
except KeyError:
# Exception handling might have already got here before us. That's fine.
pass
async def application_exception(self, exception, application_details):
"""
Called whenever an application coroutine has an exception.
"""
logging.error(
"Exception inside application: %s\n%s%s",
exception,
"".join(traceback.format_tb(exception.__traceback__)),
f" {exception}",
)

View File

@ -0,0 +1,613 @@
import asyncio
import asyncio.coroutines
import contextvars
import functools
import inspect
import os
import sys
import threading
import warnings
import weakref
from concurrent.futures import Future, ThreadPoolExecutor
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Coroutine,
Dict,
Generic,
List,
Optional,
TypeVar,
Union,
overload,
)
from .current_thread_executor import CurrentThreadExecutor
from .local import Local
if sys.version_info >= (3, 10):
from typing import ParamSpec
else:
from typing_extensions import ParamSpec
if TYPE_CHECKING:
# This is not available to import at runtime
from _typeshed import OptExcInfo
_F = TypeVar("_F", bound=Callable[..., Any])
_P = ParamSpec("_P")
_R = TypeVar("_R")
def _restore_context(context: contextvars.Context) -> None:
# Check for changes in contextvars, and set them to the current
# context for downstream consumers
for cvar in context:
cvalue = context.get(cvar)
try:
if cvar.get() != cvalue:
cvar.set(cvalue)
except LookupError:
cvar.set(cvalue)
# Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for
# inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker.
# The latter is replaced with the inspect.markcoroutinefunction decorator.
# Until 3.12 is the minimum supported Python version, provide a shim.
if hasattr(inspect, "markcoroutinefunction"):
iscoroutinefunction = inspect.iscoroutinefunction
markcoroutinefunction: Callable[[_F], _F] = inspect.markcoroutinefunction
else:
iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment]
def markcoroutinefunction(func: _F) -> _F:
func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore
return func
class ThreadSensitiveContext:
"""Async context manager to manage context for thread sensitive mode
This context manager controls which thread pool executor is used when in
thread sensitive mode. By default, a single thread pool executor is shared
within a process.
The ThreadSensitiveContext() context manager may be used to specify a
thread pool per context.
This context manager is re-entrant, so only the outer-most call to
ThreadSensitiveContext will set the context.
Usage:
>>> import time
>>> async with ThreadSensitiveContext():
... await sync_to_async(time.sleep, 1)()
"""
def __init__(self):
self.token = None
async def __aenter__(self):
try:
SyncToAsync.thread_sensitive_context.get()
except LookupError:
self.token = SyncToAsync.thread_sensitive_context.set(self)
return self
async def __aexit__(self, exc, value, tb):
if not self.token:
return
executor = SyncToAsync.context_to_thread_executor.pop(self, None)
if executor:
executor.shutdown()
SyncToAsync.thread_sensitive_context.reset(self.token)
class AsyncToSync(Generic[_P, _R]):
"""
Utility class which turns an awaitable that only works on the thread with
the event loop into a synchronous callable that works in a subthread.
If the call stack contains an async loop, the code runs there.
Otherwise, the code runs in a new loop in a new thread.
Either way, this thread then pauses and waits to run any thread_sensitive
code called from further down the call stack using SyncToAsync, before
finally exiting once the async task returns.
"""
# Keeps a reference to the CurrentThreadExecutor in local context, so that
# any sync_to_async inside the wrapped code can find it.
executors: "Local" = Local()
# When we can't find a CurrentThreadExecutor from the context, such as
# inside create_task, we'll look it up here from the running event loop.
loop_thread_executors: "Dict[asyncio.AbstractEventLoop, CurrentThreadExecutor]" = {}
def __init__(
self,
awaitable: Union[
Callable[_P, Coroutine[Any, Any, _R]],
Callable[_P, Awaitable[_R]],
],
force_new_loop: bool = False,
):
if not callable(awaitable) or (
not iscoroutinefunction(awaitable)
and not iscoroutinefunction(getattr(awaitable, "__call__", awaitable))
):
# Python does not have very reliable detection of async functions
# (lots of false negatives) so this is just a warning.
warnings.warn(
"async_to_sync was passed a non-async-marked callable", stacklevel=2
)
self.awaitable = awaitable
try:
self.__self__ = self.awaitable.__self__ # type: ignore[union-attr]
except AttributeError:
pass
self.force_new_loop = force_new_loop
self.main_event_loop = None
try:
self.main_event_loop = asyncio.get_running_loop()
except RuntimeError:
# There's no event loop in this thread.
pass
def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
__traceback_hide__ = True # noqa: F841
if not self.force_new_loop and not self.main_event_loop:
# There's no event loop in this thread. Look for the threadlocal if
# we're inside SyncToAsync
main_event_loop_pid = getattr(
SyncToAsync.threadlocal, "main_event_loop_pid", None
)
# We make sure the parent loop is from the same process - if
# they've forked, this is not going to be valid any more (#194)
if main_event_loop_pid and main_event_loop_pid == os.getpid():
self.main_event_loop = getattr(
SyncToAsync.threadlocal, "main_event_loop", None
)
# You can't call AsyncToSync from a thread with a running event loop
try:
event_loop = asyncio.get_running_loop()
except RuntimeError:
pass
else:
if event_loop.is_running():
raise RuntimeError(
"You cannot use AsyncToSync in the same thread as an async event loop - "
"just await the async function directly."
)
# Make a future for the return information
call_result: "Future[_R]" = Future()
# Make a CurrentThreadExecutor we'll use to idle in this thread - we
# need one for every sync frame, even if there's one above us in the
# same thread.
old_executor = getattr(self.executors, "current", None)
current_executor = CurrentThreadExecutor()
self.executors.current = current_executor
# Wrapping context in list so it can be reassigned from within
# `main_wrap`.
context = [contextvars.copy_context()]
# Get task context so that parent task knows which task to propagate
# an asyncio.CancelledError to.
task_context = getattr(SyncToAsync.threadlocal, "task_context", None)
loop = None
# Use call_soon_threadsafe to schedule a synchronous callback on the
# main event loop's thread if it's there, otherwise make a new loop
# in this thread.
try:
awaitable = self.main_wrap(
call_result,
sys.exc_info(),
task_context,
context,
*args,
**kwargs,
)
if not (self.main_event_loop and self.main_event_loop.is_running()):
# Make our own event loop - in a new thread - and run inside that.
loop = asyncio.new_event_loop()
self.loop_thread_executors[loop] = current_executor
loop_executor = ThreadPoolExecutor(max_workers=1)
loop_future = loop_executor.submit(
self._run_event_loop, loop, awaitable
)
if current_executor:
# Run the CurrentThreadExecutor until the future is done
current_executor.run_until_future(loop_future)
# Wait for future and/or allow for exception propagation
loop_future.result()
else:
# Call it inside the existing loop
self.main_event_loop.call_soon_threadsafe(
self.main_event_loop.create_task, awaitable
)
if current_executor:
# Run the CurrentThreadExecutor until the future is done
current_executor.run_until_future(call_result)
finally:
# Clean up any executor we were running
if loop is not None:
del self.loop_thread_executors[loop]
_restore_context(context[0])
# Restore old current thread executor state
self.executors.current = old_executor
# Wait for results from the future.
return call_result.result()
def _run_event_loop(self, loop, coro):
"""
Runs the given event loop (designed to be called in a thread).
"""
asyncio.set_event_loop(loop)
try:
loop.run_until_complete(coro)
finally:
try:
# mimic asyncio.run() behavior
# cancel unexhausted async generators
tasks = asyncio.all_tasks(loop)
for task in tasks:
task.cancel()
async def gather():
await asyncio.gather(*tasks, return_exceptions=True)
loop.run_until_complete(gather())
for task in tasks:
if task.cancelled():
continue
if task.exception() is not None:
loop.call_exception_handler(
{
"message": "unhandled exception during loop shutdown",
"exception": task.exception(),
"task": task,
}
)
if hasattr(loop, "shutdown_asyncgens"):
loop.run_until_complete(loop.shutdown_asyncgens())
finally:
loop.close()
asyncio.set_event_loop(self.main_event_loop)
def __get__(self, parent: Any, objtype: Any) -> Callable[_P, _R]:
"""
Include self for methods
"""
func = functools.partial(self.__call__, parent)
return functools.update_wrapper(func, self.awaitable)
async def main_wrap(
self,
call_result: "Future[_R]",
exc_info: "OptExcInfo",
task_context: "Optional[List[asyncio.Task[Any]]]",
context: List[contextvars.Context],
*args: _P.args,
**kwargs: _P.kwargs,
) -> None:
"""
Wraps the awaitable with something that puts the result into the
result/exception future.
"""
__traceback_hide__ = True # noqa: F841
if context is not None:
_restore_context(context[0])
current_task = asyncio.current_task()
if current_task is not None and task_context is not None:
task_context.append(current_task)
try:
# If we have an exception, run the function inside the except block
# after raising it so exc_info is correctly populated.
if exc_info[1]:
try:
raise exc_info[1]
except BaseException:
result = await self.awaitable(*args, **kwargs)
else:
result = await self.awaitable(*args, **kwargs)
except BaseException as e:
call_result.set_exception(e)
else:
call_result.set_result(result)
finally:
if current_task is not None and task_context is not None:
task_context.remove(current_task)
context[0] = contextvars.copy_context()
class SyncToAsync(Generic[_P, _R]):
"""
Utility class which turns a synchronous callable into an awaitable that
runs in a threadpool. It also sets a threadlocal inside the thread so
calls to AsyncToSync can escape it.
If thread_sensitive is passed, the code will run in the same thread as any
outer code. This is needed for underlying Python code that is not
threadsafe (for example, code which handles SQLite database connections).
If the outermost program is async (i.e. SyncToAsync is outermost), then
this will be a dedicated single sub-thread that all sync code runs in,
one after the other. If the outermost program is sync (i.e. AsyncToSync is
outermost), this will just be the main thread. This is achieved by idling
with a CurrentThreadExecutor while AsyncToSync is blocking its sync parent,
rather than just blocking.
If executor is passed in, that will be used instead of the loop's default executor.
In order to pass in an executor, thread_sensitive must be set to False, otherwise
a TypeError will be raised.
"""
# Storage for main event loop references
threadlocal = threading.local()
# Single-thread executor for thread-sensitive code
single_thread_executor = ThreadPoolExecutor(max_workers=1)
# Maintain a contextvar for the current execution context. Optionally used
# for thread sensitive mode.
thread_sensitive_context: "contextvars.ContextVar[ThreadSensitiveContext]" = (
contextvars.ContextVar("thread_sensitive_context")
)
# Contextvar that is used to detect if the single thread executor
# would be awaited on while already being used in the same context
deadlock_context: "contextvars.ContextVar[bool]" = contextvars.ContextVar(
"deadlock_context"
)
# Maintaining a weak reference to the context ensures that thread pools are
# erased once the context goes out of scope. This terminates the thread pool.
context_to_thread_executor: "weakref.WeakKeyDictionary[ThreadSensitiveContext, ThreadPoolExecutor]" = (
weakref.WeakKeyDictionary()
)
def __init__(
self,
func: Callable[_P, _R],
thread_sensitive: bool = True,
executor: Optional["ThreadPoolExecutor"] = None,
) -> None:
if (
not callable(func)
or iscoroutinefunction(func)
or iscoroutinefunction(getattr(func, "__call__", func))
):
raise TypeError("sync_to_async can only be applied to sync functions.")
self.func = func
functools.update_wrapper(self, func)
self._thread_sensitive = thread_sensitive
markcoroutinefunction(self)
if thread_sensitive and executor is not None:
raise TypeError("executor must not be set when thread_sensitive is True")
self._executor = executor
try:
self.__self__ = func.__self__ # type: ignore
except AttributeError:
pass
async def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R:
__traceback_hide__ = True # noqa: F841
loop = asyncio.get_running_loop()
# Work out what thread to run the code in
if self._thread_sensitive:
current_thread_executor = getattr(AsyncToSync.executors, "current", None)
if current_thread_executor:
# If we have a parent sync thread above somewhere, use that
executor = current_thread_executor
elif self.thread_sensitive_context.get(None):
# If we have a way of retrieving the current context, attempt
# to use a per-context thread pool executor
thread_sensitive_context = self.thread_sensitive_context.get()
if thread_sensitive_context in self.context_to_thread_executor:
# Re-use thread executor in current context
executor = self.context_to_thread_executor[thread_sensitive_context]
else:
# Create new thread executor in current context
executor = ThreadPoolExecutor(max_workers=1)
self.context_to_thread_executor[thread_sensitive_context] = executor
elif loop in AsyncToSync.loop_thread_executors:
# Re-use thread executor for running loop
executor = AsyncToSync.loop_thread_executors[loop]
elif self.deadlock_context.get(False):
raise RuntimeError(
"Single thread executor already being used, would deadlock"
)
else:
# Otherwise, we run it in a fixed single thread
executor = self.single_thread_executor
self.deadlock_context.set(True)
else:
# Use the passed in executor, or the loop's default if it is None
executor = self._executor
context = contextvars.copy_context()
child = functools.partial(self.func, *args, **kwargs)
func = context.run
task_context: List[asyncio.Task[Any]] = []
# Run the code in the right thread
exec_coro = loop.run_in_executor(
executor,
functools.partial(
self.thread_handler,
loop,
sys.exc_info(),
task_context,
func,
child,
),
)
ret: _R
try:
ret = await asyncio.shield(exec_coro)
except asyncio.CancelledError:
cancel_parent = True
try:
task = task_context[0]
task.cancel()
try:
await task
cancel_parent = False
except asyncio.CancelledError:
pass
except IndexError:
pass
if exec_coro.done():
raise
if cancel_parent:
exec_coro.cancel()
ret = await exec_coro
finally:
_restore_context(context)
self.deadlock_context.set(False)
return ret
def __get__(
self, parent: Any, objtype: Any
) -> Callable[_P, Coroutine[Any, Any, _R]]:
"""
Include self for methods
"""
func = functools.partial(self.__call__, parent)
return functools.update_wrapper(func, self.func)
def thread_handler(self, loop, exc_info, task_context, func, *args, **kwargs):
"""
Wraps the sync application with exception handling.
"""
__traceback_hide__ = True # noqa: F841
# Set the threadlocal for AsyncToSync
self.threadlocal.main_event_loop = loop
self.threadlocal.main_event_loop_pid = os.getpid()
self.threadlocal.task_context = task_context
# Run the function
# If we have an exception, run the function inside the except block
# after raising it so exc_info is correctly populated.
if exc_info[1]:
try:
raise exc_info[1]
except BaseException:
return func(*args, **kwargs)
else:
return func(*args, **kwargs)
@overload
def async_to_sync(
*,
force_new_loop: bool = False,
) -> Callable[
[Union[Callable[_P, Coroutine[Any, Any, _R]], Callable[_P, Awaitable[_R]]]],
Callable[_P, _R],
]:
...
@overload
def async_to_sync(
awaitable: Union[
Callable[_P, Coroutine[Any, Any, _R]],
Callable[_P, Awaitable[_R]],
],
*,
force_new_loop: bool = False,
) -> Callable[_P, _R]:
...
def async_to_sync(
awaitable: Optional[
Union[
Callable[_P, Coroutine[Any, Any, _R]],
Callable[_P, Awaitable[_R]],
]
] = None,
*,
force_new_loop: bool = False,
) -> Union[
Callable[
[Union[Callable[_P, Coroutine[Any, Any, _R]], Callable[_P, Awaitable[_R]]]],
Callable[_P, _R],
],
Callable[_P, _R],
]:
if awaitable is None:
return lambda f: AsyncToSync(
f,
force_new_loop=force_new_loop,
)
return AsyncToSync(
awaitable,
force_new_loop=force_new_loop,
)
@overload
def sync_to_async(
*,
thread_sensitive: bool = True,
executor: Optional["ThreadPoolExecutor"] = None,
) -> Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]]:
...
@overload
def sync_to_async(
func: Callable[_P, _R],
*,
thread_sensitive: bool = True,
executor: Optional["ThreadPoolExecutor"] = None,
) -> Callable[_P, Coroutine[Any, Any, _R]]:
...
def sync_to_async(
func: Optional[Callable[_P, _R]] = None,
*,
thread_sensitive: bool = True,
executor: Optional["ThreadPoolExecutor"] = None,
) -> Union[
Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]],
Callable[_P, Coroutine[Any, Any, _R]],
]:
if func is None:
return lambda f: SyncToAsync(
f,
thread_sensitive=thread_sensitive,
executor=executor,
)
return SyncToAsync(
func,
thread_sensitive=thread_sensitive,
executor=executor,
)

View File

@ -0,0 +1,103 @@
import asyncio
import contextvars
import time
from .compatibility import guarantee_single_callable
from .timeout import timeout as async_timeout
class ApplicationCommunicator:
"""
Runs an ASGI application in a test mode, allowing sending of
messages to it and retrieval of messages it sends.
"""
def __init__(self, application, scope):
self.application = guarantee_single_callable(application)
self.scope = scope
self.input_queue = asyncio.Queue()
self.output_queue = asyncio.Queue()
# Clear context - this ensures that context vars set in the testing scope
# are not "leaked" into the application which would normally begin with
# an empty context. In Python >= 3.11 this could also be written as:
# asyncio.create_task(..., context=contextvars.Context())
self.future = contextvars.Context().run(
asyncio.create_task,
self.application(scope, self.input_queue.get, self.output_queue.put),
)
async def wait(self, timeout=1):
"""
Waits for the application to stop itself and returns any exceptions.
"""
try:
async with async_timeout(timeout):
try:
await self.future
self.future.result()
except asyncio.CancelledError:
pass
finally:
if not self.future.done():
self.future.cancel()
try:
await self.future
except asyncio.CancelledError:
pass
def stop(self, exceptions=True):
if not self.future.done():
self.future.cancel()
elif exceptions:
# Give a chance to raise any exceptions
self.future.result()
def __del__(self):
# Clean up on deletion
try:
self.stop(exceptions=False)
except RuntimeError:
# Event loop already stopped
pass
async def send_input(self, message):
"""
Sends a single message to the application
"""
# Give it the message
await self.input_queue.put(message)
async def receive_output(self, timeout=1):
"""
Receives a single message from the application, with optional timeout.
"""
# Make sure there's not an exception to raise from the task
if self.future.done():
self.future.result()
# Wait and receive the message
try:
async with async_timeout(timeout):
return await self.output_queue.get()
except asyncio.TimeoutError as e:
# See if we have another error to raise inside
if self.future.done():
self.future.result()
else:
self.future.cancel()
try:
await self.future
except asyncio.CancelledError:
pass
raise e
async def receive_nothing(self, timeout=0.1, interval=0.01):
"""
Checks that there is no message to receive in the given time.
"""
# `interval` has precedence over `timeout`
start = time.monotonic()
while time.monotonic() - start < timeout:
if not self.output_queue.empty():
return False
await asyncio.sleep(interval)
return self.output_queue.empty()

View File

@ -0,0 +1,118 @@
# This code is originally sourced from the aio-libs project "async_timeout",
# under the Apache 2.0 license. You may see the original project at
# https://github.com/aio-libs/async-timeout
# It is vendored here to reduce chain-dependencies on this library, and
# modified slightly to remove some features we don't use.
import asyncio
import warnings
from types import TracebackType
from typing import Any # noqa
from typing import Optional, Type
class timeout:
"""timeout context manager.
Useful in cases when you want to apply timeout logic around block
of code or in cases when asyncio.wait_for is not suitable. For example:
>>> with timeout(0.001):
... async with aiohttp.get('https://github.com') as r:
... await r.text()
timeout - value in seconds or None to disable timeout logic
loop - asyncio compatible event loop
"""
def __init__(
self,
timeout: Optional[float],
*,
loop: Optional[asyncio.AbstractEventLoop] = None,
) -> None:
self._timeout = timeout
if loop is None:
loop = asyncio.get_running_loop()
else:
warnings.warn(
"""The loop argument to timeout() is deprecated.""", DeprecationWarning
)
self._loop = loop
self._task = None # type: Optional[asyncio.Task[Any]]
self._cancelled = False
self._cancel_handler = None # type: Optional[asyncio.Handle]
self._cancel_at = None # type: Optional[float]
def __enter__(self) -> "timeout":
return self._do_enter()
def __exit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> Optional[bool]:
self._do_exit(exc_type)
return None
async def __aenter__(self) -> "timeout":
return self._do_enter()
async def __aexit__(
self,
exc_type: Type[BaseException],
exc_val: BaseException,
exc_tb: TracebackType,
) -> None:
self._do_exit(exc_type)
@property
def expired(self) -> bool:
return self._cancelled
@property
def remaining(self) -> Optional[float]:
if self._cancel_at is not None:
return max(self._cancel_at - self._loop.time(), 0.0)
else:
return None
def _do_enter(self) -> "timeout":
# Support Tornado 5- without timeout
# Details: https://github.com/python/asyncio/issues/392
if self._timeout is None:
return self
self._task = asyncio.current_task(self._loop)
if self._task is None:
raise RuntimeError(
"Timeout context manager should be used " "inside a task"
)
if self._timeout <= 0:
self._loop.call_soon(self._cancel_task)
return self
self._cancel_at = self._loop.time() + self._timeout
self._cancel_handler = self._loop.call_at(self._cancel_at, self._cancel_task)
return self
def _do_exit(self, exc_type: Type[BaseException]) -> None:
if exc_type is asyncio.CancelledError and self._cancelled:
self._cancel_handler = None
self._task = None
raise asyncio.TimeoutError
if self._timeout is not None and self._cancel_handler is not None:
self._cancel_handler.cancel()
self._cancel_handler = None
self._task = None
return None
def _cancel_task(self) -> None:
if self._task is not None:
self._task.cancel()
self._cancelled = True

View File

@ -0,0 +1,278 @@
import sys
from typing import (
Any,
Awaitable,
Callable,
Dict,
Iterable,
Literal,
Optional,
Protocol,
Tuple,
Type,
TypedDict,
Union,
)
if sys.version_info >= (3, 11):
from typing import NotRequired
else:
from typing_extensions import NotRequired
__all__ = (
"ASGIVersions",
"HTTPScope",
"WebSocketScope",
"LifespanScope",
"WWWScope",
"Scope",
"HTTPRequestEvent",
"HTTPResponseStartEvent",
"HTTPResponseBodyEvent",
"HTTPResponseTrailersEvent",
"HTTPResponsePathsendEvent",
"HTTPServerPushEvent",
"HTTPDisconnectEvent",
"WebSocketConnectEvent",
"WebSocketAcceptEvent",
"WebSocketReceiveEvent",
"WebSocketSendEvent",
"WebSocketResponseStartEvent",
"WebSocketResponseBodyEvent",
"WebSocketDisconnectEvent",
"WebSocketCloseEvent",
"LifespanStartupEvent",
"LifespanShutdownEvent",
"LifespanStartupCompleteEvent",
"LifespanStartupFailedEvent",
"LifespanShutdownCompleteEvent",
"LifespanShutdownFailedEvent",
"ASGIReceiveEvent",
"ASGISendEvent",
"ASGIReceiveCallable",
"ASGISendCallable",
"ASGI2Protocol",
"ASGI2Application",
"ASGI3Application",
"ASGIApplication",
)
class ASGIVersions(TypedDict):
spec_version: str
version: Union[Literal["2.0"], Literal["3.0"]]
class HTTPScope(TypedDict):
type: Literal["http"]
asgi: ASGIVersions
http_version: str
method: str
scheme: str
path: str
raw_path: bytes
query_string: bytes
root_path: str
headers: Iterable[Tuple[bytes, bytes]]
client: Optional[Tuple[str, int]]
server: Optional[Tuple[str, Optional[int]]]
state: NotRequired[Dict[str, Any]]
extensions: Optional[Dict[str, Dict[object, object]]]
class WebSocketScope(TypedDict):
type: Literal["websocket"]
asgi: ASGIVersions
http_version: str
scheme: str
path: str
raw_path: bytes
query_string: bytes
root_path: str
headers: Iterable[Tuple[bytes, bytes]]
client: Optional[Tuple[str, int]]
server: Optional[Tuple[str, Optional[int]]]
subprotocols: Iterable[str]
state: NotRequired[Dict[str, Any]]
extensions: Optional[Dict[str, Dict[object, object]]]
class LifespanScope(TypedDict):
type: Literal["lifespan"]
asgi: ASGIVersions
state: NotRequired[Dict[str, Any]]
WWWScope = Union[HTTPScope, WebSocketScope]
Scope = Union[HTTPScope, WebSocketScope, LifespanScope]
class HTTPRequestEvent(TypedDict):
type: Literal["http.request"]
body: bytes
more_body: bool
class HTTPResponseDebugEvent(TypedDict):
type: Literal["http.response.debug"]
info: Dict[str, object]
class HTTPResponseStartEvent(TypedDict):
type: Literal["http.response.start"]
status: int
headers: Iterable[Tuple[bytes, bytes]]
trailers: bool
class HTTPResponseBodyEvent(TypedDict):
type: Literal["http.response.body"]
body: bytes
more_body: bool
class HTTPResponseTrailersEvent(TypedDict):
type: Literal["http.response.trailers"]
headers: Iterable[Tuple[bytes, bytes]]
more_trailers: bool
class HTTPResponsePathsendEvent(TypedDict):
type: Literal["http.response.pathsend"]
path: str
class HTTPServerPushEvent(TypedDict):
type: Literal["http.response.push"]
path: str
headers: Iterable[Tuple[bytes, bytes]]
class HTTPDisconnectEvent(TypedDict):
type: Literal["http.disconnect"]
class WebSocketConnectEvent(TypedDict):
type: Literal["websocket.connect"]
class WebSocketAcceptEvent(TypedDict):
type: Literal["websocket.accept"]
subprotocol: Optional[str]
headers: Iterable[Tuple[bytes, bytes]]
class WebSocketReceiveEvent(TypedDict):
type: Literal["websocket.receive"]
bytes: Optional[bytes]
text: Optional[str]
class WebSocketSendEvent(TypedDict):
type: Literal["websocket.send"]
bytes: Optional[bytes]
text: Optional[str]
class WebSocketResponseStartEvent(TypedDict):
type: Literal["websocket.http.response.start"]
status: int
headers: Iterable[Tuple[bytes, bytes]]
class WebSocketResponseBodyEvent(TypedDict):
type: Literal["websocket.http.response.body"]
body: bytes
more_body: bool
class WebSocketDisconnectEvent(TypedDict):
type: Literal["websocket.disconnect"]
code: int
class WebSocketCloseEvent(TypedDict):
type: Literal["websocket.close"]
code: int
reason: Optional[str]
class LifespanStartupEvent(TypedDict):
type: Literal["lifespan.startup"]
class LifespanShutdownEvent(TypedDict):
type: Literal["lifespan.shutdown"]
class LifespanStartupCompleteEvent(TypedDict):
type: Literal["lifespan.startup.complete"]
class LifespanStartupFailedEvent(TypedDict):
type: Literal["lifespan.startup.failed"]
message: str
class LifespanShutdownCompleteEvent(TypedDict):
type: Literal["lifespan.shutdown.complete"]
class LifespanShutdownFailedEvent(TypedDict):
type: Literal["lifespan.shutdown.failed"]
message: str
ASGIReceiveEvent = Union[
HTTPRequestEvent,
HTTPDisconnectEvent,
WebSocketConnectEvent,
WebSocketReceiveEvent,
WebSocketDisconnectEvent,
LifespanStartupEvent,
LifespanShutdownEvent,
]
ASGISendEvent = Union[
HTTPResponseStartEvent,
HTTPResponseBodyEvent,
HTTPResponseTrailersEvent,
HTTPServerPushEvent,
HTTPDisconnectEvent,
WebSocketAcceptEvent,
WebSocketSendEvent,
WebSocketResponseStartEvent,
WebSocketResponseBodyEvent,
WebSocketCloseEvent,
LifespanStartupCompleteEvent,
LifespanStartupFailedEvent,
LifespanShutdownCompleteEvent,
LifespanShutdownFailedEvent,
]
ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]]
ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]]
class ASGI2Protocol(Protocol):
def __init__(self, scope: Scope) -> None:
...
async def __call__(
self, receive: ASGIReceiveCallable, send: ASGISendCallable
) -> None:
...
ASGI2Application = Type[ASGI2Protocol]
ASGI3Application = Callable[
[
Scope,
ASGIReceiveCallable,
ASGISendCallable,
],
Awaitable[None],
]
ASGIApplication = Union[ASGI2Application, ASGI3Application]

View File

@ -0,0 +1,166 @@
from io import BytesIO
from tempfile import SpooledTemporaryFile
from asgiref.sync import AsyncToSync, sync_to_async
class WsgiToAsgi:
"""
Wraps a WSGI application to make it into an ASGI application.
"""
def __init__(self, wsgi_application):
self.wsgi_application = wsgi_application
async def __call__(self, scope, receive, send):
"""
ASGI application instantiation point.
We return a new WsgiToAsgiInstance here with the WSGI app
and the scope, ready to respond when it is __call__ed.
"""
await WsgiToAsgiInstance(self.wsgi_application)(scope, receive, send)
class WsgiToAsgiInstance:
"""
Per-socket instance of a wrapped WSGI application
"""
def __init__(self, wsgi_application):
self.wsgi_application = wsgi_application
self.response_started = False
self.response_content_length = None
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
raise ValueError("WSGI wrapper received a non-HTTP scope")
self.scope = scope
with SpooledTemporaryFile(max_size=65536) as body:
# Alright, wait for the http.request messages
while True:
message = await receive()
if message["type"] != "http.request":
raise ValueError("WSGI wrapper received a non-HTTP-request message")
body.write(message.get("body", b""))
if not message.get("more_body"):
break
body.seek(0)
# Wrap send so it can be called from the subthread
self.sync_send = AsyncToSync(send)
# Call the WSGI app
await self.run_wsgi_app(body)
def build_environ(self, scope, body):
"""
Builds a scope and request body into a WSGI environ object.
"""
script_name = scope.get("root_path", "").encode("utf8").decode("latin1")
path_info = scope["path"].encode("utf8").decode("latin1")
if path_info.startswith(script_name):
path_info = path_info[len(script_name) :]
environ = {
"REQUEST_METHOD": scope["method"],
"SCRIPT_NAME": script_name,
"PATH_INFO": path_info,
"QUERY_STRING": scope["query_string"].decode("ascii"),
"SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"],
"wsgi.version": (1, 0),
"wsgi.url_scheme": scope.get("scheme", "http"),
"wsgi.input": body,
"wsgi.errors": BytesIO(),
"wsgi.multithread": True,
"wsgi.multiprocess": True,
"wsgi.run_once": False,
}
# Get server name and port - required in WSGI, not in ASGI
if "server" in scope:
environ["SERVER_NAME"] = scope["server"][0]
environ["SERVER_PORT"] = str(scope["server"][1])
else:
environ["SERVER_NAME"] = "localhost"
environ["SERVER_PORT"] = "80"
if scope.get("client") is not None:
environ["REMOTE_ADDR"] = scope["client"][0]
# Go through headers and make them into environ entries
for name, value in self.scope.get("headers", []):
name = name.decode("latin1")
if name == "content-length":
corrected_name = "CONTENT_LENGTH"
elif name == "content-type":
corrected_name = "CONTENT_TYPE"
else:
corrected_name = "HTTP_%s" % name.upper().replace("-", "_")
# HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case
value = value.decode("latin1")
if corrected_name in environ:
value = environ[corrected_name] + "," + value
environ[corrected_name] = value
return environ
def start_response(self, status, response_headers, exc_info=None):
"""
WSGI start_response callable.
"""
# Don't allow re-calling once response has begun
if self.response_started:
raise exc_info[1].with_traceback(exc_info[2])
# Don't allow re-calling without exc_info
if hasattr(self, "response_start") and exc_info is None:
raise ValueError(
"You cannot call start_response a second time without exc_info"
)
# Extract status code
status_code, _ = status.split(" ", 1)
status_code = int(status_code)
# Extract headers
headers = [
(name.lower().encode("ascii"), value.encode("ascii"))
for name, value in response_headers
]
# Extract content-length
self.response_content_length = None
for name, value in response_headers:
if name.lower() == "content-length":
self.response_content_length = int(value)
# Build and send response start message.
self.response_start = {
"type": "http.response.start",
"status": status_code,
"headers": headers,
}
@sync_to_async
def run_wsgi_app(self, body):
"""
Called in a subthread to run the WSGI app. We encapsulate like
this so that the start_response callable is called in the same thread.
"""
# Translate the scope and incoming request body into a WSGI environ
environ = self.build_environ(self.scope, body)
# Run the WSGI app
bytes_sent = 0
for output in self.wsgi_application(environ, self.start_response):
# If this is the first response, include the response headers
if not self.response_started:
self.response_started = True
self.sync_send(self.response_start)
# If the application supplies a Content-Length header
if self.response_content_length is not None:
# The server should not transmit more bytes to the client than the header allows
bytes_allowed = self.response_content_length - bytes_sent
if len(output) > bytes_allowed:
output = output[:bytes_allowed]
self.sync_send(
{"type": "http.response.body", "body": output, "more_body": True}
)
bytes_sent += len(output)
# The server should stop iterating over the response when enough data has been sent
if bytes_sent == self.response_content_length:
break
# Close connection
if not self.response_started:
self.response_started = True
self.sync_send(self.response_start)
self.sync_send({"type": "http.response.body"})

View File

@ -0,0 +1,5 @@
The authors in alphabetical order
* Charlie Clark
* Daniel Hillier
* Elias Rabel

View File

@ -0,0 +1 @@
pip

View File

@ -0,0 +1,298 @@
et_xml is licensed under the MIT license; see the file LICENCE for details.
et_xml includes code from the Python standard library, which is licensed under
the Python license, a permissive open source license. The copyright and license
is included below for compliance with Python's terms.
This module includes corrections and new features as follows:
- Correct handling of attributes namespaces when a default namespace
has been registered.
- Records the namespaces for an Element during parsing and utilises them to
allow inspection of namespaces at specific elements in the xml tree and
during serialisation.
Misc:
- Includes the test_xml_etree with small modifications for testing the
modifications in this package.
----------------------------------------------------------------------
Copyright (c) 2001-present Python Software Foundation; All Rights Reserved
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see https://opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
Python software and documentation are licensed under the
Python Software Foundation License Version 2.
Starting with Python 3.8.6, examples, recipes, and other code in
the documentation are dual licensed under the PSF License Version 2
and the Zero-Clause BSD license.
Some software incorporated into Python is under different licenses.
The licenses are listed with code falling under that license.
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved"
are retained in Python alone or in any derivative version prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
----------------------------------------------------------------------
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

View File

@ -0,0 +1,23 @@
This software is under the MIT Licence
======================================
Copyright (c) 2010 openpyxl
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,51 @@
Metadata-Version: 2.1
Name: et_xmlfile
Version: 2.0.0
Summary: An implementation of lxml.xmlfile for the standard library
Home-page: https://foss.heptapod.net/openpyxl/et_xmlfile
Author: See AUTHORS.txt
Author-email: charlie.clark@clark-consulting.eu
License: MIT
Project-URL: Documentation, https://openpyxl.pages.heptapod.net/et_xmlfile/
Project-URL: Source, https://foss.heptapod.net/openpyxl/et_xmlfile
Project-URL: Tracker, https://foss.heptapod.net/openpyxl/et_xmfile/-/issues
Classifier: Development Status :: 5 - Production/Stable
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.8
License-File: LICENCE.python
License-File: LICENCE.rst
License-File: AUTHORS.txt
.. image:: https://foss.heptapod.net/openpyxl/et_xmlfile/badges/branch/default/coverage.svg
:target: https://coveralls.io/bitbucket/openpyxl/et_xmlfile?branch=default
:alt: coverage status
et_xmfile
=========
XML can use lots of memory, and et_xmlfile is a low memory library for creating large XML files
And, although the standard library already includes an incremental parser, `iterparse` it has no equivalent when writing XML. Once an element has been added to the tree, it is written to
the file or stream and the memory is then cleared.
This module is based upon the `xmlfile module from lxml <http://lxml.de/api.html#incremental-xml-generation>`_ with the aim of allowing code to be developed that will work with both libraries.
It was developed initially for the openpyxl project, but is now a standalone module.
The code was written by Elias Rabel as part of the `Python Düsseldorf <http://pyddf.de>`_ openpyxl sprint in September 2014.
Proper support for incremental writing was provided by Daniel Hillier in 2024
Note on performance
-------------------
The code was not developed with performance in mind, but turned out to be faster than the existing SAX-based implementation but is generally slower than lxml's xmlfile.
There is one area where an optimisation for lxml may negatively affect the performance of et_xmfile and that is when using the `.element()` method on the xmlfile context manager. It is, therefore, recommended simply to create Elements write these directly, as in the sample code.

View File

@ -0,0 +1,14 @@
et_xmlfile-2.0.0.dist-info/AUTHORS.txt,sha256=fwOAKepUY2Bd0ieNMACZo4G86ekN2oPMqyBCNGtsgQc,82
et_xmlfile-2.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
et_xmlfile-2.0.0.dist-info/LICENCE.python,sha256=TM2q68D0S4NyDsA5m7erMprc4GfdYvc8VTWi3AViirI,14688
et_xmlfile-2.0.0.dist-info/LICENCE.rst,sha256=DIS7QvXTZ-Xr-fwt3jWxYUHfXuD9wYklCFi8bFVg9p4,1131
et_xmlfile-2.0.0.dist-info/METADATA,sha256=DpfX6pCe0PvgPYi8i29YZ3zuGwe9M1PONhzSQFkVIE4,2711
et_xmlfile-2.0.0.dist-info/RECORD,,
et_xmlfile-2.0.0.dist-info/WHEEL,sha256=HiCZjzuy6Dw0hdX5R3LCFPDmFS4BWl8H-8W39XfmgX4,91
et_xmlfile-2.0.0.dist-info/top_level.txt,sha256=34-74d5NNARgTsPxCMta5o28XpBNmSN0iCZhtmx2Fk8,11
et_xmlfile/__init__.py,sha256=AQ4_2cNUEyUHlHo-Y3Gd6-8S_6eyKd55jYO4eh23UHw,228
et_xmlfile/__pycache__/__init__.cpython-312.pyc,,
et_xmlfile/__pycache__/incremental_tree.cpython-312.pyc,,
et_xmlfile/__pycache__/xmlfile.cpython-312.pyc,,
et_xmlfile/incremental_tree.py,sha256=lX4VStfzUNK0jtrVsvshPENu7E_zQirglkyRtzGDwEg,34534
et_xmlfile/xmlfile.py,sha256=6QdxBq2P0Cf35R-oyXjLl5wOItfJJ4Yy6AlIF9RX7Bg,4886

View File

@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (72.2.0)
Root-Is-Purelib: true
Tag: py3-none-any

View File

@ -0,0 +1 @@
et_xmlfile

View File

@ -0,0 +1,8 @@
from .xmlfile import xmlfile
# constants
__version__ = '2.0.0'
__author__ = 'See AUTHORS.txt'
__license__ = 'MIT'
__author_email__ = 'charlie.clark@clark-consulting.eu'
__url__ = 'https://foss.heptapod.net/openpyxl/et_xmlfile'

View File

@ -0,0 +1,917 @@
# Code modified from cPython's Lib/xml/etree/ElementTree.py
# The write() code is modified to allow specifying a particular namespace
# uri -> prefix mapping.
#
# ---------------------------------------------------------------------
# Licensed to PSF under a Contributor Agreement.
# See https://www.python.org/psf/license for licensing details.
#
# ElementTree
# Copyright (c) 1999-2008 by Fredrik Lundh. All rights reserved.
#
# fredrik@pythonware.com
# http://www.pythonware.com
# --------------------------------------------------------------------
# The ElementTree toolkit is
#
# Copyright (c) 1999-2008 by Fredrik Lundh
#
# By obtaining, using, and/or copying this software and/or its
# associated documentation, you agree that you have read, understood,
# and will comply with the following terms and conditions:
#
# Permission to use, copy, modify, and distribute this software and
# its associated documentation for any purpose and without fee is
# hereby granted, provided that the above copyright notice appears in
# all copies, and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of
# Secret Labs AB or the author not be used in advertising or publicity
# pertaining to distribution of the software without specific, written
# prior permission.
#
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
# OF THIS SOFTWARE.
# --------------------------------------------------------------------
import contextlib
import io
import xml.etree.ElementTree as ET
def current_global_nsmap():
return {
prefix: uri for uri, prefix in ET._namespace_map.items()
}
class IncrementalTree(ET.ElementTree):
def write(
self,
file_or_filename,
encoding=None,
xml_declaration=None,
default_namespace=None,
method=None,
*,
short_empty_elements=True,
nsmap=None,
root_ns_only=False,
minimal_ns_only=False,
):
"""Write element tree to a file as XML.
Arguments:
*file_or_filename* -- file name or a file object opened for writing
*encoding* -- the output encoding (default: US-ASCII)
*xml_declaration* -- bool indicating if an XML declaration should be
added to the output. If None, an XML declaration
is added if encoding IS NOT either of:
US-ASCII, UTF-8, or Unicode
*default_namespace* -- sets the default XML namespace (for "xmlns").
Takes precedence over any default namespace
provided in nsmap or
xml.etree.ElementTree.register_namespace().
*method* -- either "xml" (default), "html, "text", or "c14n"
*short_empty_elements* -- controls the formatting of elements
that contain no content. If True (default)
they are emitted as a single self-closed
tag, otherwise they are emitted as a pair
of start/end tags
*nsmap* -- a mapping of namespace prefixes to URIs. These take
precedence over any mappings registered using
xml.etree.ElementTree.register_namespace(). The
default_namespace argument, if supplied, takes precedence
over any default namespace supplied in nsmap. All supplied
namespaces will be declared on the root element, even if
unused in the document.
*root_ns_only* -- bool indicating namespace declrations should only
be written on the root element. This requires two
passes of the xml tree adding additional time to
the writing process. This is primarily meant to
mimic xml.etree.ElementTree's behaviour.
*minimal_ns_only* -- bool indicating only namespaces that were used
to qualify elements or attributes should be
declared. All namespace declarations will be
written on the root element regardless of the
value of the root_ns_only arg. Requires two
passes of the xml tree adding additional time to
the writing process.
"""
if not method:
method = "xml"
elif method not in ("text", "xml", "html"):
raise ValueError("unknown method %r" % method)
if not encoding:
encoding = "us-ascii"
with _get_writer(file_or_filename, encoding) as (write, declared_encoding):
if method == "xml" and (
xml_declaration
or (
xml_declaration is None
and encoding.lower() != "unicode"
and declared_encoding.lower() not in ("utf-8", "us-ascii")
)
):
write("<?xml version='1.0' encoding='%s'?>\n" % (declared_encoding,))
if method == "text":
ET._serialize_text(write, self._root)
else:
if method == "xml":
is_html = False
else:
is_html = True
if nsmap:
if None in nsmap:
raise ValueError(
'Found None as default nsmap prefix in nsmap. '
'Use "" as the default namespace prefix.'
)
new_nsmap = nsmap.copy()
else:
new_nsmap = {}
if default_namespace:
new_nsmap[""] = default_namespace
if root_ns_only or minimal_ns_only:
# _namespaces returns a mapping of only the namespaces that
# were used.
new_nsmap = _namespaces(
self._root,
default_namespace,
new_nsmap,
)
if not minimal_ns_only:
if nsmap:
# We want all namespaces defined in the provided
# nsmap to be declared regardless of whether
# they've been used.
new_nsmap.update(nsmap)
if default_namespace:
new_nsmap[""] = default_namespace
global_nsmap = {
prefix: uri for uri, prefix in ET._namespace_map.items()
}
if None in global_nsmap:
raise ValueError(
'Found None as default nsmap prefix in nsmap registered with '
'register_namespace. Use "" for the default namespace prefix.'
)
nsmap_scope = {}
_serialize_ns_xml(
write,
self._root,
nsmap_scope,
global_nsmap,
is_html=is_html,
is_root=True,
short_empty_elements=short_empty_elements,
new_nsmap=new_nsmap,
)
def _make_new_ns_prefix(
nsmap_scope,
global_prefixes,
local_nsmap=None,
default_namespace=None,
):
i = len(nsmap_scope)
if default_namespace is not None and "" not in nsmap_scope:
# Keep the same numbering scheme as python which assumes the default
# namespace is present if supplied.
i += 1
while True:
prefix = f"ns{i}"
if (
prefix not in nsmap_scope
and prefix not in global_prefixes
and (
not local_nsmap or prefix not in local_nsmap
)
):
return prefix
i += 1
def _get_or_create_prefix(
uri,
nsmap_scope,
global_nsmap,
new_namespace_prefixes,
uri_to_prefix,
for_default_namespace_attr_prefix=False,
):
"""Find a prefix that doesn't conflict with the ns scope or create a new prefix
This function mutates nsmap_scope, global_nsmap, new_namespace_prefixes and
uri_to_prefix. It is intended to keep state in _serialize_ns_xml consistent
while deduplicating the house keeping code or updating these dictionaries.
"""
# Check if we can reuse an existing (global) prefix within the current
# namespace scope. There maybe many prefixes pointing to a single URI by
# this point and we need to select a prefix that is not in use in the
# current scope.
for global_prefix, global_uri in global_nsmap.items():
if uri == global_uri and global_prefix not in nsmap_scope:
prefix = global_prefix
break
else: # no break
# We couldn't find a suitable existing prefix for this namespace scope,
# let's create a new one.
prefix = _make_new_ns_prefix(nsmap_scope, global_prefixes=global_nsmap)
global_nsmap[prefix] = uri
nsmap_scope[prefix] = uri
if not for_default_namespace_attr_prefix:
# Don't override the actual default namespace prefix
uri_to_prefix[uri] = prefix
if prefix != "xml":
new_namespace_prefixes.add(prefix)
return prefix
def _find_default_namespace_attr_prefix(
default_namespace,
nsmap,
local_nsmap,
global_prefixes,
provided_default_namespace=None,
):
# Search the provided nsmap for any prefixes for this uri that aren't the
# default namespace ""
for prefix, uri in nsmap.items():
if uri == default_namespace and prefix != "":
return prefix
for prefix, uri in local_nsmap.items():
if uri == default_namespace and prefix != "":
return prefix
# _namespace_map is a 1:1 mapping of uri -> prefix
prefix = ET._namespace_map.get(default_namespace)
if prefix and prefix not in nsmap:
return prefix
return _make_new_ns_prefix(
nsmap,
global_prefixes,
local_nsmap,
provided_default_namespace,
)
def process_attribs(
elem,
is_nsmap_scope_changed,
default_ns_attr_prefix,
nsmap_scope,
global_nsmap,
new_namespace_prefixes,
uri_to_prefix,
):
item_parts = []
for k, v in elem.items():
if isinstance(k, ET.QName):
k = k.text
try:
if k[:1] == "{":
uri_and_name = k[1:].rsplit("}", 1)
try:
prefix = uri_to_prefix[uri_and_name[0]]
except KeyError:
if not is_nsmap_scope_changed:
# We're about to mutate the these dicts so
# let's copy them first. We don't have to
# recompute other mappings as we're looking up
# or creating a new prefix
nsmap_scope = nsmap_scope.copy()
uri_to_prefix = uri_to_prefix.copy()
is_nsmap_scope_changed = True
prefix = _get_or_create_prefix(
uri_and_name[0],
nsmap_scope,
global_nsmap,
new_namespace_prefixes,
uri_to_prefix,
)
if not prefix:
if default_ns_attr_prefix:
prefix = default_ns_attr_prefix
else:
for prefix, known_uri in nsmap_scope.items():
if known_uri == uri_and_name[0] and prefix != "":
default_ns_attr_prefix = prefix
break
else: # no break
if not is_nsmap_scope_changed:
# We're about to mutate the these dicts so
# let's copy them first. We don't have to
# recompute other mappings as we're looking up
# or creating a new prefix
nsmap_scope = nsmap_scope.copy()
uri_to_prefix = uri_to_prefix.copy()
is_nsmap_scope_changed = True
prefix = _get_or_create_prefix(
uri_and_name[0],
nsmap_scope,
global_nsmap,
new_namespace_prefixes,
uri_to_prefix,
for_default_namespace_attr_prefix=True,
)
default_ns_attr_prefix = prefix
k = f"{prefix}:{uri_and_name[1]}"
except TypeError:
ET._raise_serialization_error(k)
if isinstance(v, ET.QName):
if v.text[:1] != "{":
v = v.text
else:
uri_and_name = v.text[1:].rsplit("}", 1)
try:
prefix = uri_to_prefix[uri_and_name[0]]
except KeyError:
if not is_nsmap_scope_changed:
# We're about to mutate the these dicts so
# let's copy them first. We don't have to
# recompute other mappings as we're looking up
# or creating a new prefix
nsmap_scope = nsmap_scope.copy()
uri_to_prefix = uri_to_prefix.copy()
is_nsmap_scope_changed = True
prefix = _get_or_create_prefix(
uri_and_name[0],
nsmap_scope,
global_nsmap,
new_namespace_prefixes,
uri_to_prefix,
)
v = f"{prefix}:{uri_and_name[1]}"
item_parts.append((k, v))
return item_parts, default_ns_attr_prefix, nsmap_scope
def write_elem_start(
write,
elem,
nsmap_scope,
global_nsmap,
short_empty_elements,
is_html,
is_root=False,
uri_to_prefix=None,
default_ns_attr_prefix=None,
new_nsmap=None,
**kwargs,
):
"""Write the opening tag (including self closing) and element text.
Refer to _serialize_ns_xml for description of arguments.
nsmap_scope should be an empty dictionary on first call. All nsmap prefixes
must be strings with the default namespace prefix represented by "".
eg.
- <foo attr1="one"> (returns tag = 'foo')
- <foo attr1="one">text (returns tag = 'foo')
- <foo attr1="one" /> (returns tag = None)
Returns:
tag:
The tag name to be closed or None if no closing required.
nsmap_scope:
The current nsmap after any prefix to uri additions from this
element. This is the input dict if unmodified or an updated copy.
default_ns_attr_prefix:
The prefix for the default namespace to use with attrs.
uri_to_prefix:
The current uri to prefix map after any uri to prefix additions
from this element. This is the input dict if unmodified or an
updated copy.
next_remains_root:
A bool indicating if the child element(s) should be treated as
their own roots.
"""
tag = elem.tag
text = elem.text
if tag is ET.Comment:
write("<!--%s-->" % text)
tag = None
next_remains_root = False
elif tag is ET.ProcessingInstruction:
write("<?%s?>" % text)
tag = None
next_remains_root = False
else:
if new_nsmap:
is_nsmap_scope_changed = True
nsmap_scope = nsmap_scope.copy()
nsmap_scope.update(new_nsmap)
new_namespace_prefixes = set(new_nsmap.keys())
new_namespace_prefixes.discard("xml")
# We need to recompute the uri to prefixes
uri_to_prefix = None
default_ns_attr_prefix = None
else:
is_nsmap_scope_changed = False
new_namespace_prefixes = set()
if uri_to_prefix is None:
if None in nsmap_scope:
raise ValueError(
'Found None as a namespace prefix. Use "" as the default namespace prefix.'
)
uri_to_prefix = {uri: prefix for prefix, uri in nsmap_scope.items()}
if "" in nsmap_scope:
# There may be multiple prefixes for the default namespace but
# we want to make sure we preferentially use "" (for elements)
uri_to_prefix[nsmap_scope[""]] = ""
if tag is None:
# tag supression where tag is set to None
# Don't change is_root so namespaces can be passed down
next_remains_root = is_root
if text:
write(ET._escape_cdata(text))
else:
next_remains_root = False
if isinstance(tag, ET.QName):
tag = tag.text
try:
# These splits / fully qualified tag creationg are the
# bottleneck in this implementation vs the python
# implementation.
# The following split takes ~42ns with no uri and ~85ns if a
# prefix is present. If the uri was present, we then need to
# look up a prefix (~14ns) and create the fully qualified
# string (~41ns). This gives a total of ~140ns where a uri is
# present.
# Python's implementation needs to preprocess the tree to
# create a dict of qname -> tag by traversing the tree which
# takes a bit of extra time but it quickly makes that back by
# only having to do a dictionary look up (~14ns) for each tag /
# attrname vs our splitting (~140ns).
# So here we have the flexibility of being able to redefine the
# uri a prefix points to midway through serialisation at the
# expense of performance (~10% slower for a 1mb file on my
# machine).
if tag[:1] == "{":
uri_and_name = tag[1:].rsplit("}", 1)
try:
prefix = uri_to_prefix[uri_and_name[0]]
except KeyError:
if not is_nsmap_scope_changed:
# We're about to mutate the these dicts so let's
# copy them first. We don't have to recompute other
# mappings as we're looking up or creating a new
# prefix
nsmap_scope = nsmap_scope.copy()
uri_to_prefix = uri_to_prefix.copy()
is_nsmap_scope_changed = True
prefix = _get_or_create_prefix(
uri_and_name[0],
nsmap_scope,
global_nsmap,
new_namespace_prefixes,
uri_to_prefix,
)
if prefix:
tag = f"{prefix}:{uri_and_name[1]}"
else:
tag = uri_and_name[1]
elif "" in nsmap_scope:
raise ValueError(
"cannot use non-qualified names with default_namespace option"
)
except TypeError:
ET._raise_serialization_error(tag)
write("<" + tag)
if elem.attrib:
item_parts, default_ns_attr_prefix, nsmap_scope = process_attribs(
elem,
is_nsmap_scope_changed,
default_ns_attr_prefix,
nsmap_scope,
global_nsmap,
new_namespace_prefixes,
uri_to_prefix,
)
else:
item_parts = []
if new_namespace_prefixes:
ns_attrs = []
for k in sorted(new_namespace_prefixes):
v = nsmap_scope[k]
if k:
k = "xmlns:" + k
else:
k = "xmlns"
ns_attrs.append((k, v))
if is_html:
write("".join([f' {k}="{ET._escape_attrib_html(v)}"' for k, v in ns_attrs]))
else:
write("".join([f' {k}="{ET._escape_attrib(v)}"' for k, v in ns_attrs]))
if item_parts:
if is_html:
write("".join([f' {k}="{ET._escape_attrib_html(v)}"' for k, v in item_parts]))
else:
write("".join([f' {k}="{ET._escape_attrib(v)}"' for k, v in item_parts]))
if is_html:
write(">")
ltag = tag.lower()
if text:
if ltag == "script" or ltag == "style":
write(text)
else:
write(ET._escape_cdata(text))
if ltag in ET.HTML_EMPTY:
tag = None
elif text or len(elem) or not short_empty_elements:
write(">")
if text:
write(ET._escape_cdata(text))
else:
tag = None
write(" />")
return (
tag,
nsmap_scope,
default_ns_attr_prefix,
uri_to_prefix,
next_remains_root,
)
def _serialize_ns_xml(
write,
elem,
nsmap_scope,
global_nsmap,
short_empty_elements,
is_html,
is_root=False,
uri_to_prefix=None,
default_ns_attr_prefix=None,
new_nsmap=None,
**kwargs,
):
"""Serialize an element or tree using 'write' for output.
Args:
write:
A function to write the xml to its destination.
elem:
The element to serialize.
nsmap_scope:
The current prefix to uri mapping for this element. This should be
an empty dictionary for the root element. Additional namespaces are
progressively added using the new_nsmap arg.
global_nsmap:
A dict copy of the globally registered _namespace_map in uri to
prefix form
short_empty_elements:
Controls the formatting of elements that contain no content. If True
(default) they are emitted as a single self-closed tag, otherwise
they are emitted as a pair of start/end tags.
is_html:
Set to True to serialize as HTML otherwise XML.
is_root:
Boolean indicating if this is a root element.
uri_to_prefix:
Current state of the mapping of uri to prefix.
default_ns_attr_prefix:
new_nsmap:
New prefix -> uri mapping to be applied to this element.
"""
(
tag,
nsmap_scope,
default_ns_attr_prefix,
uri_to_prefix,
next_remains_root,
) = write_elem_start(
write,
elem,
nsmap_scope,
global_nsmap,
short_empty_elements,
is_html,
is_root,
uri_to_prefix,
default_ns_attr_prefix,
new_nsmap=new_nsmap,
)
for e in elem:
_serialize_ns_xml(
write,
e,
nsmap_scope,
global_nsmap,
short_empty_elements,
is_html,
next_remains_root,
uri_to_prefix,
default_ns_attr_prefix,
new_nsmap=None,
)
if tag:
write(f"</{tag}>")
if elem.tail:
write(ET._escape_cdata(elem.tail))
def _qnames_iter(elem):
"""Iterate through all the qualified names in elem"""
seen_el_qnames = set()
seen_other_qnames = set()
for this_elem in elem.iter():
tag = this_elem.tag
if isinstance(tag, str):
if tag not in seen_el_qnames:
seen_el_qnames.add(tag)
yield tag, True
elif isinstance(tag, ET.QName):
tag = tag.text
if tag not in seen_el_qnames:
seen_el_qnames.add(tag)
yield tag, True
elif (
tag is not None
and tag is not ET.ProcessingInstruction
and tag is not ET.Comment
):
ET._raise_serialization_error(tag)
for key, value in this_elem.items():
if isinstance(key, ET.QName):
key = key.text
if key not in seen_other_qnames:
seen_other_qnames.add(key)
yield key, False
if isinstance(value, ET.QName):
if value.text not in seen_other_qnames:
seen_other_qnames.add(value.text)
yield value.text, False
text = this_elem.text
if isinstance(text, ET.QName):
if text.text not in seen_other_qnames:
seen_other_qnames.add(text.text)
yield text.text, False
def _namespaces(
elem,
default_namespace=None,
nsmap=None,
):
"""Find all namespaces used in the document and return a prefix to uri map"""
if nsmap is None:
nsmap = {}
out_nsmap = {}
seen_uri_to_prefix = {}
# Multiple prefixes may be present for a single uri. This will select the
# last prefix found in nsmap for a given uri.
local_prefix_map = {uri: prefix for prefix, uri in nsmap.items()}
if default_namespace is not None:
local_prefix_map[default_namespace] = ""
elif "" in nsmap:
# but we make sure the default prefix always take precedence
local_prefix_map[nsmap[""]] = ""
global_prefixes = set(ET._namespace_map.values())
has_unqual_el = False
default_namespace_attr_prefix = None
for qname, is_el in _qnames_iter(elem):
try:
if qname[:1] == "{":
uri_and_name = qname[1:].rsplit("}", 1)
prefix = seen_uri_to_prefix.get(uri_and_name[0])
if prefix is None:
prefix = local_prefix_map.get(uri_and_name[0])
if prefix is None or prefix in out_nsmap:
prefix = ET._namespace_map.get(uri_and_name[0])
if prefix is None or prefix in out_nsmap:
prefix = _make_new_ns_prefix(
out_nsmap,
global_prefixes,
nsmap,
default_namespace,
)
if prefix or is_el:
out_nsmap[prefix] = uri_and_name[0]
seen_uri_to_prefix[uri_and_name[0]] = prefix
if not is_el and not prefix and not default_namespace_attr_prefix:
# Find the alternative prefix to use with non-element
# names
default_namespace_attr_prefix = _find_default_namespace_attr_prefix(
uri_and_name[0],
out_nsmap,
nsmap,
global_prefixes,
default_namespace,
)
out_nsmap[default_namespace_attr_prefix] = uri_and_name[0]
# Don't add this uri to prefix mapping as it might override
# the uri -> "" default mapping. We'll fix this up at the
# end of the fn.
# local_prefix_map[uri_and_name[0]] = default_namespace_attr_prefix
else:
if is_el:
has_unqual_el = True
except TypeError:
ET._raise_serialization_error(qname)
if "" in out_nsmap and has_unqual_el:
# FIXME: can this be handled in XML 1.0?
raise ValueError(
"cannot use non-qualified names with default_namespace option"
)
# The xml prefix doesn't need to be declared but may have been used to
# prefix names. Let's remove it if it has been used
out_nsmap.pop("xml", None)
return out_nsmap
def tostring(
element,
encoding=None,
method=None,
*,
xml_declaration=None,
default_namespace=None,
short_empty_elements=True,
nsmap=None,
root_ns_only=False,
minimal_ns_only=False,
tree_cls=IncrementalTree,
):
"""Generate string representation of XML element.
All subelements are included. If encoding is "unicode", a string
is returned. Otherwise a bytestring is returned.
*element* is an Element instance, *encoding* is an optional output
encoding defaulting to US-ASCII, *method* is an optional output which can
be one of "xml" (default), "html", "text" or "c14n", *default_namespace*
sets the default XML namespace (for "xmlns").
Returns an (optionally) encoded string containing the XML data.
"""
stream = io.StringIO() if encoding == "unicode" else io.BytesIO()
tree_cls(element).write(
stream,
encoding,
xml_declaration=xml_declaration,
default_namespace=default_namespace,
method=method,
short_empty_elements=short_empty_elements,
nsmap=nsmap,
root_ns_only=root_ns_only,
minimal_ns_only=minimal_ns_only,
)
return stream.getvalue()
def tostringlist(
element,
encoding=None,
method=None,
*,
xml_declaration=None,
default_namespace=None,
short_empty_elements=True,
nsmap=None,
root_ns_only=False,
minimal_ns_only=False,
tree_cls=IncrementalTree,
):
lst = []
stream = ET._ListDataStream(lst)
tree_cls(element).write(
stream,
encoding,
xml_declaration=xml_declaration,
default_namespace=default_namespace,
method=method,
short_empty_elements=short_empty_elements,
nsmap=nsmap,
root_ns_only=root_ns_only,
minimal_ns_only=minimal_ns_only,
)
return lst
def compat_tostring(
element,
encoding=None,
method=None,
*,
xml_declaration=None,
default_namespace=None,
short_empty_elements=True,
nsmap=None,
root_ns_only=True,
minimal_ns_only=False,
tree_cls=IncrementalTree,
):
"""tostring with options that produce the same results as xml.etree.ElementTree.tostring
root_ns_only=True is a bit slower than False as it needs to traverse the
tree one more time to collect all the namespaces.
"""
return tostring(
element,
encoding=encoding,
method=method,
xml_declaration=xml_declaration,
default_namespace=default_namespace,
short_empty_elements=short_empty_elements,
nsmap=nsmap,
root_ns_only=root_ns_only,
minimal_ns_only=minimal_ns_only,
tree_cls=tree_cls,
)
# --------------------------------------------------------------------
# serialization support
@contextlib.contextmanager
def _get_writer(file_or_filename, encoding):
# Copied from Python 3.12
# returns text write method and release all resources after using
try:
write = file_or_filename.write
except AttributeError:
# file_or_filename is a file name
if encoding.lower() == "unicode":
encoding = "utf-8"
with open(file_or_filename, "w", encoding=encoding,
errors="xmlcharrefreplace") as file:
yield file.write, encoding
else:
# file_or_filename is a file-like object
# encoding determines if it is a text or binary writer
if encoding.lower() == "unicode":
# use a text writer as is
yield write, getattr(file_or_filename, "encoding", None) or "utf-8"
else:
# wrap a binary writer with TextIOWrapper
with contextlib.ExitStack() as stack:
if isinstance(file_or_filename, io.BufferedIOBase):
file = file_or_filename
elif isinstance(file_or_filename, io.RawIOBase):
file = io.BufferedWriter(file_or_filename)
# Keep the original file open when the BufferedWriter is
# destroyed
stack.callback(file.detach)
else:
# This is to handle passed objects that aren't in the
# IOBase hierarchy, but just have a write method
file = io.BufferedIOBase()
file.writable = lambda: True
file.write = write
try:
# TextIOWrapper uses this methods to determine
# if BOM (for UTF-16, etc) should be added
file.seekable = file_or_filename.seekable
file.tell = file_or_filename.tell
except AttributeError:
pass
file = io.TextIOWrapper(file,
encoding=encoding,
errors="xmlcharrefreplace",
newline="\n")
# Keep the original file open when the TextIOWrapper is
# destroyed
stack.callback(file.detach)
yield file.write, encoding

View File

@ -0,0 +1,158 @@
from __future__ import absolute_import
# Copyright (c) 2010-2015 openpyxl
"""Implements the lxml.etree.xmlfile API using the standard library xml.etree"""
from contextlib import contextmanager
from xml.etree.ElementTree import (
Element,
_escape_cdata,
)
from . import incremental_tree
class LxmlSyntaxError(Exception):
pass
class _IncrementalFileWriter(object):
"""Replacement for _IncrementalFileWriter of lxml"""
def __init__(self, output_file):
self._element_stack = []
self._file = output_file
self._have_root = False
self.global_nsmap = incremental_tree.current_global_nsmap()
self.is_html = False
@contextmanager
def element(self, tag, attrib=None, nsmap=None, **_extra):
"""Create a new xml element using a context manager."""
if nsmap and None in nsmap:
# Normalise None prefix (lxml's default namespace prefix) -> "", as
# required for incremental_tree
if "" in nsmap and nsmap[""] != nsmap[None]:
raise ValueError(
'Found None and "" as default nsmap prefixes with different URIs'
)
nsmap = nsmap.copy()
nsmap[""] = nsmap.pop(None)
# __enter__ part
self._have_root = True
if attrib is None:
attrib = {}
elem = Element(tag, attrib=attrib, **_extra)
elem.text = ''
elem.tail = ''
if self._element_stack:
is_root = False
(
nsmap_scope,
default_ns_attr_prefix,
uri_to_prefix,
) = self._element_stack[-1]
else:
is_root = True
nsmap_scope = {}
default_ns_attr_prefix = None
uri_to_prefix = {}
(
tag,
nsmap_scope,
default_ns_attr_prefix,
uri_to_prefix,
next_remains_root,
) = incremental_tree.write_elem_start(
self._file,
elem,
nsmap_scope=nsmap_scope,
global_nsmap=self.global_nsmap,
short_empty_elements=False,
is_html=self.is_html,
is_root=is_root,
uri_to_prefix=uri_to_prefix,
default_ns_attr_prefix=default_ns_attr_prefix,
new_nsmap=nsmap,
)
self._element_stack.append(
(
nsmap_scope,
default_ns_attr_prefix,
uri_to_prefix,
)
)
yield
# __exit__ part
self._element_stack.pop()
self._file(f"</{tag}>")
if elem.tail:
self._file(_escape_cdata(elem.tail))
def write(self, arg):
"""Write a string or subelement."""
if isinstance(arg, str):
# it is not allowed to write a string outside of an element
if not self._element_stack:
raise LxmlSyntaxError()
self._file(_escape_cdata(arg))
else:
if not self._element_stack and self._have_root:
raise LxmlSyntaxError()
if self._element_stack:
is_root = False
(
nsmap_scope,
default_ns_attr_prefix,
uri_to_prefix,
) = self._element_stack[-1]
else:
is_root = True
nsmap_scope = {}
default_ns_attr_prefix = None
uri_to_prefix = {}
incremental_tree._serialize_ns_xml(
self._file,
arg,
nsmap_scope=nsmap_scope,
global_nsmap=self.global_nsmap,
short_empty_elements=True,
is_html=self.is_html,
is_root=is_root,
uri_to_prefix=uri_to_prefix,
default_ns_attr_prefix=default_ns_attr_prefix,
)
def __enter__(self):
pass
def __exit__(self, type, value, traceback):
# without root the xml document is incomplete
if not self._have_root:
raise LxmlSyntaxError()
class xmlfile(object):
"""Context manager that can replace lxml.etree.xmlfile."""
def __init__(self, output_file, buffered=False, encoding="utf-8", close=False):
self._file = output_file
self._close = close
self.encoding = encoding
self.writer_cm = None
def __enter__(self):
self.writer_cm = incremental_tree._get_writer(self._file, encoding=self.encoding)
writer, declared_encoding = self.writer_cm.__enter__()
return _IncrementalFileWriter(writer)
def __exit__(self, type, value, traceback):
if self.writer_cm:
self.writer_cm.__exit__(type, value, traceback)
if self._close:
self._file.close()

View File

@ -0,0 +1 @@
pip

View File

@ -0,0 +1,23 @@
This software is under the MIT Licence
======================================
Copyright (c) 2010 openpyxl
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,86 @@
Metadata-Version: 2.1
Name: openpyxl
Version: 3.1.5
Summary: A Python library to read/write Excel 2010 xlsx/xlsm files
Home-page: https://openpyxl.readthedocs.io
Author: See AUTHORS
Author-email: charlie.clark@clark-consulting.eu
License: MIT
Project-URL: Documentation, https://openpyxl.readthedocs.io/en/stable/
Project-URL: Source, https://foss.heptapod.net/openpyxl/openpyxl
Project-URL: Tracker, https://foss.heptapod.net/openpyxl/openpyxl/-/issues
Classifier: Development Status :: 5 - Production/Stable
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.8
License-File: LICENCE.rst
Requires-Dist: et-xmlfile
.. image:: https://coveralls.io/repos/bitbucket/openpyxl/openpyxl/badge.svg?branch=default
:target: https://coveralls.io/bitbucket/openpyxl/openpyxl?branch=default
:alt: coverage status
Introduction
------------
openpyxl is a Python library to read/write Excel 2010 xlsx/xlsm/xltx/xltm files.
It was born from lack of existing library to read/write natively from Python
the Office Open XML format.
All kudos to the PHPExcel team as openpyxl was initially based on PHPExcel.
Security
--------
By default openpyxl does not guard against quadratic blowup or billion laughs
xml attacks. To guard against these attacks install defusedxml.
Mailing List
------------
The user list can be found on http://groups.google.com/group/openpyxl-users
Sample code::
from openpyxl import Workbook
wb = Workbook()
# grab the active worksheet
ws = wb.active
# Data can be assigned directly to cells
ws['A1'] = 42
# Rows can also be appended
ws.append([1, 2, 3])
# Python types will automatically be converted
import datetime
ws['A2'] = datetime.datetime.now()
# Save the file
wb.save("sample.xlsx")
Documentation
-------------
The documentation is at: https://openpyxl.readthedocs.io
* installation methods
* code examples
* instructions for contributing
Release notes: https://openpyxl.readthedocs.io/en/stable/changes.html

View File

@ -0,0 +1,387 @@
openpyxl-3.1.5.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
openpyxl-3.1.5.dist-info/LICENCE.rst,sha256=DIS7QvXTZ-Xr-fwt3jWxYUHfXuD9wYklCFi8bFVg9p4,1131
openpyxl-3.1.5.dist-info/METADATA,sha256=I_gMqYMN2JQ12hcQ8m3tqPgeVAkofnRUAhDHJiekrZY,2510
openpyxl-3.1.5.dist-info/RECORD,,
openpyxl-3.1.5.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
openpyxl-3.1.5.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
openpyxl-3.1.5.dist-info/top_level.txt,sha256=mKJO5QFAsUEDtJ_c97F-IbmVtHYEDymqD7d5X0ULkVs,9
openpyxl/__init__.py,sha256=s2sXcp8ThXXHswNSh-UuQi5BHsoasuczUyjNNz0Vupc,603
openpyxl/__pycache__/__init__.cpython-312.pyc,,
openpyxl/__pycache__/_constants.cpython-312.pyc,,
openpyxl/_constants.py,sha256=rhOeQ6wNH6jw73G4I242VtbmyM8fvdNVwOsOjJlJ6TU,306
openpyxl/cell/__init__.py,sha256=OXNzFFR9dlxUXiuWXyKSVQRJiQhZFel-_RQS3mHNnrQ,122
openpyxl/cell/__pycache__/__init__.cpython-312.pyc,,
openpyxl/cell/__pycache__/_writer.cpython-312.pyc,,
openpyxl/cell/__pycache__/cell.cpython-312.pyc,,
openpyxl/cell/__pycache__/read_only.cpython-312.pyc,,
openpyxl/cell/__pycache__/rich_text.cpython-312.pyc,,
openpyxl/cell/__pycache__/text.cpython-312.pyc,,
openpyxl/cell/_writer.py,sha256=3I6WLKEJGuFe8rOjxdAVuDT4sZYjcYo57-6velGepdQ,4015
openpyxl/cell/cell.py,sha256=hVJsMC9kJAxxb_CspJlBrwDt2qzfccO6YDfPHK3BBCQ,8922
openpyxl/cell/read_only.py,sha256=ApXkofmUK5QISsuTgZvmZKsU8PufSQtqe2xmYWTgLnc,3097
openpyxl/cell/rich_text.py,sha256=uAZmGB7bYDUnanHI0vJmKbfSF8riuIYS5CwlVU_3_fM,5628
openpyxl/cell/text.py,sha256=acU6BZQNSmVx4bBXPgFavoxmfoPbVYrm_ztp1bGeOmc,4367
openpyxl/chart/_3d.py,sha256=Sdm0TNpXHXNoOLUwiOSccv7yFwrel_-rjQhkrDqAAF4,3104
openpyxl/chart/__init__.py,sha256=ag4YCN1B3JH0lkS7tiiZCohVAA51x_pejGdAMuxaI1Y,564
openpyxl/chart/__pycache__/_3d.cpython-312.pyc,,
openpyxl/chart/__pycache__/__init__.cpython-312.pyc,,
openpyxl/chart/__pycache__/_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/area_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/axis.cpython-312.pyc,,
openpyxl/chart/__pycache__/bar_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/bubble_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/chartspace.cpython-312.pyc,,
openpyxl/chart/__pycache__/data_source.cpython-312.pyc,,
openpyxl/chart/__pycache__/descriptors.cpython-312.pyc,,
openpyxl/chart/__pycache__/error_bar.cpython-312.pyc,,
openpyxl/chart/__pycache__/label.cpython-312.pyc,,
openpyxl/chart/__pycache__/layout.cpython-312.pyc,,
openpyxl/chart/__pycache__/legend.cpython-312.pyc,,
openpyxl/chart/__pycache__/line_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/marker.cpython-312.pyc,,
openpyxl/chart/__pycache__/picture.cpython-312.pyc,,
openpyxl/chart/__pycache__/pie_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/pivot.cpython-312.pyc,,
openpyxl/chart/__pycache__/plotarea.cpython-312.pyc,,
openpyxl/chart/__pycache__/print_settings.cpython-312.pyc,,
openpyxl/chart/__pycache__/radar_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/reader.cpython-312.pyc,,
openpyxl/chart/__pycache__/reference.cpython-312.pyc,,
openpyxl/chart/__pycache__/scatter_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/series.cpython-312.pyc,,
openpyxl/chart/__pycache__/series_factory.cpython-312.pyc,,
openpyxl/chart/__pycache__/shapes.cpython-312.pyc,,
openpyxl/chart/__pycache__/stock_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/surface_chart.cpython-312.pyc,,
openpyxl/chart/__pycache__/text.cpython-312.pyc,,
openpyxl/chart/__pycache__/title.cpython-312.pyc,,
openpyxl/chart/__pycache__/trendline.cpython-312.pyc,,
openpyxl/chart/__pycache__/updown_bars.cpython-312.pyc,,
openpyxl/chart/_chart.py,sha256=j5xn6mQYmZ4E7y2V1Xvx1jwhX2_O68Mp-8zeXRteS7E,5746
openpyxl/chart/area_chart.py,sha256=uROD3fdus6yD1TGu87j4z7KtOEH7tI-3Z5NFK73wwgw,2890
openpyxl/chart/axis.py,sha256=yommy5q2mQWKmmLRouWBpimiBZDBM1K-UKAIwCwKDNc,12580
openpyxl/chart/bar_chart.py,sha256=_TQHleMT3gSa6B1BkKD_FkLFcv8LRaoiHbpy2yflLO4,4142
openpyxl/chart/bubble_chart.py,sha256=KL7VZYFyLDpA8MC-IFtRAUIN262xK6MzjU41DrSVgpY,2004
openpyxl/chart/chartspace.py,sha256=PuPGBsVbpK5JagbB7SWgp4JwdQtTrZzIm8mf3kfGAuY,6069
openpyxl/chart/data_source.py,sha256=GAuWoCOJ4k7RZNJZkZck0zt_-D5UfDEwqwQ3ND4-s34,5782
openpyxl/chart/descriptors.py,sha256=uj-qptwKOBeg7U5xBN4QJQ2OwQvFQ7o4n5eMXXIWS7M,736
openpyxl/chart/error_bar.py,sha256=GS_L7PiyKNnJVHvQqG2hLxEW237igLLCatCNC-xGMxk,1832
openpyxl/chart/label.py,sha256=IjvI-CZjTY8ydoUzUOihcbxoRWiSpFb_ipD6C2I8Pu4,4133
openpyxl/chart/layout.py,sha256=QHakp_CIcoNuvjyZMsQ2p_qP44DIQs4aquy7yln94JM,2040
openpyxl/chart/legend.py,sha256=iPMycOhYDAVYd05OU_QDB-GSavdw_1L9CMuJIETOoGI,2040
openpyxl/chart/line_chart.py,sha256=6tAyDCzFiuiBFuUDTWhQepH8xVCx2s57lH951cEcwn0,3951
openpyxl/chart/marker.py,sha256=kfybMkshK3qefOUW7OX-Os0vfl5OCXfg8MytwHC2i-w,2600
openpyxl/chart/picture.py,sha256=Q4eBNQMKQDHR91RnPc7tM-YZVdcnWncedUlfagj67gk,1156
openpyxl/chart/pie_chart.py,sha256=UOvkjrBpNd_rT-rvKcpPeVd9dK-ELdMIaHjAUEr6oN8,4793
openpyxl/chart/pivot.py,sha256=9kVDmnxnR0uQRQ-Wbl6qw8eew9LGhqomaDBaXqQGZY4,1741
openpyxl/chart/plotarea.py,sha256=em7yorXFz9SmJruqOR4Pn-2oEj0Su4rnzyNc5e0IZ_U,5805
openpyxl/chart/print_settings.py,sha256=UwB6Kn6xkLRBejXScl-utF8dkNhV7Lm3Lfk7ACpbRgs,1454
openpyxl/chart/radar_chart.py,sha256=93I1Y1dmXZ6Y0F1VKXz9I3x1ufgwygBOdbPZumR5n3s,1521
openpyxl/chart/reader.py,sha256=oQD-29oxSLW2yzXdyXNhzQYNXgM64Y3kVSOIkrPZCuU,802
openpyxl/chart/reference.py,sha256=N3T4qYMH9BVrtbDRiKIZz-qGvPAdfquWTGL0XKxD9G8,3098
openpyxl/chart/scatter_chart.py,sha256=JMU32jjxTj7txPJ2TebBHPS5UcMsRHVqLz_psnN2YZs,1563
openpyxl/chart/series.py,sha256=k8eR8cviH9EPllRjjr_2a-lH5S3_HWBTLyE7XKghzWc,5896
openpyxl/chart/series_factory.py,sha256=ey1zgNwM1g4bQwB9lLhM6E-ctLIM2kLWM3X7CPw8SDs,1368
openpyxl/chart/shapes.py,sha256=JkgMy3DUWDKLV6JZHKb_pUBvWpzTAQ3biUMr-1fJWZU,2815
openpyxl/chart/stock_chart.py,sha256=YJ7eElBX5omHziKo41ygTA7F_NEkyIlFUfdDJXZuKhM,1604
openpyxl/chart/surface_chart.py,sha256=_-yGEX-Ou2NJVmJCA_K_bSLyzk-RvbPupyQLmjfCWj0,2914
openpyxl/chart/text.py,sha256=voJCf4PK5olmX0g_5u9aQo8B5LpCUlOeq4j4pnOy_A0,1847
openpyxl/chart/title.py,sha256=L-7KxwcpMb2aZk4ikgMsIgFPVtBafIppx9ykd5FPJ4w,1952
openpyxl/chart/trendline.py,sha256=9pWSJa9Adwtd6v_i7dPT7qNKzhOrSMWZ4QuAOntZWVg,3045
openpyxl/chart/updown_bars.py,sha256=QA4lyEMtMVvZCrYUpHZYMVS1xsnaN4_T5UBi6E7ilQ0,897
openpyxl/chartsheet/__init__.py,sha256=3Ony1WNbxxWuddTW-peuUPvO3xqIWFWe3Da2OUzsVnI,71
openpyxl/chartsheet/__pycache__/__init__.cpython-312.pyc,,
openpyxl/chartsheet/__pycache__/chartsheet.cpython-312.pyc,,
openpyxl/chartsheet/__pycache__/custom.cpython-312.pyc,,
openpyxl/chartsheet/__pycache__/properties.cpython-312.pyc,,
openpyxl/chartsheet/__pycache__/protection.cpython-312.pyc,,
openpyxl/chartsheet/__pycache__/publish.cpython-312.pyc,,
openpyxl/chartsheet/__pycache__/relation.cpython-312.pyc,,
openpyxl/chartsheet/__pycache__/views.cpython-312.pyc,,
openpyxl/chartsheet/chartsheet.py,sha256=GTXNfQPYBaS4B7XB4f7gDkAo2kCjtZqidl6iDxp-JQ8,3911
openpyxl/chartsheet/custom.py,sha256=qVgeCzT7t1tN_pDwaLqtR3ubuPDLeTR5KKlcxwnTWa8,1691
openpyxl/chartsheet/properties.py,sha256=dR1nrp22FsPkyDrwQaZV7t-p-Z2Jc88Y2IhIGbBvFhk,679
openpyxl/chartsheet/protection.py,sha256=eJixEBmdoTDO2_0h6g51sdSdfSdCaP8UUNsbEqHds6U,1265
openpyxl/chartsheet/publish.py,sha256=PrwqsUKn2SK67ZM3NEGT9FH4nOKC1cOxxm3322hHawQ,1587
openpyxl/chartsheet/relation.py,sha256=ZAAfEZb639ve0k6ByRwmHdjBrjqVC0bHOLgIcBwRx6o,2731
openpyxl/chartsheet/views.py,sha256=My3Au-DEAcC4lwBARhrCcwsN7Lp9H6cFQT-SiAcJlko,1341
openpyxl/comments/__init__.py,sha256=k_QJ-OPRme8HgAYQlyxbbRhmS1n2FyowqIeekBW-7vw,67
openpyxl/comments/__pycache__/__init__.cpython-312.pyc,,
openpyxl/comments/__pycache__/author.cpython-312.pyc,,
openpyxl/comments/__pycache__/comment_sheet.cpython-312.pyc,,
openpyxl/comments/__pycache__/comments.cpython-312.pyc,,
openpyxl/comments/__pycache__/shape_writer.cpython-312.pyc,,
openpyxl/comments/author.py,sha256=PZB_fjQqiEm8BdHDblbfzB0gzkFvECWq5i1jSHeJZco,388
openpyxl/comments/comment_sheet.py,sha256=Uv2RPpIxrikDPHBr5Yj1dDkusZB97yVE-NQTM0-EnBk,5753
openpyxl/comments/comments.py,sha256=CxurAWM7WbCdbeya-DQklbiWSFaxhtrUNBZEzulTyxc,1466
openpyxl/comments/shape_writer.py,sha256=Ls1d0SscfxGM9H2spjxMNHeJSaZJuLawlXs4t4qH7v4,3809
openpyxl/compat/__init__.py,sha256=fltF__CdGK97l2V3MtIDxbwgV_p1AZvLdyqcEtXKsqs,1592
openpyxl/compat/__pycache__/__init__.cpython-312.pyc,,
openpyxl/compat/__pycache__/abc.cpython-312.pyc,,
openpyxl/compat/__pycache__/numbers.cpython-312.pyc,,
openpyxl/compat/__pycache__/product.cpython-312.pyc,,
openpyxl/compat/__pycache__/singleton.cpython-312.pyc,,
openpyxl/compat/__pycache__/strings.cpython-312.pyc,,
openpyxl/compat/abc.py,sha256=Y-L6pozzgjr81OfXsjDkGDeKEq6BOfMr6nvrFps_o6Q,155
openpyxl/compat/numbers.py,sha256=2dckE0PHT7eB89Sc2BdlWOH4ZLXWt3_eo73-CzRujUY,1617
openpyxl/compat/product.py,sha256=-bDgNMHGDgbahgw0jqale8TeIARLw7HO0soQAL9b_4k,264
openpyxl/compat/singleton.py,sha256=R1HiH7XpjaW4kr3GILWMc4hRGZkXyc0yK7T1jcg_QWg,1023
openpyxl/compat/strings.py,sha256=D_TWf8QnMH6WMx6xuCDfXl0boc1k9q7j8hGalVQ2RUk,604
openpyxl/descriptors/__init__.py,sha256=eISTR0Sa1ZKKNQPxMZtqlE39JugYzkjxiZf7u9fttiw,1952
openpyxl/descriptors/__pycache__/__init__.cpython-312.pyc,,
openpyxl/descriptors/__pycache__/base.cpython-312.pyc,,
openpyxl/descriptors/__pycache__/container.cpython-312.pyc,,
openpyxl/descriptors/__pycache__/excel.cpython-312.pyc,,
openpyxl/descriptors/__pycache__/namespace.cpython-312.pyc,,
openpyxl/descriptors/__pycache__/nested.cpython-312.pyc,,
openpyxl/descriptors/__pycache__/sequence.cpython-312.pyc,,
openpyxl/descriptors/__pycache__/serialisable.cpython-312.pyc,,
openpyxl/descriptors/__pycache__/slots.cpython-312.pyc,,
openpyxl/descriptors/base.py,sha256=-CuNfswEGazgOoX3GuM2Bs2zkBImT992TvR2R1xsnXM,7135
openpyxl/descriptors/container.py,sha256=IcO91M02hR0vXZtWGurz0IH1Vi2PoEECP1PEbz62FJQ,889
openpyxl/descriptors/excel.py,sha256=d6a6mtoZ-33jwMGlgvNTL54cqLANKyhMihG6887j8r0,2412
openpyxl/descriptors/namespace.py,sha256=LjI4e9R09NSbClr_ewv0YmHgWY8RO5xq1s-SpAvz2wo,313
openpyxl/descriptors/nested.py,sha256=5LSsf2uvTKsrGEEQF1KVXMLHZFoRgmLfL_lzW0lWQjI,2603
openpyxl/descriptors/sequence.py,sha256=OqF34K_nUC46XD5B_6xzGHeEICz_82hkFkNFXpBkSSE,3490
openpyxl/descriptors/serialisable.py,sha256=U_7wMEGQRIOiimUUL4AbdOiWMc_aLyKeaRnj_Z7dVO8,7361
openpyxl/descriptors/slots.py,sha256=xNj5vLWWoounpYqbP2JDnnhlTiTLRn-uTfQxncpFfn0,824
openpyxl/drawing/__init__.py,sha256=xlXVaT3Fs9ltvbbRIGTSRow9kw9nhLY3Zj1Mm6vXRHE,66
openpyxl/drawing/__pycache__/__init__.cpython-312.pyc,,
openpyxl/drawing/__pycache__/colors.cpython-312.pyc,,
openpyxl/drawing/__pycache__/connector.cpython-312.pyc,,
openpyxl/drawing/__pycache__/drawing.cpython-312.pyc,,
openpyxl/drawing/__pycache__/effect.cpython-312.pyc,,
openpyxl/drawing/__pycache__/fill.cpython-312.pyc,,
openpyxl/drawing/__pycache__/geometry.cpython-312.pyc,,
openpyxl/drawing/__pycache__/graphic.cpython-312.pyc,,
openpyxl/drawing/__pycache__/image.cpython-312.pyc,,
openpyxl/drawing/__pycache__/line.cpython-312.pyc,,
openpyxl/drawing/__pycache__/picture.cpython-312.pyc,,
openpyxl/drawing/__pycache__/properties.cpython-312.pyc,,
openpyxl/drawing/__pycache__/relation.cpython-312.pyc,,
openpyxl/drawing/__pycache__/spreadsheet_drawing.cpython-312.pyc,,
openpyxl/drawing/__pycache__/text.cpython-312.pyc,,
openpyxl/drawing/__pycache__/xdr.cpython-312.pyc,,
openpyxl/drawing/colors.py,sha256=d92d6LQv2xi4xVt0F6bEJz-kpe4ahghNsOIY0_cxgQI,15251
openpyxl/drawing/connector.py,sha256=4be6kFwDmixqYX6ko22JE3cqJ9xUM7lRonSer1BDVgY,3863
openpyxl/drawing/drawing.py,sha256=Wbv24TZbNaPngDR3adOj6jUBg-iyMYyfvgEPg-5IPu8,2339
openpyxl/drawing/effect.py,sha256=vZ5r9k3JfyaAoBggFzN9wyvsEDnMnAmkQZsdVQN1-wo,9435
openpyxl/drawing/fill.py,sha256=Z_kAY5bncgu1WkZNvgjiX5ucrYI6GLXyUi6H3_mne2k,13092
openpyxl/drawing/geometry.py,sha256=0UM5hMHYy_R3C-lHt5x3NECDn7O1tfbKu5BweLwdLlg,17523
openpyxl/drawing/graphic.py,sha256=013KhmTqp1PFKht9lRRA6SHjznxq9EL4u_ybA88OuCk,4811
openpyxl/drawing/image.py,sha256=ROO0YJjzH9eqjPUKU5bMtt4bXnHFK9uofDa2__R3G2k,1455
openpyxl/drawing/line.py,sha256=CRxV0NUpce4RfXPDllodcneoHk8vr2Ind8HaWnUv2HE,3904
openpyxl/drawing/picture.py,sha256=tDYob2x4encQ9rUWOe29PqtiRSDEj746j-SvQ7rVV10,4205
openpyxl/drawing/properties.py,sha256=TyLOF3ehp38XJvuupNZdsOqZ0HNXkVPBDYwU5O1GhBM,4948
openpyxl/drawing/relation.py,sha256=InbM75ymWUjICXhjyCcYqp1FWcfCFp9q9vecYLptzk4,344
openpyxl/drawing/spreadsheet_drawing.py,sha256=CUWSpIYWOHUEp-USOAGVNlLfXBQObcGdg_RZ_bggPYM,10721
openpyxl/drawing/text.py,sha256=6_ShIu9FLG7MJvMLs_G_tTatTaBqxpaX5KMKxSfTY7Y,22421
openpyxl/drawing/xdr.py,sha256=XE2yRzlCqoJBWg3TPRxelzZ4GmBV9dDFTtiJgJZku-U,626
openpyxl/formatting/__init__.py,sha256=vpkL3EimMa-moJjcWk4l3bIWdJ3c7a8pKOfGlnPte9c,59
openpyxl/formatting/__pycache__/__init__.cpython-312.pyc,,
openpyxl/formatting/__pycache__/formatting.cpython-312.pyc,,
openpyxl/formatting/__pycache__/rule.cpython-312.pyc,,
openpyxl/formatting/formatting.py,sha256=AdXlrhic4CPvyJ300oFJPJUH-2vS0VNOLiNudt3U26c,2701
openpyxl/formatting/rule.py,sha256=96Fc5-hSByCrvkC1O0agEoZyL9G_AdeulrjRXnf_rZ8,9288
openpyxl/formula/__init__.py,sha256=AgvEdunVryhzwecuFVO2EezdJT3h5gCXpw2j3f5VUWA,69
openpyxl/formula/__pycache__/__init__.cpython-312.pyc,,
openpyxl/formula/__pycache__/tokenizer.cpython-312.pyc,,
openpyxl/formula/__pycache__/translate.cpython-312.pyc,,
openpyxl/formula/tokenizer.py,sha256=o1jDAOl79YiCWr-2LmSICyAbhm2hdb-37jriasmv4dc,15088
openpyxl/formula/translate.py,sha256=Zs9adqfZTAuo8J_QNbqK3vjQDlSFhWc0vWc6TCMDYrI,6653
openpyxl/packaging/__init__.py,sha256=KcNtO2zoYizOgG-iZzayZffSL1WeZR98i1Q8QYTRhfI,90
openpyxl/packaging/__pycache__/__init__.cpython-312.pyc,,
openpyxl/packaging/__pycache__/core.cpython-312.pyc,,
openpyxl/packaging/__pycache__/custom.cpython-312.pyc,,
openpyxl/packaging/__pycache__/extended.cpython-312.pyc,,
openpyxl/packaging/__pycache__/interface.cpython-312.pyc,,
openpyxl/packaging/__pycache__/manifest.cpython-312.pyc,,
openpyxl/packaging/__pycache__/relationship.cpython-312.pyc,,
openpyxl/packaging/__pycache__/workbook.cpython-312.pyc,,
openpyxl/packaging/core.py,sha256=OSbSFGZrKYcZszcHe3LhQEyiAf2Wylwxm4_6N8WO-Yo,4061
openpyxl/packaging/custom.py,sha256=uCEl7IwITFX2pOxiAITnvNbfsav80uHB0wXUFvjIRUQ,6738
openpyxl/packaging/extended.py,sha256=JFksxDd67rA57n-vxg48tbeZh2g2LEOb0fgJLeqbTWM,4810
openpyxl/packaging/interface.py,sha256=vlGVt4YvyUR4UX9Tr9xmkn1G8s_ynYVtAx4okJ6-g_8,920
openpyxl/packaging/manifest.py,sha256=y5zoDQnhJ1aW_HPLItY_WE94fSLS4jxvfIqn_J2zJ6Q,5366
openpyxl/packaging/relationship.py,sha256=jLhvFvDVZBRTZTXokRrrsEiLI9CmFlulhGzA_OYKM0Q,3974
openpyxl/packaging/workbook.py,sha256=s4jl4gqqMkaUHmMAR52dc9ZoNTieuXcq1OG3cgNDYjw,6495
openpyxl/pivot/__init__.py,sha256=c12-9kMPWlUdjwSoZPsFpmeW8KVXH0HCGpO3dlCTVqI,35
openpyxl/pivot/__pycache__/__init__.cpython-312.pyc,,
openpyxl/pivot/__pycache__/cache.cpython-312.pyc,,
openpyxl/pivot/__pycache__/fields.cpython-312.pyc,,
openpyxl/pivot/__pycache__/record.cpython-312.pyc,,
openpyxl/pivot/__pycache__/table.cpython-312.pyc,,
openpyxl/pivot/cache.py,sha256=kKQMEcoYb9scl_CNNWfmNOTewD5S3hpBGwViMtDCyx0,27840
openpyxl/pivot/fields.py,sha256=0CQLdTOBhYAa9gfEZb_bvkgCx8feASYp64dqFskDkqU,7057
openpyxl/pivot/record.py,sha256=c45ft1YsPAVRneMVh_WvUQ1nZt9RJQ_josRuolKx3qE,2671
openpyxl/pivot/table.py,sha256=riKBeb1aICXWipnhpSaSx9iqP-AkfcyOSm3Dfl407dA,40756
openpyxl/reader/__init__.py,sha256=c12-9kMPWlUdjwSoZPsFpmeW8KVXH0HCGpO3dlCTVqI,35
openpyxl/reader/__pycache__/__init__.cpython-312.pyc,,
openpyxl/reader/__pycache__/drawings.cpython-312.pyc,,
openpyxl/reader/__pycache__/excel.cpython-312.pyc,,
openpyxl/reader/__pycache__/strings.cpython-312.pyc,,
openpyxl/reader/__pycache__/workbook.cpython-312.pyc,,
openpyxl/reader/drawings.py,sha256=iZPok8Dc_mZMyRPk_EfDXDQvZdwfHwbYjvxfK2cXtag,2209
openpyxl/reader/excel.py,sha256=kgStQtO1j0vV56GWaXxo3GA2EXuouGtnFrRVMocq8EY,12357
openpyxl/reader/strings.py,sha256=oG2Mq6eBD0-ElFOxPdHTBUmshUxTNrK1sns1UJRaVis,1113
openpyxl/reader/workbook.py,sha256=4w0LRV7qNNGHDnYd19zUgWnJOEX8tHjm3vlkxwllzv4,4352
openpyxl/styles/__init__.py,sha256=2QNNdlz4CjhnkBQVNhZ-12Yz73_uHIinqRKWo_TjNwg,363
openpyxl/styles/__pycache__/__init__.cpython-312.pyc,,
openpyxl/styles/__pycache__/alignment.cpython-312.pyc,,
openpyxl/styles/__pycache__/borders.cpython-312.pyc,,
openpyxl/styles/__pycache__/builtins.cpython-312.pyc,,
openpyxl/styles/__pycache__/cell_style.cpython-312.pyc,,
openpyxl/styles/__pycache__/colors.cpython-312.pyc,,
openpyxl/styles/__pycache__/differential.cpython-312.pyc,,
openpyxl/styles/__pycache__/fills.cpython-312.pyc,,
openpyxl/styles/__pycache__/fonts.cpython-312.pyc,,
openpyxl/styles/__pycache__/named_styles.cpython-312.pyc,,
openpyxl/styles/__pycache__/numbers.cpython-312.pyc,,
openpyxl/styles/__pycache__/protection.cpython-312.pyc,,
openpyxl/styles/__pycache__/proxy.cpython-312.pyc,,
openpyxl/styles/__pycache__/styleable.cpython-312.pyc,,
openpyxl/styles/__pycache__/stylesheet.cpython-312.pyc,,
openpyxl/styles/__pycache__/table.cpython-312.pyc,,
openpyxl/styles/alignment.py,sha256=wQOEtmYhPJFtnuBq0juMe5EsCp9DNSVS1ieBhlAnwWE,2198
openpyxl/styles/borders.py,sha256=BLUTOyBbxWQzv8Kuh1u4sWfJiIPJc8QExb7nGwdSmXc,3302
openpyxl/styles/builtins.py,sha256=cMtJverVSjdIdCckP6L-AlI0OLMRPgbQwaJWUkldA0U,31182
openpyxl/styles/cell_style.py,sha256=8Ol5F6ktKeSqhDVF-10w5eIh7W-jkzijpPPHqqv1qDs,5414
openpyxl/styles/colors.py,sha256=Ss3QqNS5YISVkJxlNfd4q_YSrFKdKjATWLDSu2rPMBc,4612
openpyxl/styles/differential.py,sha256=dqEGny_ou1jC3tegBal1w_UbONyQEJXvGPURs8xWwfU,2267
openpyxl/styles/fills.py,sha256=LmR4H00GzKDWyYjzDEayzKZN28S_muD65DvAFWlbaCI,6380
openpyxl/styles/fonts.py,sha256=nkeiJUgKYnWaETvn51sOo9zQXJiOEJKHDTqvxt0JiBc,3516
openpyxl/styles/named_styles.py,sha256=nfL1KPpd6b0Y0qBrGJQ15EUOebfeO1eZBQhPVpcZW-o,7254
openpyxl/styles/numbers.py,sha256=6kK7mdBD-0xs7bjYDFNGsUAvoFvRu5wSMjOF9J5j-Go,5097
openpyxl/styles/protection.py,sha256=BUHgARq7SjOVfW_ST53hKCUofVBEWXn3Lnn_c5n4i_I,394
openpyxl/styles/proxy.py,sha256=ajsvzRp_MOeV_rZSEfVoti6-3tW8aowo5_Hjwp2AlfA,1432
openpyxl/styles/styleable.py,sha256=Yl_-oPljEuFzg9tXKSSCuvWRL4L0HC5bHMFJVhex6Oc,4499
openpyxl/styles/stylesheet.py,sha256=7kZpzyavLrOJcdZqZzl3WZTyM60CqWP8i_OQ0J_1xy0,8790
openpyxl/styles/table.py,sha256=VexRqPPQmjRzWe1rVTOgyOQgvlCBuEYTif5MEV_0qsk,2801
openpyxl/utils/__init__.py,sha256=wCMNXgIoA4aF4tpSuSzxm1k3SmJJGOEjtdbqdJZZG7I,324
openpyxl/utils/__pycache__/__init__.cpython-312.pyc,,
openpyxl/utils/__pycache__/bound_dictionary.cpython-312.pyc,,
openpyxl/utils/__pycache__/cell.cpython-312.pyc,,
openpyxl/utils/__pycache__/dataframe.cpython-312.pyc,,
openpyxl/utils/__pycache__/datetime.cpython-312.pyc,,
openpyxl/utils/__pycache__/escape.cpython-312.pyc,,
openpyxl/utils/__pycache__/exceptions.cpython-312.pyc,,
openpyxl/utils/__pycache__/formulas.cpython-312.pyc,,
openpyxl/utils/__pycache__/indexed_list.cpython-312.pyc,,
openpyxl/utils/__pycache__/inference.cpython-312.pyc,,
openpyxl/utils/__pycache__/protection.cpython-312.pyc,,
openpyxl/utils/__pycache__/units.cpython-312.pyc,,
openpyxl/utils/bound_dictionary.py,sha256=zfzflQom1FqfEw8uexBqI8eExCeAWELzSk4TqqpD-w8,717
openpyxl/utils/cell.py,sha256=P7og4c4JcSN__amIsubIMgSMlQ4SrAA5eZ0cjkoXlaQ,6967
openpyxl/utils/dataframe.py,sha256=d3SPeb4p9YKFwlFTUWhdVUYYyMLNrd9atC6iSf2QB6w,2957
openpyxl/utils/datetime.py,sha256=xQ8zHJFb-n4nlN6fA_fFZKHlHeNOB7El48p9-YOPvGE,4529
openpyxl/utils/escape.py,sha256=4dgcSlSdPNk0vkJNHRUK9poEe8pn4sBIQ5Rjz-7H1Uk,790
openpyxl/utils/exceptions.py,sha256=WT40gTyd9YUhg1MeqZNzHp9qJnL5eXzbCEb_VtHp3Kk,889
openpyxl/utils/formulas.py,sha256=-I0zyvicBZMaAH1XzsmmEEzE4GB-NA605aArWVt9ik4,4248
openpyxl/utils/indexed_list.py,sha256=hBsQP9gunTit7iKdMGw_tM3y5uIpXDjUx7jswbQF6Dc,1257
openpyxl/utils/inference.py,sha256=dM1FBW_Rx_xE7P8vGo6WNhbBe-2eqpGuJj4eqdS7UjE,1583
openpyxl/utils/protection.py,sha256=opm7GVM2ePQvpNzKT-W56u-0yP8liS9WJkxpzpG_tE0,830
openpyxl/utils/units.py,sha256=eGpGrdzyoKlqLs99eALNC5c1gSLXRo4GdUNAqdB4wzg,2642
openpyxl/workbook/__init__.py,sha256=yKMikN8VqoVZJcoZSVW3p9Smt88ibeqNq9NHhGBJqEM,68
openpyxl/workbook/__pycache__/__init__.cpython-312.pyc,,
openpyxl/workbook/__pycache__/_writer.cpython-312.pyc,,
openpyxl/workbook/__pycache__/child.cpython-312.pyc,,
openpyxl/workbook/__pycache__/defined_name.cpython-312.pyc,,
openpyxl/workbook/__pycache__/external_reference.cpython-312.pyc,,
openpyxl/workbook/__pycache__/function_group.cpython-312.pyc,,
openpyxl/workbook/__pycache__/properties.cpython-312.pyc,,
openpyxl/workbook/__pycache__/protection.cpython-312.pyc,,
openpyxl/workbook/__pycache__/smart_tags.cpython-312.pyc,,
openpyxl/workbook/__pycache__/views.cpython-312.pyc,,
openpyxl/workbook/__pycache__/web.cpython-312.pyc,,
openpyxl/workbook/__pycache__/workbook.cpython-312.pyc,,
openpyxl/workbook/_writer.py,sha256=pB4s05erNEBJFT_w5LT-2DlxqXkZLOutXWVgewRLVds,6506
openpyxl/workbook/child.py,sha256=r_5V9DNkGSYZhzi62P10ZnsO5iT518YopcTdmSvtAUc,4052
openpyxl/workbook/defined_name.py,sha256=EAF1WvGYU4WG7dusDi29yBAr15BhkYtkF_GrFym1DDY,5394
openpyxl/workbook/external_link/__init__.py,sha256=YOkLI226nyopB6moShzGIfBRckdQgPiFXjVZwXW-DpE,71
openpyxl/workbook/external_link/__pycache__/__init__.cpython-312.pyc,,
openpyxl/workbook/external_link/__pycache__/external.cpython-312.pyc,,
openpyxl/workbook/external_link/external.py,sha256=LXXuej0-d0iRnwlJ-13S81kbuDxvhAWo3qfnxpsClvM,4509
openpyxl/workbook/external_reference.py,sha256=9bKX9_QgNJxv7fEUd0G-ocXyZajMAsDzG11d0miguxY,348
openpyxl/workbook/function_group.py,sha256=x5QfUpFdsjtbFbAJzZof7SrZ376nufNY92mpCcaSPiQ,803
openpyxl/workbook/properties.py,sha256=vMUriu67iQU11xIos37ayv73gjq1kdHgI27ncJ3Vk24,5261
openpyxl/workbook/protection.py,sha256=LhiyuoOchdrun9xMwq_pxGzbkysziThfKivk0dHHOLw,6008
openpyxl/workbook/smart_tags.py,sha256=xHHXCrUPnHeRoM_eakrCOz-eCpct3Y7xKHShr9wGv7s,1181
openpyxl/workbook/views.py,sha256=uwQqZCrRavAoBDLZIBtgz7riOEhEaHplybV4cX_TMgY,5214
openpyxl/workbook/web.py,sha256=87B5mEZ6vfHTwywcGtcYL6u7D3RyJVDCJxV0nHZeS-w,2642
openpyxl/workbook/workbook.py,sha256=oaErvSH1qUphUAPOZTCHj2UHyKeDqsj2DycKCDcgo7M,13232
openpyxl/worksheet/__init__.py,sha256=c12-9kMPWlUdjwSoZPsFpmeW8KVXH0HCGpO3dlCTVqI,35
openpyxl/worksheet/__pycache__/__init__.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/_read_only.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/_reader.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/_write_only.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/_writer.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/cell_range.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/cell_watch.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/controls.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/copier.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/custom.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/datavalidation.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/dimensions.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/drawing.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/errors.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/filters.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/formula.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/header_footer.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/hyperlink.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/merge.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/ole.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/page.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/pagebreak.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/picture.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/print_settings.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/properties.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/protection.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/related.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/scenario.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/smart_tag.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/table.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/views.cpython-312.pyc,,
openpyxl/worksheet/__pycache__/worksheet.cpython-312.pyc,,
openpyxl/worksheet/_read_only.py,sha256=6Kd4Q-73UoJDY66skRJy_ks-wCHNttlGhsDxvB99PuY,5709
openpyxl/worksheet/_reader.py,sha256=vp_D7w4DiADMdyNrYpQglrCVvVLT9_DsSZikOd--n2c,16375
openpyxl/worksheet/_write_only.py,sha256=yqW-DtBDDYTwGCBHRVIwkheSB7SSLO3xlw-RsXtPorE,4232
openpyxl/worksheet/_writer.py,sha256=bDtw6BV5tdztARQEkQPprExRr8hZVFkj0DyolqxVu2k,10283
openpyxl/worksheet/cell_range.py,sha256=YP8AUnqUFP5wOV_avMDFRSZ0Qi2p78RWFuwyyCua7m8,15013
openpyxl/worksheet/cell_watch.py,sha256=LdxGcTmXbZ4sxm6inasFgZPld1ijdL5_ODSUvvz13DU,608
openpyxl/worksheet/controls.py,sha256=FPLg4N94T-IL27NLg8Le_U4WYDT_6Aa25LDG_kiEDVA,2735
openpyxl/worksheet/copier.py,sha256=0Di1qSks0g7Jtgmpc_M20O-KPCW81Yr2myC5j458nyU,2319
openpyxl/worksheet/custom.py,sha256=CRlQ98GwqqKmEDkv8gPUCa0ApNM2Vz-BLs_-RMu3jLA,639
openpyxl/worksheet/datavalidation.py,sha256=m-O7NOoTDr_bAfxB9xEeY5QttFiuPtzs-IFAlF0j4FE,6131
openpyxl/worksheet/dimensions.py,sha256=HzM77FrYixiQDCugRT-C9ZpKq7GNFaGchxT73U4cisY,9102
openpyxl/worksheet/drawing.py,sha256=2nfrLyTX0kAizPIINF12KwDW9mRnaq8hs-NrSBcWpmE,275
openpyxl/worksheet/errors.py,sha256=KkFC4bnckvCp74XsVXA7JUCi4MIimEFu3uAddcQpjo0,2435
openpyxl/worksheet/filters.py,sha256=8eUj2LuP8Qbz1R1gkK1c6W_UKS8-__6XlFMVkunIua0,13854
openpyxl/worksheet/formula.py,sha256=5yuul6s1l-K_78KXHC6HrF_pLhxypoldh5jMg7zmlyY,1045
openpyxl/worksheet/header_footer.py,sha256=91F6NUDUEwrhgeWrxG9XtDPyPD03XAtGU_ONBpkAfUc,7886
openpyxl/worksheet/hyperlink.py,sha256=sXzPkkjl9BWNzCxwwEEaSS53J37jIXPmnnED-j8MIBo,1103
openpyxl/worksheet/merge.py,sha256=gNOIH6EJ8wVcJpibAv4CMc7UpD7_DrGvgaCSvG2im5A,4125
openpyxl/worksheet/ole.py,sha256=khVvqMt4GPc9Yr6whLDfkUo51euyLXfJe1p4zFee4no,3530
openpyxl/worksheet/page.py,sha256=4jeSRcDE0S2RPzIAmA3Bh-uXRyq0hnbO5h5pJdGHbbQ,4901
openpyxl/worksheet/pagebreak.py,sha256=XXFIMOY4VdPQCd86nGPghA6hOfLGK5G_KFuvjBNPRsw,1811
openpyxl/worksheet/picture.py,sha256=72TctCxzk2JU8uFfjiEbTBufEe5eQxIieSPBRhU6m1Q,185
openpyxl/worksheet/print_settings.py,sha256=k_g4fkrs9bfz-S-RIKIBGqzVgubufMdryWQ3ejXQoRI,5215
openpyxl/worksheet/properties.py,sha256=9iXTOVC8B9C-2pp_iU5l0r5Fjf3Uzv0SIOUKRrZ2hw4,3087
openpyxl/worksheet/protection.py,sha256=vj5M6WWC5xKiHeWS_tJqXxrlOJHJ7GpW2JdPw7r9jjE,3758
openpyxl/worksheet/related.py,sha256=ZLDpgcrW6DWl8vvh2sSVB_r1JyG8bC8EicCBKjfssTs,335
openpyxl/worksheet/scenario.py,sha256=VlJW4pi1OTy1cJ9m7ZxazIy8PSlo17BGpnUYixmNotQ,2401
openpyxl/worksheet/smart_tag.py,sha256=nLbt04IqeJllk7TmNS1eTNdb7On5jMf3llfyy3otDSk,1608
openpyxl/worksheet/table.py,sha256=gjt-jNP8dhVy8w5g-gMJpfHO-eV1EoxJy91yi-5HG64,11671
openpyxl/worksheet/views.py,sha256=DkZcptwpbpklHILSlvK-a2LmJ7BWb1wbDcz2JVl7404,4974
openpyxl/worksheet/worksheet.py,sha256=4JM5qjoJumtcqftHFkimtFEQrz7E2DBmXnkVo7R3WX8,27572
openpyxl/writer/__init__.py,sha256=c12-9kMPWlUdjwSoZPsFpmeW8KVXH0HCGpO3dlCTVqI,35
openpyxl/writer/__pycache__/__init__.cpython-312.pyc,,
openpyxl/writer/__pycache__/excel.cpython-312.pyc,,
openpyxl/writer/__pycache__/theme.cpython-312.pyc,,
openpyxl/writer/excel.py,sha256=6ioXn3hSHHIUnkW2wCyBgPA4CncO6FXL5yGSAzsqp6Y,9572
openpyxl/writer/theme.py,sha256=5Hhq-0uP55sf_Zhw7i3M9azCfCjALQxoo7CV_9QPmTA,10320
openpyxl/xml/__init__.py,sha256=A5Kj0GWk5XI-zJxbAL5vIppV_AgEHLRveGu8RK5c7U0,1016
openpyxl/xml/__pycache__/__init__.cpython-312.pyc,,
openpyxl/xml/__pycache__/constants.cpython-312.pyc,,
openpyxl/xml/__pycache__/functions.cpython-312.pyc,,
openpyxl/xml/constants.py,sha256=HDNnhcj-WO9ayO4Mqwca3Au0ZTNfsDqWDtleREs_Wto,4833
openpyxl/xml/functions.py,sha256=jBtfa8__w4gBlEPGHLGCAtJiaNKPyihTLsfmigyq2_Q,2025

View File

@ -0,0 +1,6 @@
Wheel-Version: 1.0
Generator: bdist_wheel (0.43.0)
Root-Is-Purelib: true
Tag: py2-none-any
Tag: py3-none-any

View File

@ -0,0 +1 @@
openpyxl

View File

@ -0,0 +1,19 @@
# Copyright (c) 2010-2024 openpyxl
DEBUG = False
from openpyxl.compat.numbers import NUMPY
from openpyxl.xml import DEFUSEDXML, LXML
from openpyxl.workbook import Workbook
from openpyxl.reader.excel import load_workbook as open
from openpyxl.reader.excel import load_workbook
import openpyxl._constants as constants
# Expose constants especially the version number
__author__ = constants.__author__
__author_email__ = constants.__author_email__
__license__ = constants.__license__
__maintainer_email__ = constants.__maintainer_email__
__url__ = constants.__url__
__version__ = constants.__version__

View File

@ -0,0 +1,13 @@
# Copyright (c) 2010-2024 openpyxl
"""
Package metadata
"""
__author__ = "See AUTHORS"
__author_email__ = "charlie.clark@clark-consulting.eu"
__license__ = "MIT"
__maintainer_email__ = "openpyxl-users@googlegroups.com"
__url__ = "https://openpyxl.readthedocs.io"
__version__ = "3.1.5"
__python__ = "3.8"

View File

@ -0,0 +1,4 @@
# Copyright (c) 2010-2024 openpyxl
from .cell import Cell, WriteOnlyCell, MergedCell
from .read_only import ReadOnlyCell

View File

@ -0,0 +1,136 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.compat import safe_string
from openpyxl.xml.functions import Element, SubElement, whitespace, XML_NS
from openpyxl import LXML
from openpyxl.utils.datetime import to_excel, to_ISO8601
from datetime import timedelta
from openpyxl.worksheet.formula import DataTableFormula, ArrayFormula
from openpyxl.cell.rich_text import CellRichText
def _set_attributes(cell, styled=None):
"""
Set coordinate and datatype
"""
coordinate = cell.coordinate
attrs = {'r': coordinate}
if styled:
attrs['s'] = f"{cell.style_id}"
if cell.data_type == "s":
attrs['t'] = "inlineStr"
elif cell.data_type != 'f':
attrs['t'] = cell.data_type
value = cell._value
if cell.data_type == "d":
if hasattr(value, "tzinfo") and value.tzinfo is not None:
raise TypeError("Excel does not support timezones in datetimes. "
"The tzinfo in the datetime/time object must be set to None.")
if cell.parent.parent.iso_dates and not isinstance(value, timedelta):
value = to_ISO8601(value)
else:
attrs['t'] = "n"
value = to_excel(value, cell.parent.parent.epoch)
if cell.hyperlink:
cell.parent._hyperlinks.append(cell.hyperlink)
return value, attrs
def etree_write_cell(xf, worksheet, cell, styled=None):
value, attributes = _set_attributes(cell, styled)
el = Element("c", attributes)
if value is None or value == "":
xf.write(el)
return
if cell.data_type == 'f':
attrib = {}
if isinstance(value, ArrayFormula):
attrib = dict(value)
value = value.text
elif isinstance(value, DataTableFormula):
attrib = dict(value)
value = None
formula = SubElement(el, 'f', attrib)
if value is not None and not attrib.get('t') == "dataTable":
formula.text = value[1:]
value = None
if cell.data_type == 's':
if isinstance(value, CellRichText):
el.append(value.to_tree())
else:
inline_string = Element("is")
text = Element('t')
text.text = value
whitespace(text)
inline_string.append(text)
el.append(inline_string)
else:
cell_content = SubElement(el, 'v')
if value is not None:
cell_content.text = safe_string(value)
xf.write(el)
def lxml_write_cell(xf, worksheet, cell, styled=False):
value, attributes = _set_attributes(cell, styled)
if value == '' or value is None:
with xf.element("c", attributes):
return
with xf.element('c', attributes):
if cell.data_type == 'f':
attrib = {}
if isinstance(value, ArrayFormula):
attrib = dict(value)
value = value.text
elif isinstance(value, DataTableFormula):
attrib = dict(value)
value = None
with xf.element('f', attrib):
if value is not None and not attrib.get('t') == "dataTable":
xf.write(value[1:])
value = None
if cell.data_type == 's':
if isinstance(value, CellRichText):
el = value.to_tree()
xf.write(el)
else:
with xf.element("is"):
if isinstance(value, str):
attrs = {}
if value != value.strip():
attrs["{%s}space" % XML_NS] = "preserve"
el = Element("t", attrs) # lxml can't handle xml-ns
el.text = value
xf.write(el)
else:
with xf.element("v"):
if value is not None:
xf.write(safe_string(value))
if LXML:
write_cell = lxml_write_cell
else:
write_cell = etree_write_cell

View File

@ -0,0 +1,332 @@
# Copyright (c) 2010-2024 openpyxl
"""Manage individual cells in a spreadsheet.
The Cell class is required to know its value and type, display options,
and any other features of an Excel cell. Utilities for referencing
cells using Excel's 'A1' column/row nomenclature are also provided.
"""
__docformat__ = "restructuredtext en"
# Python stdlib imports
from copy import copy
import datetime
import re
from openpyxl.compat import (
NUMERIC_TYPES,
)
from openpyxl.utils.exceptions import IllegalCharacterError
from openpyxl.utils import get_column_letter
from openpyxl.styles import numbers, is_date_format
from openpyxl.styles.styleable import StyleableObject
from openpyxl.worksheet.hyperlink import Hyperlink
from openpyxl.worksheet.formula import DataTableFormula, ArrayFormula
from openpyxl.cell.rich_text import CellRichText
# constants
TIME_TYPES = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
TIME_FORMATS = {
datetime.datetime:numbers.FORMAT_DATE_DATETIME,
datetime.date:numbers.FORMAT_DATE_YYYYMMDD2,
datetime.time:numbers.FORMAT_DATE_TIME6,
datetime.timedelta:numbers.FORMAT_DATE_TIMEDELTA,
}
STRING_TYPES = (str, bytes, CellRichText)
KNOWN_TYPES = NUMERIC_TYPES + TIME_TYPES + STRING_TYPES + (bool, type(None))
ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]')
ERROR_CODES = ('#NULL!', '#DIV/0!', '#VALUE!', '#REF!', '#NAME?', '#NUM!',
'#N/A')
TYPE_STRING = 's'
TYPE_FORMULA = 'f'
TYPE_NUMERIC = 'n'
TYPE_BOOL = 'b'
TYPE_NULL = 'n'
TYPE_INLINE = 'inlineStr'
TYPE_ERROR = 'e'
TYPE_FORMULA_CACHE_STRING = 'str'
VALID_TYPES = (TYPE_STRING, TYPE_FORMULA, TYPE_NUMERIC, TYPE_BOOL,
TYPE_NULL, TYPE_INLINE, TYPE_ERROR, TYPE_FORMULA_CACHE_STRING)
_TYPES = {int:'n', float:'n', str:'s', bool:'b'}
def get_type(t, value):
if isinstance(value, NUMERIC_TYPES):
dt = 'n'
elif isinstance(value, STRING_TYPES):
dt = 's'
elif isinstance(value, TIME_TYPES):
dt = 'd'
elif isinstance(value, (DataTableFormula, ArrayFormula)):
dt = 'f'
else:
return
_TYPES[t] = dt
return dt
def get_time_format(t):
value = TIME_FORMATS.get(t)
if value:
return value
for base in t.mro()[1:]:
value = TIME_FORMATS.get(base)
if value:
TIME_FORMATS[t] = value
return value
raise ValueError("Could not get time format for {0!r}".format(value))
class Cell(StyleableObject):
"""Describes cell associated properties.
Properties of interest include style, type, value, and address.
"""
__slots__ = (
'row',
'column',
'_value',
'data_type',
'parent',
'_hyperlink',
'_comment',
)
def __init__(self, worksheet, row=None, column=None, value=None, style_array=None):
super().__init__(worksheet, style_array)
self.row = row
"""Row number of this cell (1-based)"""
self.column = column
"""Column number of this cell (1-based)"""
# _value is the stored value, while value is the displayed value
self._value = None
self._hyperlink = None
self.data_type = 'n'
if value is not None:
self.value = value
self._comment = None
@property
def coordinate(self):
"""This cell's coordinate (ex. 'A5')"""
col = get_column_letter(self.column)
return f"{col}{self.row}"
@property
def col_idx(self):
"""The numerical index of the column"""
return self.column
@property
def column_letter(self):
return get_column_letter(self.column)
@property
def encoding(self):
return self.parent.encoding
@property
def base_date(self):
return self.parent.parent.epoch
def __repr__(self):
return "<Cell {0!r}.{1}>".format(self.parent.title, self.coordinate)
def check_string(self, value):
"""Check string coding, length, and line break character"""
if value is None:
return
# convert to str string
if not isinstance(value, str):
value = str(value, self.encoding)
value = str(value)
# string must never be longer than 32,767 characters
# truncate if necessary
value = value[:32767]
if next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
raise IllegalCharacterError(f"{value} cannot be used in worksheets.")
return value
def check_error(self, value):
"""Tries to convert Error" else N/A"""
try:
return str(value)
except UnicodeDecodeError:
return u'#N/A'
def _bind_value(self, value):
"""Given a value, infer the correct data type"""
self.data_type = "n"
t = type(value)
try:
dt = _TYPES[t]
except KeyError:
dt = get_type(t, value)
if dt is None and value is not None:
raise ValueError("Cannot convert {0!r} to Excel".format(value))
if dt:
self.data_type = dt
if dt == 'd':
if not is_date_format(self.number_format):
self.number_format = get_time_format(t)
elif dt == "s" and not isinstance(value, CellRichText):
value = self.check_string(value)
if len(value) > 1 and value.startswith("="):
self.data_type = 'f'
elif value in ERROR_CODES:
self.data_type = 'e'
self._value = value
@property
def value(self):
"""Get or set the value held in the cell.
:type: depends on the value (string, float, int or
:class:`datetime.datetime`)
"""
return self._value
@value.setter
def value(self, value):
"""Set the value and infer type and display options."""
self._bind_value(value)
@property
def internal_value(self):
"""Always returns the value for excel."""
return self._value
@property
def hyperlink(self):
"""Return the hyperlink target or an empty string"""
return self._hyperlink
@hyperlink.setter
def hyperlink(self, val):
"""Set value and display for hyperlinks in a cell.
Automatically sets the `value` of the cell with link text,
but you can modify it afterwards by setting the `value`
property, and the hyperlink will remain.
Hyperlink is removed if set to ``None``."""
if val is None:
self._hyperlink = None
else:
if not isinstance(val, Hyperlink):
val = Hyperlink(ref="", target=val)
val.ref = self.coordinate
self._hyperlink = val
if self._value is None:
self.value = val.target or val.location
@property
def is_date(self):
"""True if the value is formatted as a date
:type: bool
"""
return self.data_type == 'd' or (
self.data_type == 'n' and is_date_format(self.number_format)
)
def offset(self, row=0, column=0):
"""Returns a cell location relative to this cell.
:param row: number of rows to offset
:type row: int
:param column: number of columns to offset
:type column: int
:rtype: :class:`openpyxl.cell.Cell`
"""
offset_column = self.col_idx + column
offset_row = self.row + row
return self.parent.cell(column=offset_column, row=offset_row)
@property
def comment(self):
""" Returns the comment associated with this cell
:type: :class:`openpyxl.comments.Comment`
"""
return self._comment
@comment.setter
def comment(self, value):
"""
Assign a comment to a cell
"""
if value is not None:
if value.parent:
value = copy(value)
value.bind(self)
elif value is None and self._comment:
self._comment.unbind()
self._comment = value
class MergedCell(StyleableObject):
"""
Describes the properties of a cell in a merged cell and helps to
display the borders of the merged cell.
The value of a MergedCell is always None.
"""
__slots__ = ('row', 'column')
_value = None
data_type = "n"
comment = None
hyperlink = None
def __init__(self, worksheet, row=None, column=None):
super().__init__(worksheet)
self.row = row
self.column = column
def __repr__(self):
return "<MergedCell {0!r}.{1}>".format(self.parent.title, self.coordinate)
coordinate = Cell.coordinate
_comment = comment
value = _value
def WriteOnlyCell(ws=None, value=None):
return Cell(worksheet=ws, column=1, row=1, value=value)

View File

@ -0,0 +1,136 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.cell import Cell
from openpyxl.utils import get_column_letter
from openpyxl.utils.datetime import from_excel
from openpyxl.styles import is_date_format
from openpyxl.styles.numbers import BUILTIN_FORMATS, BUILTIN_FORMATS_MAX_SIZE
class ReadOnlyCell:
__slots__ = ('parent', 'row', 'column', '_value', 'data_type', '_style_id')
def __init__(self, sheet, row, column, value, data_type='n', style_id=0):
self.parent = sheet
self._value = None
self.row = row
self.column = column
self.data_type = data_type
self.value = value
self._style_id = style_id
def __eq__(self, other):
for a in self.__slots__:
if getattr(self, a) != getattr(other, a):
return
return True
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return "<ReadOnlyCell {0!r}.{1}>".format(self.parent.title, self.coordinate)
@property
def coordinate(self):
column = get_column_letter(self.column)
return "{1}{0}".format(self.row, column)
@property
def coordinate(self):
return Cell.coordinate.__get__(self)
@property
def column_letter(self):
return Cell.column_letter.__get__(self)
@property
def style_array(self):
return self.parent.parent._cell_styles[self._style_id]
@property
def has_style(self):
return self._style_id != 0
@property
def number_format(self):
_id = self.style_array.numFmtId
if _id < BUILTIN_FORMATS_MAX_SIZE:
return BUILTIN_FORMATS.get(_id, "General")
else:
return self.parent.parent._number_formats[
_id - BUILTIN_FORMATS_MAX_SIZE]
@property
def font(self):
_id = self.style_array.fontId
return self.parent.parent._fonts[_id]
@property
def fill(self):
_id = self.style_array.fillId
return self.parent.parent._fills[_id]
@property
def border(self):
_id = self.style_array.borderId
return self.parent.parent._borders[_id]
@property
def alignment(self):
_id = self.style_array.alignmentId
return self.parent.parent._alignments[_id]
@property
def protection(self):
_id = self.style_array.protectionId
return self.parent.parent._protections[_id]
@property
def is_date(self):
return Cell.is_date.__get__(self)
@property
def internal_value(self):
return self._value
@property
def value(self):
return self._value
@value.setter
def value(self, value):
if self._value is not None:
raise AttributeError("Cell is read only")
self._value = value
class EmptyCell:
__slots__ = ()
value = None
is_date = False
font = None
border = None
fill = None
number_format = None
alignment = None
data_type = 'n'
def __repr__(self):
return "<EmptyCell>"
EMPTY_CELL = EmptyCell()

View File

@ -0,0 +1,202 @@
# Copyright (c) 2010-2024 openpyxl
"""
RichText definition
"""
from copy import copy
from openpyxl.compat import NUMERIC_TYPES
from openpyxl.cell.text import InlineFont, Text
from openpyxl.descriptors import (
Strict,
String,
Typed
)
from openpyxl.xml.functions import Element, whitespace
class TextBlock(Strict):
""" Represents text string in a specific format
This class is used as part of constructing a rich text strings.
"""
font = Typed(expected_type=InlineFont)
text = String()
def __init__(self, font, text):
self.font = font
self.text = text
def __eq__(self, other):
return self.text == other.text and self.font == other.font
def __str__(self):
"""Just retun the text"""
return self.text
def __repr__(self):
font = self.font != InlineFont() and self.font or "default"
return f"{self.__class__.__name__} text={self.text}, font={font}"
def to_tree(self):
el = Element("r")
el.append(self.font.to_tree(tagname="rPr"))
t = Element("t")
t.text = self.text
whitespace(t)
el.append(t)
return el
#
# Rich Text class.
# This class behaves just like a list whose members are either simple strings, or TextBlock() instances.
# In addition, it can be initialized in several ways:
# t = CellRFichText([...]) # initialize with a list.
# t = CellRFichText((...)) # initialize with a tuple.
# t = CellRichText(node) # where node is an Element() from either lxml or xml.etree (has a 'tag' element)
class CellRichText(list):
"""Represents a rich text string.
Initialize with a list made of pure strings or :class:`TextBlock` elements
Can index object to access or modify individual rich text elements
it also supports the + and += operators between rich text strings
There are no user methods for this class
operations which modify the string will generally call an optimization pass afterwards,
that merges text blocks with identical formats, consecutive pure text strings,
and remove empty strings and empty text blocks
"""
def __init__(self, *args):
if len(args) == 1:
args = args[0]
if isinstance(args, (list, tuple)):
CellRichText._check_rich_text(args)
else:
CellRichText._check_element(args)
args = [args]
else:
CellRichText._check_rich_text(args)
super().__init__(args)
@classmethod
def _check_element(cls, value):
if not isinstance(value, (str, TextBlock, NUMERIC_TYPES)):
raise TypeError(f"Illegal CellRichText element {value}")
@classmethod
def _check_rich_text(cls, rich_text):
for t in rich_text:
CellRichText._check_element(t)
@classmethod
def from_tree(cls, node):
text = Text.from_tree(node)
if text.t:
return (text.t.replace('x005F_', ''),)
s = []
for r in text.r:
t = ""
if r.t:
t = r.t.replace('x005F_', '')
if r.rPr:
s.append(TextBlock(r.rPr, t))
else:
s.append(t)
return cls(s)
# Merge TextBlocks with identical formatting
# remove empty elements
def _opt(self):
last_t = None
l = CellRichText(tuple())
for t in self:
if isinstance(t, str):
if not t:
continue
elif not t.text:
continue
if type(last_t) == type(t):
if isinstance(t, str):
last_t += t
continue
elif last_t.font == t.font:
last_t.text += t.text
continue
if last_t:
l.append(last_t)
last_t = t
if last_t:
# Add remaining TextBlock at end of rich text
l.append(last_t)
super().__setitem__(slice(None), l)
return self
def __iadd__(self, arg):
# copy used here to create new TextBlock() so we don't modify the right hand side in _opt()
CellRichText._check_rich_text(arg)
super().__iadd__([copy(e) for e in list(arg)])
return self._opt()
def __add__(self, arg):
return CellRichText([copy(e) for e in list(self) + list(arg)])._opt()
def __setitem__(self, indx, val):
CellRichText._check_element(val)
super().__setitem__(indx, val)
self._opt()
def append(self, arg):
CellRichText._check_element(arg)
super().append(arg)
def extend(self, arg):
CellRichText._check_rich_text(arg)
super().extend(arg)
def __repr__(self):
return "CellRichText([{}])".format(', '.join((repr(s) for s in self)))
def __str__(self):
return ''.join([str(s) for s in self])
def as_list(self):
"""
Returns a list of the strings contained.
The main reason for this is to make editing easier.
"""
return [str(s) for s in self]
def to_tree(self):
"""
Return the full XML representation
"""
container = Element("is")
for obj in self:
if isinstance(obj, TextBlock):
container.append(obj.to_tree())
else:
el = Element("r")
t = Element("t")
t.text = obj
whitespace(t)
el.append(t)
container.append(el)
return container

View File

@ -0,0 +1,184 @@
# Copyright (c) 2010-2024 openpyxl
"""
Richtext definition
"""
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.descriptors import (
Alias,
Typed,
Integer,
Set,
NoneSet,
Bool,
String,
Sequence,
)
from openpyxl.descriptors.nested import (
NestedBool,
NestedInteger,
NestedString,
NestedText,
)
from openpyxl.styles.fonts import Font
class PhoneticProperties(Serialisable):
tagname = "phoneticPr"
fontId = Integer()
type = NoneSet(values=(['halfwidthKatakana', 'fullwidthKatakana',
'Hiragana', 'noConversion']))
alignment = NoneSet(values=(['noControl', 'left', 'center', 'distributed']))
def __init__(self,
fontId=None,
type=None,
alignment=None,
):
self.fontId = fontId
self.type = type
self.alignment = alignment
class PhoneticText(Serialisable):
tagname = "rPh"
sb = Integer()
eb = Integer()
t = NestedText(expected_type=str)
text = Alias('t')
def __init__(self,
sb=None,
eb=None,
t=None,
):
self.sb = sb
self.eb = eb
self.t = t
class InlineFont(Font):
"""
Font for inline text because, yes what you need are different objects with the same elements but different constraints.
"""
tagname = "RPrElt"
rFont = NestedString(allow_none=True)
charset = Font.charset
family = Font.family
b =Font.b
i = Font.i
strike = Font.strike
outline = Font.outline
shadow = Font.shadow
condense = Font.condense
extend = Font.extend
color = Font.color
sz = Font.sz
u = Font.u
vertAlign = Font.vertAlign
scheme = Font.scheme
__elements__ = ('rFont', 'charset', 'family', 'b', 'i', 'strike',
'outline', 'shadow', 'condense', 'extend', 'color', 'sz', 'u',
'vertAlign', 'scheme')
def __init__(self,
rFont=None,
charset=None,
family=None,
b=None,
i=None,
strike=None,
outline=None,
shadow=None,
condense=None,
extend=None,
color=None,
sz=None,
u=None,
vertAlign=None,
scheme=None,
):
self.rFont = rFont
self.charset = charset
self.family = family
self.b = b
self.i = i
self.strike = strike
self.outline = outline
self.shadow = shadow
self.condense = condense
self.extend = extend
self.color = color
self.sz = sz
self.u = u
self.vertAlign = vertAlign
self.scheme = scheme
class RichText(Serialisable):
tagname = "RElt"
rPr = Typed(expected_type=InlineFont, allow_none=True)
font = Alias("rPr")
t = NestedText(expected_type=str, allow_none=True)
text = Alias("t")
__elements__ = ('rPr', 't')
def __init__(self,
rPr=None,
t=None,
):
self.rPr = rPr
self.t = t
class Text(Serialisable):
tagname = "text"
t = NestedText(allow_none=True, expected_type=str)
plain = Alias("t")
r = Sequence(expected_type=RichText, allow_none=True)
formatted = Alias("r")
rPh = Sequence(expected_type=PhoneticText, allow_none=True)
phonetic = Alias("rPh")
phoneticPr = Typed(expected_type=PhoneticProperties, allow_none=True)
PhoneticProperties = Alias("phoneticPr")
__elements__ = ('t', 'r', 'rPh', 'phoneticPr')
def __init__(self,
t=None,
r=(),
rPh=(),
phoneticPr=None,
):
self.t = t
self.r = r
self.rPh = rPh
self.phoneticPr = phoneticPr
@property
def content(self):
"""
Text stripped of all formatting
"""
snippets = []
if self.plain is not None:
snippets.append(self.plain)
for block in self.formatted:
if block.t is not None:
snippets.append(block.t)
return u"".join(snippets)

View File

@ -0,0 +1,105 @@
# Copyright (c) 2010-2024 openpyxl
from openpyxl.descriptors import Typed, Alias
from openpyxl.descriptors.serialisable import Serialisable
from openpyxl.descriptors.nested import (
NestedBool,
NestedInteger,
NestedMinMax,
)
from openpyxl.descriptors.excel import ExtensionList
from .marker import PictureOptions
from .shapes import GraphicalProperties
class View3D(Serialisable):
tagname = "view3D"
rotX = NestedMinMax(min=-90, max=90, allow_none=True)
x_rotation = Alias('rotX')
hPercent = NestedMinMax(min=5, max=500, allow_none=True)
height_percent = Alias('hPercent')
rotY = NestedInteger(min=-90, max=90, allow_none=True)
y_rotation = Alias('rotY')
depthPercent = NestedInteger(allow_none=True)
rAngAx = NestedBool(allow_none=True)
right_angle_axes = Alias('rAngAx')
perspective = NestedInteger(allow_none=True)
extLst = Typed(expected_type=ExtensionList, allow_none=True)
__elements__ = ('rotX', 'hPercent', 'rotY', 'depthPercent', 'rAngAx',
'perspective',)
def __init__(self,
rotX=15,
hPercent=None,
rotY=20,
depthPercent=None,
rAngAx=True,
perspective=None,
extLst=None,
):
self.rotX = rotX
self.hPercent = hPercent
self.rotY = rotY
self.depthPercent = depthPercent
self.rAngAx = rAngAx
self.perspective = perspective
class Surface(Serialisable):
tagname = "surface"
thickness = NestedInteger(allow_none=True)
spPr = Typed(expected_type=GraphicalProperties, allow_none=True)
graphicalProperties = Alias('spPr')
pictureOptions = Typed(expected_type=PictureOptions, allow_none=True)
extLst = Typed(expected_type=ExtensionList, allow_none=True)
__elements__ = ('thickness', 'spPr', 'pictureOptions',)
def __init__(self,
thickness=None,
spPr=None,
pictureOptions=None,
extLst=None,
):
self.thickness = thickness
self.spPr = spPr
self.pictureOptions = pictureOptions
class _3DBase(Serialisable):
"""
Base class for 3D charts
"""
tagname = "ChartBase"
view3D = Typed(expected_type=View3D, allow_none=True)
floor = Typed(expected_type=Surface, allow_none=True)
sideWall = Typed(expected_type=Surface, allow_none=True)
backWall = Typed(expected_type=Surface, allow_none=True)
def __init__(self,
view3D=None,
floor=None,
sideWall=None,
backWall=None,
):
if view3D is None:
view3D = View3D()
self.view3D = view3D
if floor is None:
floor = Surface()
self.floor = floor
if sideWall is None:
sideWall = Surface()
self.sideWall = sideWall
if backWall is None:
backWall = Surface()
self.backWall = backWall
super(_3DBase, self).__init__()

View File

@ -0,0 +1,19 @@
# Copyright (c) 2010-2024 openpyxl
from .area_chart import AreaChart, AreaChart3D
from .bar_chart import BarChart, BarChart3D
from .bubble_chart import BubbleChart
from .line_chart import LineChart, LineChart3D
from .pie_chart import (
PieChart,
PieChart3D,
DoughnutChart,
ProjectedPieChart
)
from .radar_chart import RadarChart
from .scatter_chart import ScatterChart
from .stock_chart import StockChart
from .surface_chart import SurfaceChart, SurfaceChart3D
from .series_factory import SeriesFactory as Series
from .reference import Reference

Some files were not shown because too many files have changed in this diff Show More