diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 4265067..4b24281 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -161,7 +161,7 @@ jobs: with: subject-path: "wheels-*/*" - name: Publish to PyPI - if: "startsWith(github.ref, 'refs/tags/')" + if: ${{ startsWith(github.ref, 'refs/tags/') }} uses: PyO3/maturin-action@v1 env: MATURIN_USERNAME: ${{ secrets.PYPI_USERNAME }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ebd592c..9232ff1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/Cargo.lock b/Cargo.lock index 843d886..e495268 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,12 +14,6 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" -[[package]] -name = "bitflags" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - [[package]] name = "cfg-if" version = "1.0.0" @@ -53,17 +47,23 @@ dependencies = [ [[package]] name = "fast_mail_parser" -version = "0.3.0" +version = "0.4.0" dependencies = [ "mailparse", "pyo3", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "indoc" -version = "1.0.9" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" [[package]] name = "libc" @@ -71,16 +71,6 @@ version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - [[package]] name = "mailparse" version = "0.15.0" @@ -93,33 +83,25 @@ dependencies = [ ] [[package]] -name = "once_cell" -version = "1.20.2" +name = "memoffset" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] [[package]] -name = "parking_lot" -version = "0.12.3" +name = "once_cell" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] -name = "parking_lot_core" -version = "0.9.10" +name = "portable-atomic" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets", -] +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "proc-macro2" @@ -132,14 +114,16 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.16.6" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0220c44442c9b239dd4357aa856ac468a4f5e1f0df19ddb89b2522952eb4c6ca" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" dependencies = [ "cfg-if", "indoc", "libc", - "parking_lot", + "memoffset", + "once_cell", + "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", @@ -148,9 +132,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.16.6" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c819d397859445928609d0ec5afc2da5204e0d0f73d6bf9e153b04e83c9cdc2" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" dependencies = [ "once_cell", "target-lexicon", @@ -158,9 +142,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.16.6" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca882703ab55f54702d7bfe1189b41b0af10272389f04cae38fe4cd56c65f75f" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" dependencies = [ "libc", "pyo3-build-config", @@ -168,9 +152,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.16.6" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568749402955ad7be7bad9a09b8593851cd36e549ac90bfd44079cea500f3f21" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -180,11 +164,13 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.16.6" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611f64e82d98f447787e82b8e7b0ebc681e1eb78fc1252668b2c605ffb4e1eb8" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" dependencies = [ + "heck", "proc-macro2", + "pyo3-build-config", "quote", "syn", ] @@ -204,32 +190,11 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" -[[package]] -name = "redox_syscall" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" -dependencies = [ - "bitflags", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "smallvec" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" - [[package]] name = "syn" -version = "1.0.109" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ "proc-macro2", "quote", @@ -250,70 +215,6 @@ checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unindent" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/Cargo.toml b/Cargo.toml index bc6a868..95a55d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,36 +1,35 @@ [package] -name = "fast_mail_parser" -description = "Very fast Python library for .eml files parsing." -edition = "2021" -version = "0.3.0" -authors = ["Andrii Sokyrko "] -readme = "Readme.md" -license-file = "LICENSE" -homepage = "https://github.com/namecheap/fast_mail_parser" -repository = "https://github.com/namecheap/fast_mail_parser" -keywords = ["parser", "email", "rfc822", "mime", "maildir"] -categories = ["email", "parsing"] -publish = false + authors = [ "Andrii Sokyrko " ] + categories = [ "email", "parsing" ] + description = "Very fast Python library for .eml files parsing." + edition = "2021" + homepage = "https://github.com/namecheap/fast_mail_parser" + keywords = [ "email", "maildir", "mime", "parser", "rfc822" ] + license-file = "LICENSE" + name = "fast_mail_parser" + publish = false + readme = "Readme.md" + repository = "https://github.com/namecheap/fast_mail_parser" + version = "0.4.0" [profile.dev] -opt-level = 0 -debug = true + debug = true + opt-level = 0 [profile.release] -opt-level = 3 -debug = false -strip = "debuginfo" -lto = true -codegen-units = 1 + codegen-units = 1 + debug = false + lto = true + opt-level = 3 + strip = "debuginfo" [lib] -name = "fast_mail_parser" -path = "src/fast_mail_parser.rs" -crate-type = ["cdylib"] + crate-type = [ "cdylib" ] + name = "_core" [dependencies] -mailparse = "0.15.0" -pyo3 = "0.16.6" + mailparse = "0.15.0" + pyo3 = { features = [ "extension-module" ], version = "0.22.0" } [features] -default = ["pyo3/extension-module"] + default = [ "pyo3/extension-module" ] diff --git a/pyproject.toml b/pyproject.toml index 25cec93..18a9a10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,42 +1,47 @@ +[project.urls] + Homepage = "https://github.com/namecheap/fast_mail_parser" + Issues = "https://github.com/namecheap/fast_mail_parser/issues" + Repository = "https://github.com/namecheap/fast_mail_parser.git" + [project] -name = "fast_mail_parser" -version = "0.3.0" -description = "Very fast Python library for .eml files parsing." -authors = [{ name = "Andrii Sokyrko", email = "wartwvister@gmail.com" }] -keywords = ["parser", "email", "rfc822", "mime", "maildir"] -requires-python = ">= 3.7" -readme = "Readme.md" -license = { file = "LICENSE" } -classifiers = [ - "License :: OSI Approved :: Apache Software License", - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Programming Language :: Python", - "Programming Language :: Rust", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Operating System :: POSIX", - "Operating System :: Unix", - "Topic :: Communications :: Email", - "Topic :: Software Development :: Libraries", -] -dependencies = ["toml==0.10.2", "wheel", "maturin>=1.0,<2.0"] - -[project.optional-dependencies] -test = ["pytest~=7.4.4", "pytest-benchmark==4.0.0", "mail-parser==3.15.0"] + description = "Very fast Python library for .eml files parsing." + license = { file = "LICENSE" } + name = "fast_mail_parser" + readme = "Readme.md" + requires-python = ">= 3.9" + version = "0.4.0" -[project.urls] -Homepage = "https://github.com/namecheap/fast_mail_parser" -Repository = "https://github.com/namecheap/fast_mail_parser.git" -Issues = "https://github.com/namecheap/fast_mail_parser/issues" + keywords = [ "email", "maildir", "mime", "parser", "rfc822" ] + + authors = [ { email = "wartwvister@gmail.com", name = "Andrii Sokyrko" } ] + + classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX", + "Operating System :: Unix", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python", + "Programming Language :: Rust", + "Topic :: Communications :: Email", + "Topic :: Software Development :: Libraries", + ] + + dependencies = [ ] + +[dependency-groups] + test = [ "mail-parser>=4.1.4", "pytest-benchmark>=5.1.0", "pytest>=8.4.1" ] [build-system] -requires = ["maturin>=1.0,<2.0"] -build-backend = "maturin" + build-backend = "maturin" + requires = [ "maturin>=1.0,<2.0" ] [tool.maturin] -sdist-include = ["src/*"] + module-name = "fast_mail_parser._core" + python-packages = [ "fast_mail_parser" ] + python-source = "src" + sdist-include = [ "src/*" ] diff --git a/src/fast_mail_parser.rs b/src/fast_mail_parser.rs deleted file mode 100644 index bb8862a..0000000 --- a/src/fast_mail_parser.rs +++ /dev/null @@ -1,106 +0,0 @@ -mod mail_parser; - -use pyo3::prelude::*; -use pyo3::types::PyBytes; -use pyo3::{create_exception, exceptions, wrap_pyfunction}; -use std::collections::HashMap; - -create_exception!(fast_mail_parser, ParseError, exceptions::PyException); - -#[pyclass] -#[derive(Clone)] -pub struct PyAttachment { - #[pyo3(get)] - pub mimetype: String, - #[pyo3(get)] - pub content: Py, - #[pyo3(get)] - pub filename: String, -} - -impl PyAttachment { - pub(crate) fn from_attachment(py: Python, attachment: mail_parser::Attachment) -> Self { - PyAttachment { - mimetype: attachment.mimetype, - content: Py::from(PyBytes::new(py, attachment.content.as_slice())), - filename: attachment.filename, - } - } -} - -#[pyclass] -pub struct PyMail { - #[pyo3(get)] - pub subject: String, - #[pyo3(get)] - pub text_plain: Vec, - #[pyo3(get)] - pub text_html: Vec, - #[pyo3(get)] - pub date: String, - #[pyo3(get)] - pub attachments: Vec, - #[pyo3(get)] - pub headers: HashMap, -} - -impl PyMail { - pub(crate) fn from_mail(py: Python, mail: mail_parser::Mail) -> Self { - Self { - subject: mail.subject, - text_plain: mail.text_plain, - text_html: mail.text_html, - date: mail.date, - attachments: mail - .attachments - .into_iter() - .map(|a| PyAttachment::from_attachment(py, a)) - .collect(), - headers: mail.headers, - } - } -} - -trait PyToBytes { - fn to_bytes(&self, py: Python) -> PyResult>; -} - -impl PyToBytes for PyObject { - fn to_bytes(&self, py: Python) -> PyResult> { - let mut result = self - .extract::<&PyBytes>(py) - .map(|s| s.as_bytes().to_vec().into_iter()); - - if result.is_err() { - result = self - .extract::(py) - .map(|s| s.chars().map(|c| c as u8).collect::>().into_iter()) - .map_err(|_| { - PyErr::new::( - "The argument cannot be interpreted as bytes.", - ) - }) - } - - result.map(|iter| iter.collect()) - } -} - -#[pyfunction] -pub fn parse_email(py: Python, payload: PyObject) -> PyResult { - let message = payload.to_bytes(py)?; - - mail_parser::parse_email(message.as_slice()) - .map_err(|e| ParseError::new_err(format!("Message parsing error: {}", e))) - .map(|mail| PyMail::from_mail(py, mail)) -} - -#[pymodule] -fn fast_mail_parser(py: Python, m: &PyModule) -> PyResult<()> { - m.add_wrapped(wrap_pyfunction!(parse_email))?; - m.add_class::()?; - m.add_class::()?; - m.add("ParseError", py.get_type::())?; - - Ok(()) -} diff --git a/fast_mail_parser/__init__.py b/src/fast_mail_parser/__init__.py similarity index 53% rename from fast_mail_parser/__init__.py rename to src/fast_mail_parser/__init__.py index 240b07b..44e4e26 100644 --- a/fast_mail_parser/__init__.py +++ b/src/fast_mail_parser/__init__.py @@ -1,8 +1,8 @@ -from .fast_mail_parser import parse_email, ParseError, PyMail, PyAttachment +from ._core import ParseError, PyAttachment, PyMail, parse_email __all__ = [ - "parse_email", "ParseError", - "PyMail", "PyAttachment", + "PyMail", + "parse_email", ] diff --git a/fast_mail_parser/__init__.pyi b/src/fast_mail_parser/_core.pyi similarity index 100% rename from fast_mail_parser/__init__.pyi rename to src/fast_mail_parser/_core.pyi diff --git a/fast_mail_parser/py.typed b/src/fast_mail_parser/py.typed similarity index 100% rename from fast_mail_parser/py.typed rename to src/fast_mail_parser/py.typed diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c68bed5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,193 @@ +use mailparse::*; +use pyo3::prelude::*; +use pyo3::types::PyBytes; +use pyo3::{create_exception, exceptions, wrap_pyfunction}; +use std::collections::HashMap; + +create_exception!(fast_mail_parser, ParseError, exceptions::PyException); + +pub(crate) fn parse_email_raw(payload: &[u8]) -> Result { + Mail::new(payload) +} + +#[derive(Debug)] +pub(crate) struct Mail { + pub(crate) subject: String, + pub(crate) text_plain: Vec, + pub(crate) text_html: Vec, + pub(crate) date: String, + pub(crate) attachments: Vec, + pub(crate) headers: HashMap, +} + +#[derive(Debug)] +pub(crate) struct Attachment { + pub(crate) mimetype: String, + pub(crate) content: Vec, + pub(crate) filename: String, +} + +impl<'a> Mail { + pub(crate) fn new(payload: &'a [u8]) -> Result { + let mail = parse_mail(payload)?; + + let headers: HashMap = mail + .get_headers() + .into_iter() + .map(|h| (h.get_key(), h.get_value())) + .collect(); + + let subject = headers.get("Subject").map(String::from).unwrap_or_default(); + + let date = headers.get("Date").map(String::from).unwrap_or_default(); + + let mut attachments = vec![]; + let mut text_plain = vec![]; + let mut text_html = vec![]; + + for mail in Self::extract_mail_parts(mail) { + let attachment_name = mail.ctype.params.get("name"); + let mime = mail.ctype.mimetype.as_str(); + + attachments.push(Attachment { + mimetype: mime.to_string(), + content: mail.get_body_raw().unwrap_or_default(), + filename: attachment_name.cloned().unwrap_or_default(), + }); + + if attachment_name.is_none() { + if mime == "text/plain" { + text_plain.push(mail.get_body().unwrap_or_default()) + } else if mime == "text/html" { + text_html.push(mail.get_body().unwrap_or_default()) + } + } + } + + Ok(Self { + subject, + text_plain, + text_html, + date, + attachments, + headers, + }) + } + + fn extract_mail_parts(mut mail: ParsedMail<'a>) -> Vec> { + let mut result = vec![]; + let subparts = std::mem::take(&mut mail.subparts); + + for part in subparts { + result.extend(Self::extract_mail_parts(part)); + } + + result.push(mail); + + result + } +} + +#[pyclass] +pub struct PyAttachment { + #[pyo3(get)] + pub mimetype: String, + #[pyo3(get)] + pub content: Py, + #[pyo3(get)] + pub filename: String, +} + +impl Clone for PyAttachment { + fn clone(&self) -> Self { + Python::with_gil(|py| PyAttachment { + mimetype: self.mimetype.clone(), + content: self.content.clone_ref(py), + filename: self.filename.clone(), + }) + } +} + +impl PyAttachment { + pub(crate) fn from_attachment(py: Python, attachment: Attachment) -> Self { + PyAttachment { + mimetype: attachment.mimetype, + content: PyBytes::new_bound(py, attachment.content.as_slice()).into(), + filename: attachment.filename, + } + } +} + +#[pyclass] +pub struct PyMail { + #[pyo3(get)] + pub subject: String, + #[pyo3(get)] + pub text_plain: Vec, + #[pyo3(get)] + pub text_html: Vec, + #[pyo3(get)] + pub date: String, + #[pyo3(get)] + pub attachments: Vec, + #[pyo3(get)] + pub headers: HashMap, +} + +impl PyMail { + pub(crate) fn from_mail(py: Python, mail: Mail) -> Self { + Self { + subject: mail.subject, + text_plain: mail.text_plain, + text_html: mail.text_html, + date: mail.date, + attachments: mail + .attachments + .into_iter() + .map(|a| PyAttachment::from_attachment(py, a)) + .collect(), + headers: mail.headers, + } + } +} + +trait PyToBytes { + fn to_bytes(&self, py: Python) -> PyResult>; +} + +impl PyToBytes for Bound<'_, PyAny> { + fn to_bytes(&self, _py: Python) -> PyResult> { + if let Ok(bytes) = self.downcast::() { + return Ok(bytes.as_bytes().to_vec()); + } + + if let Ok(string) = self.extract::() { + return Ok(string.into_bytes()); + } + + Err(PyErr::new::( + "The argument cannot be interpreted as bytes.", + )) + } +} + +#[pyfunction] +pub fn parse_email(py: Python, payload: Bound<'_, PyAny>) -> PyResult { + let message = payload.to_bytes(py)?; + + parse_email_raw(message.as_slice()) + .map_err(|e| ParseError::new_err(format!("Message parsing error: {}", e))) + .map(|mail| PyMail::from_mail(py, mail)) +} + +/// A Python module implemented in Rust. The name of this function must match +/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to +/// import the module. +#[pymodule] +fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(parse_email, m)?)?; + m.add_class::()?; + m.add_class::()?; + m.add("ParseError", m.py().get_type_bound::())?; + Ok(()) +} diff --git a/src/mail_parser.rs b/src/mail_parser.rs deleted file mode 100644 index a778eb9..0000000 --- a/src/mail_parser.rs +++ /dev/null @@ -1,90 +0,0 @@ -use mailparse::*; -use std::collections::HashMap; - -pub(crate) fn parse_email(payload: &[u8]) -> Result { - Mail::new(payload) -} - -#[derive(Debug)] -pub(crate) struct Mail { - pub(crate) subject: String, - pub(crate) text_plain: Vec, - pub(crate) text_html: Vec, - pub(crate) date: String, - pub(crate) attachments: Vec, - pub(crate) headers: HashMap, -} - -#[derive(Debug)] -pub(crate) struct Attachment { - pub(crate) mimetype: String, - pub(crate) content: Vec, - pub(crate) filename: String, -} - -impl<'a> Mail { - pub(crate) fn new(payload: &'a [u8]) -> Result { - let mail = parse_mail(payload)?; - - let headers: HashMap = mail - .get_headers() - .into_iter() - .map(|h| (h.get_key(), h.get_value())) - .collect(); - - let subject = headers - .get("Subject") - .map(String::from) - .unwrap_or_default(); - - let date = headers - .get("Date") - .map(String::from) - .unwrap_or_default(); - - let mut attachments = vec![]; - let mut text_plain = vec![]; - let mut text_html = vec![]; - - for mail in Self::extract_mail_parts(mail) { - let attachment_name = mail.ctype.params.get("name"); - let mime = mail.ctype.mimetype.as_str(); - - attachments.push(Attachment { - mimetype: mime.to_string(), - content: mail.get_body_raw().unwrap_or_default(), - filename: attachment_name.cloned().unwrap_or_default(), - }); - - if attachment_name.is_none() { - if mime == "text/plain" { - text_plain.push(mail.get_body().unwrap_or_default()) - } else if mime == "text/html" { - text_html.push(mail.get_body().unwrap_or_default()) - } - } - } - - Ok(Self { - subject, - text_plain, - text_html, - date, - attachments, - headers, - }) - } - - fn extract_mail_parts(mut mail: ParsedMail<'a>) -> Vec> { - let mut result = vec![]; - let subparts = std::mem::take(&mut mail.subparts); - - for part in subparts { - result.extend(Self::extract_mail_parts(part)); - } - - result.push(mail); - - result - } -} diff --git a/taplo.toml b/taplo.toml new file mode 100644 index 0000000..20dd224 --- /dev/null +++ b/taplo.toml @@ -0,0 +1,20 @@ +[formatting] + align_comments = true + align_entries = true + allowed_blank_lines = 1 + array_auto_collapse = false + array_auto_expand = true + array_trailing_comma = true + column_width = 100 + compact_arrays = false + compact_entries = false + compact_inline_tables = false + crlf = false + indent_entries = true + indent_string = " " + indent_tables = true + inline_table_expand = true + reorder_arrays = true + reorder_inline_tables = true + reorder_keys = true + trailing_newline = true diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..6e64801 --- /dev/null +++ b/uv.lock @@ -0,0 +1,190 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "fast-mail-parser" +version = "0.4.0" +source = { editable = "." } + +[package.dev-dependencies] +test = [ + { name = "mail-parser" }, + { name = "pytest" }, + { name = "pytest-benchmark" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +test = [ + { name = "mail-parser", specifier = ">=4.1.4" }, + { name = "pytest", specifier = ">=8.4.1" }, + { name = "pytest-benchmark", specifier = ">=5.1.0" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "mail-parser" +version = "4.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/57/20176377e709ea57481f559159f00b366d143cc0a8cf7271b7f0e9c9c7ff/mail_parser-4.1.4.tar.gz", hash = "sha256:b65abad3beee3ffb75c7851bc373412c69e1a22561d49dd7142ee7fb4321ed82", size = 27193, upload-time = "2025-06-25T22:14:20.453Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/e1/23f6629cab94f740ec8c4827742ebd8ef7b3b760cb3dea6e92349289d25c/mail_parser-4.1.4-py3-none-any.whl", hash = "sha256:ec2f7996792d81c0beb174a14795865f9c0f09f47c8afc7b9f7c1240f59d71c2", size = 27077, upload-time = "2025-06-25T22:14:19.529Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + +[[package]] +name = "pytest-benchmark" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "py-cpuinfo" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/d0/a8bd08d641b393db3be3819b03e2d9bb8760ca8479080a26a5f6e540e99c/pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105", size = 337810, upload-time = "2024-10-30T11:51:48.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/d6/b41653199ea09d5969d4e385df9bbfd9a100f28ca7e824ce7c0a016e3053/pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89", size = 44259, upload-time = "2024-10-30T11:51:45.94Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +]