pax_global_header00006660000000000000000000000064145713375220014523gustar00rootroot0000000000000052 comment=f516cdff15434f9d0c1e348edab2c68285fcc5fb rdata-0.11.2/000077500000000000000000000000001457133752200126775ustar00rootroot00000000000000rdata-0.11.2/.all-contributorsrc000066400000000000000000000060631457133752200165350ustar00rootroot00000000000000{ "projectName": "rdata", "projectOwner": "VNMabus", "repoType": "github", "repoHost": "https://github.com", "files": [ "CONTRIBUTORS.md" ], "imageSize": 100, "commit": false, "commitConvention": "none", "contributors": [ { "login": "vnmabus", "name": "Carlos Ramos Carreño", "avatar_url": "https://avatars.githubusercontent.com/u/2364173?v=4", "profile": "https://github.com/vnmabus", "contributions": [ "code", "data", "doc", "example", "ideas", "infra", "maintenance", "projectManagement", "question", "review", "test", "tutorial" ] }, { "login": "", "name": "CSC - IT Center for Science Ltd", "avatar_url": "https://avatars.githubusercontent.com/u/5947494?v=4", "profile": "https://www.csc.fi", "contributions": [ { "type": "code", "url": "https://github.com/vnmabus/rdata/commits?author=trossi" } ] }, { "login": "trossi", "name": "Tuomas Rossi", "avatar_url": "https://avatars.githubusercontent.com/u/34502776?v=4", "profile": "https://github.com/trossi", "contributions": [ "code", "ideas", "bug" ] }, { "login": "VolodyaCO", "name": "Vladimir Vargas-Calderón", "avatar_url": "https://avatars.githubusercontent.com/u/31494271?v=4", "profile": "https://www.researchgate.net/profile/Vladimir_Vargas-Calderon", "contributions": [ "bug" ] }, { "login": "Jorgelindo238", "name": "Jorgelindo", "avatar_url": "https://avatars.githubusercontent.com/u/79350063?v=4", "profile": "https://jorgelindodaveiga.myportfolio.com/", "contributions": [ "bug" ] }, { "login": "zoj613", "name": "zoj613", "avatar_url": "https://avatars.githubusercontent.com/u/44142765?v=4", "profile": "https://github.com/zoj613", "contributions": [ "bug" ] }, { "login": "schlegelp", "name": "Philipp Schlegel", "avatar_url": "https://avatars.githubusercontent.com/u/7161148?v=4", "profile": "https://github.com/schlegelp", "contributions": [ "bug" ] }, { "login": "deeenes", "name": "deeenes", "avatar_url": "https://avatars.githubusercontent.com/u/2679889?v=4", "profile": "https://denes.omnipathdb.org/", "contributions": [ "bug" ] }, { "login": "soheila-sahami", "name": "Soheila", "avatar_url": "https://avatars.githubusercontent.com/u/9429831?v=4", "profile": "https://github.com/soheila-sahami", "contributions": [ "ideas" ] }, { "login": "userLUX", "name": "userLUX", "avatar_url": "https://avatars.githubusercontent.com/u/107994632?v=4", "profile": "https://github.com/userLUX", "contributions": [ "bug" ] } ], "contributorsPerLine": 7, "linkToUsage": true } rdata-0.11.2/.gitattributes000066400000000000000000000002351457133752200155720ustar00rootroot00000000000000# Mark rda and rds files as binary. # Otherwise git might change the line endings of # ascii-formatted files, which breaks the tests *.rda -text *.rds -text rdata-0.11.2/.github/000077500000000000000000000000001457133752200142375ustar00rootroot00000000000000rdata-0.11.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001457133752200164225ustar00rootroot00000000000000rdata-0.11.2/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000071611457133752200213220ustar00rootroot00000000000000name: Bug report description: Create a report to help us reproduce and fix a bug labels: [bug] body: - type: markdown attributes: value: > #### Please check that the bug has not been previously notified before submitting, by searching through the [issues list](https://github.com/vnmabus/rdata/issues). - type: textarea attributes: label: Bug description summary description: > Please describe the bug in a brief paragraph(s). Be clear and concise. validations: required: true - type: textarea attributes: label: Code to reproduce the bug description: | Please add a minimal code example that can reproduce the error. If the bug does not require more code than loading a data file you can leave this empty. This will be automatically converted to a Python block. placeholder: | import rdata parsed = rdata.parser.parse_file("data.rda") converted = rdata.conversion.convert(parsed) converted render: Python - type: textarea attributes: label: Data file(s) description: > If the bug was caused by loading a particular data file, please attach it or paste a link to it here. - type: textarea attributes: label: Expected result description: > Paste or describe the result that you expected here. validations: required: true - type: textarea attributes: label: Actual result description: > Paste or describe the result that you obtained here. If the code raises an error, you can past it in the next field. validations: required: true - type: textarea attributes: label: Traceback (if an exception is raised) description: | If an exception is raised, copy and paste the traceback here. placeholder: | FileNotFoundError Traceback (most recent call last) Cell In[5], line 3 1 import rdata ----> 3 parsed = rdata.parser.parse_file("data.rda") 4 converted = rdata.conversion.convert(parsed) 5 converted File .../rdata/parser/_parser.py:1139, in parse_file(file_or_path, expand_altrep, altrep_constructor_dict, extension) 1137 if extension is None: 1138 extension = getattr(path, "suffix", None) -> 1139 data = path.read_bytes() 1141 return parse_data( 1142 data, 1143 expand_altrep=expand_altrep, 1144 altrep_constructor_dict=altrep_constructor_dict, 1145 extension=extension, 1146 ) File .../pathlib.py:1050, in Path.read_bytes(self) 1046 def read_bytes(self): 1047 """ 1048 Open the file in bytes mode, read it, and close the file. 1049 """ -> 1050 with self.open(mode='rb') as f: 1051 return f.read() File .../pathlib.py:1044, in Path.open(self, mode, buffering, encoding, errors, newline) 1042 if "b" not in mode: 1043 encoding = io.text_encoding(encoding) -> 1044 return io.open(self, mode, buffering, encoding, errors, newline) FileNotFoundError: [Errno 2] No such file or directory: 'data.rda' render: Python - type: textarea attributes: label: Software versions description: > Include the version of the library used (obtained with `rdata.__version__`). If relevant, you can include here the OS version and versions of related software. placeholder: | rdata version: 0.10.0 OS: Windows 10 validations: required: true - type: textarea attributes: label: Additional context description: > Add any other context about the problem here. rdata-0.11.2/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000020011457133752200223410ustar00rootroot00000000000000name: Feature request description: Suggest an idea for this project labels: [enhancement] body: - type: markdown attributes: value: > #### Please check that this idea has not been proposed previously, by searching through the [issues list](https://github.com/vnmabus/rdata/issues). - type: textarea attributes: label: Motivation description: > A clear and concise description of what the problem is. Ex. I am always frustrated when [...] validations: required: true - type: textarea attributes: label: Desired functionality description: > A clear and concise description of what you want to happen. validations: required: true - type: textarea attributes: label: Alternatives description: > A clear and concise description of any alternative solutions or features you have considered. validations: required: false - type: textarea attributes: label: Additional context description: > Add any other context about the problem here. rdata-0.11.2/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000022301457133752200200350ustar00rootroot00000000000000 ## References to issues or other PRs ## Describe the proposed changes ## Additional information ## Checklist before requesting a review - [ ] I have performed a self-review of my code - [ ] The code conforms to the style used in this package (checked with [Ruff](https://docs.astral.sh/ruff/)) - [ ] The code is fully documented and typed (type-checked with [Mypy](https://mypy-lang.org/)) - [ ] I have added thorough tests for the new/changed functionality rdata-0.11.2/.github/workflows/000077500000000000000000000000001457133752200162745ustar00rootroot00000000000000rdata-0.11.2/.github/workflows/main.yml000066400000000000000000000015441457133752200177470ustar00rootroot00000000000000name: Tests on: push: pull_request: jobs: build: runs-on: ${{ matrix.os }} name: Python ${{ matrix.python-version }} on ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | pip3 install pytest-cov || pip3 install --user pytest-cov; - name: Run tests run: | pip3 debug --verbose . pip3 install ".[test]" pytest --cov=rdata/ --cov-report=xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 rdata-0.11.2/.github/workflows/mypy.yml000066400000000000000000000011761457133752200200220ustar00rootroot00000000000000name: Mypy on: pull_request: jobs: build: runs-on: ubuntu-latest name: Mypy steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.11" - name: Install dependencies run: | pip3 install ".[test,typing]" mypy; rm -rf build; - uses: tsuyoshicho/action-mypy@v4 with: github_token: ${{ secrets.github_token }} reporter: github-pr-review install_types: false # The action will output fail if there are mypy errors level: error filter_mode: nofilterrdata-0.11.2/.github/workflows/python-publish.yml000066400000000000000000000021031457133752200220000ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} rdata-0.11.2/.github/workflows/ruff.yml000066400000000000000000000003161457133752200177610ustar00rootroot00000000000000name: Ruff on: [push] jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: chartboost/ruff-action@v1 with: args: check --output-format githubrdata-0.11.2/.gitignore000066400000000000000000000023111457133752200146640ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # ruff /.ruff_cache/ rdata-0.11.2/CITATION.cff000066400000000000000000000013761457133752200146000ustar00rootroot00000000000000cff-version: 1.2.0 message: "If you use this software, please cite it as below." authors: - family-names: "Ramos-Carreño" given-names: "Carlos" orcid: "https://orcid.org/0000-0003-2566-7058" affiliation: "Universidad Autónoma de Madrid" email: vnmabus@gmail.com title: "rdata: Read R datasets from Python" date-released: 2022-03-24 doi: 10.5281/zenodo.6382237 url: "https://github.com/vnmabus/rdata" license: MIT keywords: - rdata - Python - R - parser - conversion identifiers: - description: "This is the collection of archived snapshots of all versions of rdata" type: doi value: 10.5281/zenodo.6382237 - description: "This is the archived snapshot of version 0.7 of rdata" type: doi value: 10.5281/zenodo.6382238rdata-0.11.2/CODE_OF_CONDUCT.md000066400000000000000000000121531457133752200155000ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at carlosramosca@hotmail.com. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. rdata-0.11.2/CONTRIBUTING.md000066400000000000000000000031161457133752200151310ustar00rootroot00000000000000# How to contribute Thank you for considering contributing to rdata! You can contribute to rdata in several ways. ## Opening issues Open a new issue if you find some bug or you want to propose a new feature. Please first check that nobody has asked that already! Make sure that your proposal is clearly written, preferably in English. In case you are reporting a bug, please include all relevant information, such as the software version and machine information. ## Discussing the project You can open a discussion for any topic related with this package. Do you have doubts about how to use the package? Open a discussion! Do you want to show related projects, recent research or some use case for this software? Open a discussion! You are also encouraged to answer the discussions of other users and participate actively in the discussions forum. ## Improving the documentation Do you feel that the documentation could be more clear? Did you found a typo? You can easily edit the documentation and make a pull request clicking the "Edit this page" link in the documentation. Advanced users can also propose the addition of new pages and examples. In case you want to do that, please open an issue to discuss that first. ## Contributing software You can improve this package by adding new functionality, solving pending bugs or implementing accepted feature requests. Please discuss that first to ensure that it will be accepted and to assign that to you and prevent duplicated efforts. In any case, make sure that you own the rights to the software and are ok with releasing it under a MIT license. rdata-0.11.2/CONTRIBUTORS.md000066400000000000000000000124531457133752200151630ustar00rootroot00000000000000 ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
Carlos Ramos Carreño
Carlos Ramos Carreño

💻 🔣 📖 💡 🤔 🚇 🚧 📆 💬 👀 ⚠️
CSC - IT Center for Science Ltd
CSC - IT Center for Science Ltd

💻
Tuomas Rossi
Tuomas Rossi

💻 🤔 🐛
Vladimir Vargas-Calderón
Vladimir Vargas-Calderón

🐛
Jorgelindo
Jorgelindo

🐛
zoj613
zoj613

🐛
Philipp Schlegel
Philipp Schlegel

🐛
deeenes
deeenes

🐛
Soheila
Soheila

🤔
userLUX
userLUX

🐛
Add your contributions
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!rdata-0.11.2/LICENSE000066400000000000000000000020621457133752200137040ustar00rootroot00000000000000MIT License Copyright (c) 2018 Rdata developers. 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. rdata-0.11.2/MANIFEST.in000066400000000000000000000001621457133752200144340ustar00rootroot00000000000000include MANIFEST.in include LICENSE include rdata/py.typed include *.txt global-include *.rda global-include *.rdsrdata-0.11.2/README.rst000066400000000000000000000177421457133752200144010ustar00rootroot00000000000000rdata ===== |build-status| |docs| |coverage| |repostatus| |versions| |pypi| |conda| |zenodo| |pyOpenSci| Read R datasets from Python. .. Github does not support include in README for dubious security reasons, so we copy-paste instead. Also Github does not understand Sphinx directives. .. include:: docs/index.rst .. include:: docs/simpleusage.rst The package rdata offers a lightweight way to import R datasets/objects stored in the ".rda" and ".rds" formats into Python. Its main advantages are: - It is a pure Python implementation, with no dependencies on the R language or related libraries. Thus, it can be used anywhere where Python is supported, including the web using `Pyodide `__. - It attempt to support all R objects that can be meaningfully translated. As opposed to other solutions, you are no limited to import dataframes or data with a particular structure. - It allows users to easily customize the conversion of R classes to Python ones. Does your data use custom R classes? Worry no longer, as it is possible to define custom conversions to the Python classes of your choosing. - It has a permissive license (MIT). As opposed to other packages that depend on R libraries and thus need to adhere to the GPL license, you can use rdata as a dependency on MIT, BSD or even closed source projects. Installation ============ rdata is on PyPi and can be installed using :code:`pip`: .. code:: pip install rdata It is also available for :code:`conda` using the :code:`conda-forge` channel: .. code:: conda install -c conda-forge rdata Installing the develop version ------------------------------ The current version from the develop branch can be installed as .. code:: pip install git+https://github.com/vnmabus/rdata.git@develop Documentation ============= The documentation of rdata is in `ReadTheDocs `__. Examples ======== Examples of use are available in `ReadTheDocs `__. Simple usage ============ Read a R dataset ---------------- The common way of reading an R dataset is the following one: .. code:: python import rdata converted = rdata.read_rda(rdata.TESTDATA_PATH / "test_vector.rda") converted which results in .. code:: {'test_vector': array([1., 2., 3.])} Under the hood, this is equivalent to the following code: .. code:: python import rdata parsed = rdata.parser.parse_file(rdata.TESTDATA_PATH / "test_vector.rda") converted = rdata.conversion.convert(parsed) converted This consists on two steps: #. First, the file is parsed using the function `rdata.parser.parse_file `__. This provides a literal description of the file contents as a hierarchy of Python objects representing the basic R objects. This step is unambiguous and always the same. #. Then, each object must be converted to an appropriate Python object. In this step there are several choices on which Python type is the most appropriate as the conversion for a given R object. Thus, we provide a default `rdata.conversion.convert `__ routine, which tries to select Python objects that preserve most information of the original R object. For custom R classes, it is also possible to specify conversion routines to Python objects. Convert custom R classes ------------------------ The basic `convert `__ routine only constructs a `SimpleConverter `__ object and calls its `convert `__ method. All arguments of `convert `__ are directly passed to the `SimpleConverter `__ initialization method. It is possible, although not trivial, to make a custom `Converter `__ object to change the way in which the basic R objects are transformed to Python objects. However, a more common situation is that one does not want to change how basic R objects are converted, but instead wants to provide conversions for specific R classes. This can be done by passing a dictionary to the `SimpleConverter `__ initialization method, containing as keys the names of R classes and as values, callables that convert a R object of that class to a Python object. By default, the dictionary used is `DEFAULT_CLASS_MAP `__, which can convert commonly used R classes such as `data.frame `__ and `factor `__. As an example, here is how we would implement a conversion routine for the factor class to `bytes `__ objects, instead of the default conversion to Pandas `Categorical `__ objects: .. code:: python import rdata def factor_constructor(obj, attrs): values = [bytes(attrs['levels'][i - 1], 'utf8') if i >= 0 else None for i in obj] return values new_dict = { **rdata.conversion.DEFAULT_CLASS_MAP, "factor": factor_constructor } converted = rdata.read_rda( rdata.TESTDATA_PATH / "test_dataframe.rda", constructor_dict=new_dict, ) converted which has the following result: .. code:: {'test_dataframe': class value 1 b'a' 1 2 b'b' 2 3 b'b' 3} Additional examples =================== Additional examples illustrating the functionalities of this package can be found in the `ReadTheDocs documentation `__. .. |build-status| image:: https://github.com/vnmabus/rdata/actions/workflows/main.yml/badge.svg?branch=master :alt: build status :scale: 100% :target: https://github.com/vnmabus/rdata/actions/workflows/main.yml .. |docs| image:: https://readthedocs.org/projects/rdata/badge/?version=latest :alt: Documentation Status :scale: 100% :target: https://rdata.readthedocs.io/en/latest/?badge=latest .. |coverage| image:: http://codecov.io/github/vnmabus/rdata/coverage.svg?branch=develop :alt: Coverage Status :scale: 100% :target: https://codecov.io/gh/vnmabus/rdata/branch/develop .. |repostatus| image:: https://www.repostatus.org/badges/latest/active.svg :alt: Project Status: Active – The project has reached a stable, usable state and is being actively developed. :target: https://www.repostatus.org/#active .. |versions| image:: https://img.shields.io/pypi/pyversions/rdata :alt: PyPI - Python Version :scale: 100% .. |pypi| image:: https://badge.fury.io/py/rdata.svg :alt: Pypi version :scale: 100% :target: https://pypi.python.org/pypi/rdata/ .. |conda| image:: https://anaconda.org/conda-forge/rdata/badges/version.svg :alt: Conda version :scale: 100% :target: https://anaconda.org/conda-forge/rdata .. |zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.6382237.svg :alt: Zenodo DOI :scale: 100% :target: https://doi.org/10.5281/zenodo.6382237 .. |pyOpenSci| image:: https://tinyurl.com/y22nb8up :alt: pyOpenSci: Peer reviewed :scale: 100% :target: https://github.com/pyOpenSci/software-submission/issues/144 rdata-0.11.2/asv_benchmarks/000077500000000000000000000000001457133752200156655ustar00rootroot00000000000000rdata-0.11.2/asv_benchmarks/.gitignore000066400000000000000000000000721457133752200176540ustar00rootroot00000000000000*__pycache__* env/ html/ results/ rdata/ benchmarks/cache/rdata-0.11.2/asv_benchmarks/asv.conf.json000066400000000000000000000003641457133752200203000ustar00rootroot00000000000000{ "version": 1, "project": "rdata", "project_url": "https://rdata.readthedocs.io/", "repo": "..", "branches": ["develop"], "environment_type": "conda", "show_commit_url": "http://github.com/vnmabus/rdata/commit/" } rdata-0.11.2/asv_benchmarks/benchmarks/000077500000000000000000000000001457133752200200025ustar00rootroot00000000000000rdata-0.11.2/asv_benchmarks/benchmarks/__init__.py000066400000000000000000000000331457133752200221070ustar00rootroot00000000000000"""ASV benchmark suite.""" rdata-0.11.2/asv_benchmarks/benchmarks/array_parsing.py000066400000000000000000000015131457133752200232150ustar00rootroot00000000000000"""Benchmarks for array parsing time.""" import rdata from rdata.testing import execute_r_data_source class TimeArrayParsing: """ A test for the time that it takes to parse arrays of different sizes. The following R code is used to create arrays of different sizes: ::: for (i in 1:MAX_TESTS) { ::: n = 2^i * 1024^2 ::: saveRDS( ::: runif(n), ::: file=sprintf("array_%s.rds", i), ::: compress=FALSE, ::: ) ::: } """ MAX_TESTS = 5 params = range(MAX_TESTS) def setup_cache(self) -> None: """Initialize the data.""" execute_r_data_source(self, MAX_TESTS=self.MAX_TESTS) def time_array(self, i: int) -> None: """Test the time that it takes to parse an array.""" rdata.parser.parse_file(f"array_{i + 1}.rds") rdata-0.11.2/docs/000077500000000000000000000000001457133752200136275ustar00rootroot00000000000000rdata-0.11.2/docs/.gitignore000066400000000000000000000000751457133752200156210ustar00rootroot00000000000000/functions/ /modules/ /auto_examples/ /jupyterlite_contents/ rdata-0.11.2/docs/Makefile000066400000000000000000000011321457133752200152640ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = rdata SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)rdata-0.11.2/docs/__init__.py000066400000000000000000000000251457133752200157350ustar00rootroot00000000000000"""Documentation.""" rdata-0.11.2/docs/_static/000077500000000000000000000000001457133752200152555ustar00rootroot00000000000000rdata-0.11.2/docs/_static/R_logo.svg000066400000000000000000000037311457133752200172230ustar00rootroot00000000000000 rdata-0.11.2/docs/_static/download.png000066400000000000000000004226201457133752200176000ustar00rootroot00000000000000PNG  IHDR(}abKGD pHYs  tIME  :G7 IDATxkmu1>%Zc;&mٮm$vֈAP h>) i  TmD"v6imqk[-ՖDZ+Gdd=(c|5k%y9ךk9ߠϿe)J'C;?Q|D՝ UB`a$\ȽGDA0 с)p̻or4WNk4̶cퟟ )gϮұ|y p =6Z;)?˖58m_&לG#?lm`ot\w-'5oWED' T 鯛t DFՏ+~ym00$"  bwC? (Q,DV+`ðւٝ Q ޲a[Z |~ptHFJXNhE!ŦNA3gs f/_=LHL37|mV܍盃G.i^"M1~.;3? ϟOzog2*@e1o/a .ˢ*0[;I"iQl>'n7Z+ʦG)x[ns*"\KO5o~CfHAIrR~s25?m3>c?FgDuJ@j|ߟ,prˡ+FenبC# W HbȽ/IdPNHX~ 9[<':d^dp-HwJu8>zf D"#V`[(w- 5Sy-k\v4>N'D 0 1(=fΞK2Sbk+Ujۮ4<&+S$윥oeY}aLJaB c<8sF@=OQ0e;tdqOђd3~GfQ-W&)7Wͭΰa-+XŽWUH|R +NDл˕׫?_,]80fy呇jܝÊ: Mp1>˽K)V]z*0^Fiɫ5YqKk r_&e<İ~h|kY8-"$ȒnɂrHe ϡ|m((LjHt:G8zâ7}"z8uG\Ad{L^' Vܺq PqJ~|]5wX҆(:a2dLI0:K @?j>JyoݾMW__ʗqU|w? %«W7]ި@FvN@`P1,?+ϐkJs̜wY,>x ’ S\ -8wvxΆʚ&oJiŦ ){9"cKycn)5Ja is"asu Hs X8sCߩ1<1ԫg[kYk>4˅"'L[>4j~?CVȽ1?Oqwgs' 3g cx"1xAd𶷽 |}}ϝ?~W~/|5\r--`$FDMܪ VE9QI(+X  1 Ӊ"â /rg`$L_3tqL~̣JT^HZϼ$sW=p߷.1뙇 Q|N4B=syݹ`)<1(>g=B&\C`%BWr'N'" W")zV!(HD8Z0D)d֞T3ZH?S ]ʏ=\&qa1  [@Pa' !2{m؏qx?|0F}U&DT抈[H£{$e.?[嫅4U>w֟UL7H Q`x[ֿo.{|!m[c 4.imkt_~I74'dԧ1l6(F<>{Ӹxr1 .e]ѶJ>sj@|Hm򬯕/m^NPa6!eޫQwD>X,hTOfHI YGOd @ Zܔs̋_$(;-Pt6?vi'-zTۢ%{MD1|)ϰ~XVJAeW%y׎J,zmsgQ XxkW#߆gxױ^od& +` 2OXypmdyBw a7 U}JDt%B?cU?_1VnbpvSa2Anaa`0f `$_z?R犴k߶ej'y\reУ!lMV(-/yz$I`]dCԶF:)}\]_&VR,yq)+v^K?/}o2cNMxl*W~ɔB9v5I8z^{O#hGrkgOrEJpƣni-EU,je3GóEʌ"2lr.7{5?Oկ~>ڀ P@,;1aIA=JfO3<^&mϕOl;lե-֩%/meQ0xH.IT.R<ۮ\ .<]mjES"?! F\Iˡ=9Ś>iCfzd&(_qƵ'T6$;1:DI.yfy[;o^vf6L-34YԺa+XDsl2v3m11/{/üH PEus͜J6/PxYwz0>3kS@'~Y6J^.{|ͱK(9B^tHHo}>率<‘pL9&!KFE`=z/|/_˗/?&^=B]y}mM[O1ǦNtx16/4\8l Ib yln g^3Bo`g2[ -,[=&`qn,wC_Hu4@4CO)BKmrsnys(7V!y-iö0ɦVԘ蠩я2x Q՞Ts$KMu#:rt#C?hXL,;lڷ{|9{DJ>{shw/-~Sw⍒GrG *KQb5 C=}ڻ۾woyX5Op76&c&D b[=yĞ| BR4zٖn;-#ҁo1z[GZ^d]Q6+o۾;q=dO$&BMDa)T,zb`ɟ+|+>=B>98Zi=ǩ->O0U &D S}!g39wM!Ѵ=kr2=<ո^oI2̓_)y*>AVRbUo[J $(rm޶x8RC O}jYZ@vvұȿOv<^Yt=Z,P+EO7}]/y枼I?`(K'q3X 8g`;H Om7mFZԓl ŦD~9R`IÓ2DH~Td}MB@oZsc"l$@BRt͞IC TfL{SG}}{jR۽Pi@HS)=o aD_[ KHinU1Bl.GKZ:L]d71E62C-n'~N s[Ξz^;!4`+ֹ}=|~YC 5Ae'ϭ9ÿ́t`^Ӭ%0t1Jeij=#쓑"|&rź2OǦ0W5A Ťrg|q'$Pv:/#ϳǒX y:V'/6FRPz)5y?(æ-żHR筥'q@N!#SCߏh'2e IDATýJn#96D eWcΛĵ$>_zj%0U Oja1}n<اC06Yh#z%tw{@}-e@S)R]˅i,%:_qO-ϯ9O_HbN)#_fI.Sp&}ġԃ1U+H.s؟(T_ޅ}C _3-R4Јad~r~}y`cK_OOY0 ߧd ur~>?pdN{ϩb>4_`0 GיT~."Ai H[9icSC'h%HN=CW7ōB{854୙B K@)>WGis1ݠIxh9CԠo&1d7FDҫ&MƤԺ/1!/–>6a {-\FoqüTʱwe |?Gm M^u>w8!1ٱ$rWtǷ a#bkJS=zF3)L$ p~~J:bqtlu?Ii* 0ƍŊ/C-brZڪ-S @~$CԵ(sfрb[p 2P>[洢7#I_g(U&gױMBYoP2qi1ƫ4n]$hpwETe#PqGRe7uqP ٣Q .DHsz=rcJnpdx$tAaZkDEh| Ƣ - qZ1` UB<:9`LgcN+6 Ѥ@R€8!ÄT9=C$/XRh8Yl9Ԡr\ȣ0Ƈ)u}8m|+ m]DZr{T" 0B1I[&D~,jV{] !WE%^?$L28ꨗfaY""2GEEHBYV&ciqki D{.S4L%:aX)w,E!y]TјzZ|OXRK֪@Jf,:>3Q-B jɞ%͔Taj_G8l>@1z.:c mڃUg`UZ >?}<}VaN%:{hl ^r} R!ig?Y~:.^8a7ADVK#[:_`CѰr}~! IחpP*<.x3##KOP@P¶|—Ha0 Cɢ&ua \~P# ܠ"T Jhaˬs: To H͐O94&őAP8SO#TEBŁ< F 2ՒqJ־g@R>.J $'w?_Ws$s&kot >/*ܬ+phc-d*"Z8u5~jJ0~N 63o戮'fi3 Sz~6{,|Q(ҪfZWey֋38@ e|OϾ|# 9 l6~YR=(8}pu 1LѢ{st>sx }7__o0 j8BoM<ԋѲDӦ:'(W7źyikLJn+}XxE WW(xr|yn~2M,}}0V99KvAsO/9yf}ĺ FG170Zd m"XM1)1zjy-ٴ(J]tu~'?7qͅA2e$,.(M hljʓIn@fܲa?wJxFl㌷|E$Y]ckY0?_/kƸhziP',9GomO6^&y6}>Z_2BȒ%jiI$BnpCo};ڛyekx9-f;IQe ~?] fƧ~}䛣k~!*hhFЈO`T/w1\_yOAxuc-֯~[ќpSn4WwdFBg؄O&Ü vX[sJ6=J}C]W8# P*Pys_y[,Ӿ?g%/N7mƽ6mV ]Y[<%n%边10!֟cqTf.X3ХF~Z9:Jy %͠fט uuڇ*%> % twIX"{'S˓Vm O)ܓI봛shFN&>,Pi& U膙b2\xj({)<䲏wչ_okPe 女F1 `']nMlϑu1'Uy&8QVdtV,wCY_SE't(L o~Rq´ôS[P6veJ{n?OB>Ԝ)gc앑w9vm~ r{ r7ֻaT򤟋os.(S2 yUMnFy/ڜ9\l*;Xf|"ʉ3ѱcJaB,Ws7oУ8q-Zĉ-_'"ZߤĖEDZww'i'Ѣu>*\]Y]<U\sÄ˼Szev߳f׋X|~0KoP3ۮ64RQ^~sw3p > %By[qvr"M\;àg.>_t>ӴQ\r:?mǮf.|nK;oOC + ]zn4ldBEbhדPbnw@ӗ*U7߼Khi%#d-|MfGqJ1Ő5!"0+!ȭk9m uȮGCijiMt!Y*Y9wp*Z:"xΤ?yά8Qq7nR|ttU } 2̡LE{ :p~V qb9` H{yݲEwd-u|=}i=i6<7R*/,bNj99C_^}-Zr}.Yjc/Lg3LC˄-:oA+HRY[ XZڕzv B?=;{Ŗ%yw:u_ )d-"iW}M_瑌]d}%]kSvo~W*Q*TS_fq:LSl^e^!y%qI#y@^9] X|Ӡ[BuiGNNOc=%{Z1\@,C>"߳RlMĻhz>g|xT~[iY9y&uc(ь)(?CI:MFR&Qln&{{s^}U|Ar!caojfLɜIK"}&v[q:kkܣ;;\8| K6Azsvz.E)*QƲbRWz11IU3(ЧvZ("ɵ}QO3 e3\\Gn#tVMy6!kչr| ϒ_iuwͧtPoCH.3uNU!65G5 \DƝO@TZ#zUrLWJCi¡-(+>y}a%R%| ,zV~k-ݘ}=:&4:QOr _`Z CB`Pq$;C [`rޏnW:kWpCsھ>ϳf9:S痾>:-tV؋]s9 HtbM2uDŽjkDԘpD96V!pP^(ѫ.]*JF.]&X!9Ż6yO7Ϯ]|{oF?|e+Xix̩EFַ)#F`l׮ZyMB4oPV.yeߡuwD9DB).LZϮEY-b1CgWuX<>غ;<6Or:ϻ7z#WgC^92 uO3pEIR d U(ashpη ŀpz` T+-i|+<6=a^wҡSQO!L_wYިg]]ZjEwrD O@`}þzʕwD);@7 I10UNa;V6zisw*PΌ>UK3$\HB.g#U`vH/@{h%5p zmnV`[G` b(<ޙ_篳7KEڸ4Q&Nirqz+71 .;k[A5zH ]oX%3|yp"A"?W2o g]G/.LBgi]o8a?R)[;ʮmBQ^zK<}U:1~I:\&iG•L:f{W!nIg\ nZKEf垲cNOlG}Vi \oB=Uq>N|I|0aLXBsbȴ $>ӆnyo}CGިӸ/z7f(shOy6O!=:V89'f@w2AGOrq0BH%ӧV:YB$N|td@P.`,a AcsP"Hl;)=LƩ9κ-K]7%"nBz.(uAWX*[=,m+Rٔx'co1#tf2o={. "1h+S:Rz`bպ9H${-]+;FLdXǾi֓+@YSϓu.&o>F48*%^rNk?4p'K!J#s[a"8[)u; 2씳-TeCo^?>ѶlQb mrN˧]#U]d^M&e, JK}8D !VDf< da\v '"y]VjN҅z{KLĶ->d詂rs!\19OeXVS8;Ai J]Nou>_3 膥iX]Sc J)" i % >u*[|O 0@0;0)B.fP@hR[7US) @kϻqtL'9+4k7kqt&}")yX5z5ý, 0RTi$=i1I)?>"9{]-T )_o]JEz!p7x3Uؤ<8T!GOs.BYU"=ɹ a{7")|b(1D+d7ju:L= ̮=jOYľ*8C0(\Ңq:snУ"3e0 aܛ?G۾\9~]ݖCv 8wc ܶڿ v\ܞ7!hYYj"f9J*,u0b])nd~ '<ok%m$-u ѳrGn)M `-;&zfO(OoCGWLRؤ Ij %y4=Lx*WVp+ƒ TSUg;sי vH|f鸇k:֔BqyIF^[KuBe:G(fwLD0LQL Z: wh^v~s CT_64{s cO,2ku&L$CmHjk0p`7 :9Qd2F:ʻj+ByM${GL!1{7Clzw}iQ;Ri6JY5PQ- '\(,(K87{ ,pNL79ױ.dˬ-F@*3YxHh|f%8A+KXsq4[ -qFsXm^0F1P)n3mH)K+\#rȡ0b<\svהfLv>2Wu^G+gѷg/HEKf<Жp8qd"@V)Υ{bI>ݡl)HV=w{g`eWnj 0<{I{zDϛ?MXmy&_ZPXqJ."uVMk)!œXYu:,dW[nʍ[xt*^~*n ޼ FQ+/??  YpUx>W1\ĊN.x0EW_W^/*v޼޸QƬ2qd\.!n$zڈ߳p 6`/#Va8bJq߅txx[wއøLQV1:il?)\=u7y~>ϽGNII-=x>ZI*^4"j9vLCsqʑhYw)-|Zo([!`мt#To%i,uUU2*022U \|K_oxW/Ƶ͈kkl `_Gt).yt*FB5@1\b]` _rK0v=+{W w}C\6Va+èo ` %]G1iXlQ^~'Öoߝwz} 7l_s9 ƏDg&Ku Z%T ;p*!br#h('ơ_ݯ% Ȯ+N/Qf]m-с8 ?A1 m[8KJv}fޗ?e&'l`b@M^!+)1lLpL\8/CHaL+@5o2G:-4']Gَ`DU\ \3~9{ ַ_Q1!ZðdF$z(}sqGЗ鶶sHZ,{)m߽"JiIegy: !5(LZh:_~cҩe-Գbp$93o/ơ=;ÝF|!it%8(†RKkODXc!~/WY|w?/]| _^8s*+ 2( Hhl9=sMQ֮D!Q`Tx0p7=k;i$S7"`0st ^-|+/|xw{38R b5pl0a1=㳍=EUO9nz(ϯW zy\zڵ9&=UxE)^Cifu+Cc)vHQ;)srO)OԘs8_{)oȻ{y -ʰ+_a2d(|-Bv֫,ԈtCܻz( ǵ*]YHd'%P)Sv`U XVf y8C!hWg ,8%Ͼ/OZ+L9Dv J<:ـ lm|$>_xt6| *Xޘbqn c2kA O+{ .oSy Ċ01z k/Lxsa"'8:AHrS H7~6^_3G _#!)"Srt"Ԫ#<Ֆzr&{\]QzN%sd]fU:<乩"Ln+V  |Ǖ^b <5kd8kH9r8-|^8Ex1>LXvyzR;ĊN.t #b`T*a_$>'񹗮a:^xX(vY RY^˧HZPwS= &kO$|S8rpR@d\35{ס:X IDAT6mWήDV\?!!qctMWt O}&?=ۂuu'\Z߆&uW:Èͬg#{A <~'c=#۟nW<'^gqFvF?{~AZDnRAвN0wN9Hҩmu .υ+Ş{ao`Q38F"pk|e _'+6~3cc 2.H~(T@]-Ck3S_ PfXu4w { @RSngͣ-4\ksn-D( ؠ(&* M6ٷUVZeƫW^Qo{UʑfVege" ҉(H@@}s=gcݞ}q#,!{=k97}"=OrCXK884~{5YϕLx|Z3]e}v *~Y0dЫD֋ zW =?ءUSiHdN3#:l#=O,M$u=3NU˨c+~R^Ü7 g"[ k RvU\//HFB*ZqVXcz^+ېr˨>Haj1"m(8C lWާ;ypN,nDK6 i> kU%+x+CW⎈-yO`F'̪UU^K|Acܙ,RiO`'kb%1`2b'y:2&3l׌O_.zY\ ZUV"jhݮ$>#`ǢiHi>NhTa2xBř^I>Rl7\׵mYK"82^$CgKty(~Wf*|$$UyX H3+O={:OKI}, jPC.$B?=ntw[0{Piv:=V/ս>U*C*=3<7{%89:KW$9\JY8Jϗi gTcN{>kw{ yIӾťć. 9J /N Qv Yy,n$w(h@`~k{-&ey`_T[/ua1hbH'g=Z,> tbqA Gdr_{ KZaV=,j/T4dB[>F( (()G!0啯/W*:O"ʽ?">'9YKY }L3pۍlwK|G7F^*Vg07LI_}⧬" 'fEoInZys0] TNk(C["%D2&ChRljp\N3 AF%L 濍ҽ' tCueR9 UgKkx~X^0QsPVXxrA.n?s>A>3S׵c3aXS,:A/>1y9FTȔ NQ}S"b i-y2-~Ny8Q(n<aU@:,]CwgW.)9lmrFsN)m=Ʒ _MkBs,‡/g9o@,Tk~f̨P)+BqJ2)uVMlPbwxy[[\)ۼϔ\'e~ʿ[n}Jrx>$7$#$,ؓ}2>opCZvɼ$hZUZӚmJؗ2R̆-+6|tKjEQ'`B٘P'TɃj3H!/c xmۨdH4$ -nJZ9\#e^7+J,RڐbK9LiƄdtM3ÿ_y͆6ysˇR@ӏg='Y1Cbf6bPOvO|mZ$UOú%`Qh0"QKOjt-D5Q1i7;*ж^Qbڸ؋’to{汧v[_rȏ-h3QoVW%\~c6# qiZ̕ӗ~e+&W^Z M>άpFPhBm F~ai+uÎ~gBƧ"yvT+o7} _ 4'}M.Ԃ:!k7ZZͫQYS5e%qRc1ImYH2_f0ǘÄ5Q|ƵRe%̩l\}$ Gև4S2'yd?: 9ā`g&$Xi5ZFTԬXC QMVTdU$ܟfō)8 턓41(v& fD@)tr@~Jܜb &{W25GԬ0|[T*P'/z8(^W_>A@Xy0!cL3F$Jf bLvlYU64ZxFom%q4+^%`E[]ጤ34Q~fnN¬:&zSJ-kX{}.Y.7%l~%mG#94ZhIմn-S+w:Ղyƌ& vYUJ90{GDMP/.3(K?NU2ϗ봪}+g֒IBlbm6Fmk#DrkYm5Z r&itz=R_rRGn4-CFZ>*y_fŌi j~|> L %7FQ泦^k=&VAjFjqK]_@)QŁfR¹'hT^]Ѫs9$6r%aMVKԧ^R,U9*c7j0)+hn:$AԋpA-7S\JϿYq*$B`[t;⣗_c)guQ3$(ezq4ĴW#e[5UQmTpWL.г2:i153BZ?dkTx| ^+--`,de|7ic+ MA2+VfONu,ά}atlJ#䇀LbRuGWWZJ^q4 Efy7vك~ĵ_[bPU}TY0,pw~Ǿ~=5,řNdch)Ax6RcV{&5ġ˵^Gݍnql%HD!B{VD$olXQxTnzը$)e*ڽK8xdŵsOF Մs/^`~֩`3 -ZyB҄ Qm9PZi򗧴&˿M˔,(:Wm'k?b%Lߍm gM '팾2&e-#S1Ur*fD_O an7}MY58e~F'E*nu{ҿi qLZŜiS371p$6e1kWKLa@fXY_|:6X2d·i)l آ<%ײjo REFVf,#>jY vgѦKag{SUX=9XqOӐpo7N,W;h`SeM(᲻bwDnMީhZV/~σo0~X_0>$]A*. b !'0Ł5{V )0S( UإU4iCJU?d&V#Jk,"˂Z]_~z7q+Ne7H3z0%^MwYkYf2/_-a &qm);~0dGga)̿Px"v2 &i&8a;WW_r[ԅ5J}tZiwJM\cFc˃ndw%*e]+F%0:/0N쮧WVOfԒ:W؆9I[ca4o.|)4Ts"uڄ4r72J:1RB=ᢅG[OH 5ڵ%Vʾ|UkesF3K lT޹=Ew7?Gb)BH17pO,]YsU,A=?*"><_QCA0 `&lזEL1! VƳ XIB㚶sL5x(*}G_@xu?FvMv=3Z뉶γVSzn@Dj Ģf%yz{S^mţK5ia5NK>HOvYU%g7es/U?S9Co:WwYx~^-ϰS];Ȍ4+uD|(]狸r DɲLD98Rih &Rh"}N5#-dZ z&w!csd4)FG?T'F@28ԦYikN&pQ0kUlgxhU 2{ \q]u30]RŘ$+sQQPU_ȓQKg$džQPbMÿ2 oꐚPS T?ON (Z(~%!>F32rTOKRTyZxx)|A,5 v.HXpə!Ykxu\Ϲ-QE~4E{ "( JH;g +U `Ŭءkz6>`;בsuEro%# co?e.k 2Ga [FMZ;BDOP4q0lXi}RM?k8JC/V&^ x*e28$&gX+AfuùO I:kqއlucm=PPU>?Ҋ,+?c0#$Ym;~NlOts|qIuohvR}/kٳ1N^Ɣ:+ ),T1~}5gb?$%h/uy%ֳ|Tфqͮ(c| IDATY˿S߹O_w Kְex%O 142ޒ)ֲV)m5 Fe QqV|izyަuAVrVZ4Bs4]h6NL1&`gpÿ˜$ֹk j"S(2e\N3s>ޫdܞJ@S=E?W^-JFLm>;}Io_R#h4* Bգ+~]~}y_+QIl;};T} Ԯ"*d1j)o<" 9~pڪ ك D"B/\3ޥ)bEP ح+OVTFP-4#H|#* 8  TkbU81L+=TQ M`mE+FP>HR9d\Oh Rn<se^VV@l$yY:Ofg:d%fVagT+4X Ib/S*Ob)3jN4(CI w>,;>Oq5X/:pU>x!x]Ǧ\G9:Lw=P%Bͣ,%jV`[[@w1U[U}Wj6%rwJ;96}zv`luҼEgPK0ߪ*WބTمY\ކ'YF= $2qz_$fr{jٛIEpJG,Xx2Ot77~'nXLJ^6D}a&̜)@2qۯGY &0UTǢ^:{ޫYgA$ V-뺖uV~BN=;ٝ !PE}_e,1:3N2`E-X> bk {J%k >>z M>ٙ 0I>XCdCwӸVEY)4)>]s`7KUXfM kY ܻϟ_ٹz=v-_>%{/`cSQCl&5J!HZTG;`]؈Zܓ|dsLog2{{ Úf|@"( 7yiW~5fcRلaj *3!*4ln\"/M g`ax FB'%V"U[9;u^/*Ǚ(ZoʇJCG] Vk<PVG%@e~1T"0rƤh/ȩ^$ <* tT]4Dr˹"8TO5/~?a $2 /5.wWȀ4q['v#9ҴlYUw~\|:)Xl!`m icaOؤ(c Nb {$?hMH>O$BL&ԔM}>[7SŸGOcj=Q;Jwt%hCcg8okNݵkj u Ejත|[z33xI$,-:2?n}}>/r=Dl b;$ag83~}] Y,X$%%ogX!ס5o*O} ˗봊ZCB" $o0b=>D:̳}8ۯ0WMPA񑱜%|v~?2?[ M$11 ]6F+ueaRh5 k _<.GE$>KO{2zL&GH V?V[?sxk.Hպm _c8k(?g=*p&+r<[`@Xpzr5JɬAr4d֗~̛t7IKbhҜMJzEs!^2pVF&rǔ0 QUW3]ÝO>Iz#}LȢr00dtFPꅯa}"GaN>|boxt`T yCp$hlHո:c+R2>EæxY@>]ts9xѦM86_φk$ lߘH ⮇W-Tػ΅}źd89ob]t{Ϝo0jQR=V>N [nͭ%*iOTĚ(#0 ynj3@3n Uq&Pf$ ~Wrɛϲ94/f*45* hڍR~3Gd[ٰ|y7q5ozf`#$,^qrLzef{8uqײt?UN=}Gv>>ޥzwp %ua#IP ZjJ?mIuNzUٻiq$ݜ>w^i,UĒjL"E%{.nfvu8z8X#xϢg>x33|[y)'s=t3 W gW~ze\iV  &^#-RceRjp[7Z_1ɩJ$9|X1>}=0h'8g*J@BG=ݗcE@)xWodЗHұIP2Llt '|;y|iډb*Bf<V/ =|˹o6LTWl8 yvU16JIZّy( R$i5p ,O^9Iyw:6D#tk93 Nbl4zWA¹O݌q={z;>m%Ez蘵d^T`\%AdF?b z/xbcbSK.sX(tb:!5A#‚ᯯoCg֡v b- w9lg S|{'X8vW8ch 9FľiۘG+Q:-Ap^gԴѵl!<×e8OFF7ٲy`T 9Zs"ׄ%.QjW Fi_ZՊHafKrГ fע+CӸ|g`d@frCOc_7|.? `6yt!AX#0{7o;ffu XC%5Ǎ(J.JM2^q.>ռ~ll-et3X t .kKt$QIGoK< &(*+0:YU`0ǯ'מww?-?oc3O39΁&,rC c6cZ1X)NIt?s>.7szcg耓3~C!w<_v<`82pRB%@܅X &$=3o=oܻ}2Bt,cP{vwppf@?Lш&UظJ|&Hm5ڵ]$HBWkLjY ?=cֵ*#mln qc-ط_{Z S7t=ɊF=WiXn#a J9h oxܳ"K5,6-1/X-Qo=dNRC┎',؈GW 3|Bi7dc 񦜉MA!X3<VNyOBe-]pё sYy6*GkXmՠI<(+ir\5s[ 55q* Qa(5`\Sd⏬\4RGqpmϊ ^~XOiN њ Whm H w̓!5!ඇqӽ2订ND—5gO~j)Ee-3"s ^\W؁15MBy}˻_2l~J.,C,:SMm4Ս$L%Y缚', s31 9hf*q/ ,Xaw Ol߽;gEo [Naƍl9ns:Ȕ$'ndѳy^Nv뗿vzPE\)XuFQ”b n'yنwxI[HZ jtL#L{^w=%dN ._/ZIg'ޏcDꕁ&,9n}x=~u  74E^Q6*gQ}W+k=ڐzv8mŗr6of>ױ6Ek˂!tǬ.ۿt{y/{)u y,VdcHP4tÜM :m /o}[ڎ;^4hIE&fsĚ ÀK׽\5ptN50.4q <~С3H.K[Ygbh{ZCj{MpNiw=JiFj < 5(Am(A/dΌ12$p4oȸ&&3xA}Abԑ?&Ў*%1zK&!ܴu۹ ^|Q."KtCBt$NAsx2G-pxOsc>yxXkDqvI9ÿ|ƛC"3IVcl !U}0ޠrd5]fUT:`6 q`f%!U!K`x7|,5fW!'W$V|Y:1U,U,hž,{0wxy//9KdTkbX_|۹;?kgb:y[1%$$&!}xdMz+ן˦f)^'%S5&xnxO~Fٵdy2Md! dBWaP򑵏1t;OGaȹA˺\񣉅rt<';*' !qr)T; kt@=(yG F5* j/\h$ dI[ؤ>cxn\G J F| ɵaDϒIwn[/z;lRUDb> IDATlש0 XU(JEa2'v'/ト8 ApbVXAȹU=>t[|z:O/T- Onf$z\޺4N[ t>4)l_wqո,j-b:Α!@d>ވrx[gʠ3C0bK~=~ _soyOp_ϙz P]YBuZlr9/}K9e5H~_˲E@K;6mԲ*(Ԁ1J&3E ௯ 3q2C &8CE|>AL]3KLA+n^t)'~Cƙ\+mpu2|LTҢ}ir}CGE5<;G//%kBSz'H6,;Ojf'ew=>o| x+yfn *G 2Yks}xl_>Ԝ0C}^H`ἓO/$l\Qo*"$"½Ͽp wnݎل.AG]5c5&Ĥ26hi?4V {'6 a*gs_T˫BǺAU|>q6E# "&W$ LCvȼV?Jˆ\Ě oT՛2٦CЛ鿖1{Gt4)D2b:x\ví,hfLzM:~WHyTx|H0D+Q=ZkXa<ٖUQ ?<=[\Vbfu'A p۸o2%uAiQ{ HԐl#05Ÿ5D/ cI`pӣWpKH2%œJFXś^^sr94Y@xf^6ʓΛ^KyeBJX&YS萀O/}J`[ ?w̑i$al ԧ(wYxƚb+}U%<􎁝%]G./+L |O@mvs ?ÿKy雙͞+MJbfC?l=t{Q$JI୰;0%3(YuR/AV: j/C>c)w?*8x4&9Zazd1lԿ#/F[ɊM湃l EC~QgPۮ" ՆŻPQD)/Ryzش}qlf5 v]Bls:ªSr@B\u̡ 0y`/ QM$)dp+2۷?|rgaΕ:6|(̫0ۇmX?Gp%wn݇WG.p<| 'mbcvt &HMXpBja5?xOx+ Oޅ¸j[F!C5Jh $ :cO4 X@g/v/=%µhG/Yw$W]u{ @:H1~N?NdҍR^QA=8 6b-_~ktgI1HV6~ի޹^Bu@ m M~n}) M1I}:pυ.wN")lgm5Zi2!BYN Z>pKzTzGXsrZHfBl.FlRmD5jDJ[ʣ;Y2WV+a݌stJT]nH6B f7Wsg)Qނ2} s}x/Yҵt+./u!?~ Z 9y?S<_p:d=NgrrlH|[]l"5`L ;D%V"%L-9G,'gx|b@mЫ5$spZן78:{Y+sK2{"{SfŒt&3Ef`ڣˮ w=ġ2Sbو-Eߴ"e߻ "+2q*jN^V)RzR[ 穃D/6E&lOG#<Fi")h|?նY >^ڶďqC 7NMQ ;f $ѵa8U̪8ml 4&M /։LGm:v_Ϟo_ń}=גTYn‡y|j_8b^v_ V@sfV OvgwW^˶%wiu\HHnJ]\Bg:R_3!n'Uؾ^xbHfԾji9^q$s-Qjyk 8x0cٶ%a#0yE1N}q%X65F%?us.Z]4wvA+#V cY%T>Va懷ýf6fQH]N4 Ocᶶbx{`ˮ<c9Fx )IIŲUITu-Z}tyumn}'?] d0[5*Nxyy/|Z7Edߤ1uKWK=mw/s9lؚH-Vt71kVFޮdwvvjN !Ua嵍Pad*gf;S/›`YV 0N&+'@ዥ^wWVd"qLG87ws3 /GR QTۑ8ngf3խ. M.KnqXQ\(skإBvѪnm>uo_LvoqmS1ƸTS׺.kHD=TX(}mg t=0n;)7 k;p|50ư?[笌;]o%}$?֋21ԫ׳q+d4I@;'y~?~/pe!ڙ%?Hɞ}ァ\S>g<Ɯ L7P4E%sT"wXgKе^mH+'^&R%gUrYpԦ`8{)9[Ogwr٣Wk<4*>?O_=l3bQ8 o]V8MV 4{'"Ul 8g/ċYC:hc f,+m!Vtm2$` .h4b[69??khdjEB+~&7¬ڡa7A5 l!a &4%OMLhA|:vI4߹WZd2>Mki<(VBhV.4%6nmP3'#Gfpbw[34ɖ4&M 3\C 2b wэ#,Bʔ&$.k!ôUUo-BXtۄӔ&UUZ3hhqnEaa*~ӟϽ1f X9}\3B8p D\Xë1c“/ߘq;(0iABh-Ƴי=jћQy/k5&ܭFC^˞{>`mH,i{5aEuq%7D>]u-6c! / R2C=š*6eil?eYcVMo}|x L8o&QbUV+!&!{[V BL~rD\X uRj'8L2fT,s1c_8ǥbrŘsvBHCJT Š2?,_<Α(foPI=CIף=?!zY)N*(K9baFi?B+.΍o<P~c-p3sZUY>~YTSSB' eTOIm>Y}种^j=k{1d1j^ku3S[ŋ`!9ky3c@=uEC{0\V&"ͬ%:v}á- ٷQVDpƣ=w_5s4_|s:JpS*,{ g@Oy7hzps9ɖUMSoZA+Agc9g$q_;v]@W&czYNK`-=EYt[VBnS ܃TdqtGl&7Ovo>,WŶ6"S5U O!>E:hʰPd-z׫! o8^f+L7{>'!{$~1j8j5; ?.}yߧ~>^-\u󇘸,fIAqV ꡦA}?iE,הFѹ&TՔh%4 `Q} ?Jvww}_G8Ul 6~ڪ,R^R[ ?K[E#<^M*9g$'}ϧQ8 l@6,jN; :"@H@]tgӂ,/Dy.ϼwmoJpli'>i{"??W[)ɺ REfD9ߕZuطHHʪrD[7M{tb;BVWPtB N8||{+~|-dDh5!D}ZԨ9v y5~|CS !,4Q6LlA,ﻟ)NGC$dZCQl6c6mS85_O?WN"u$Hyah1|<&U%ʊ4hרr%*k[63];$Ir?HՐgq=CcT㑨蒑CwD㡌߱}հ͓*TmYgV!dm^2(e0n"rlGF^`cxѨg.`0&xzIcSmNχakY1^X Ňexբ^l -bS @+=/}1Fqx89<&j>ċy|?EÎdzBuB@r>)6zU;`]ɂ;7<λyMڭg 2)u>+x@3 IDATܳO?(];IT*` =0bO>"m_c2|,RU qBb*~}@- nب 1ެhdYU7W2-/^䏾8?8ح0s=^{oZҒ*T #?_Mf!{7֚\]bجPl?@e譟v/ʳy$Xf4( =}xSSӏNg9O}–HDZ( -Vj $}'n?MlxC}tRqQZA=8r8"d: PϡQ kF"SQb°9|3o~q[ ?e||]jMޢ!B'ş{ 1v,ھ4Ӵ44_[%;Ku*O+UfMw~cP6j{:Zc_m^nŤZNS3OfײȜgT d7X7ӑV[fQ:ϲcd&p *fs?IuTbFaE:` G#Ux79 iIA|= v}+Ʋ2-a[oYj-Ɛ/DGC%smc?˃pHڋ>ֳoNAH.B&4TL듼as~?OğC>{ jnTG[Y(0,E}qo,P9X98.Ӷ=U"H0),+m%&h\!qы c.p8=?<_<v&ԇ 5fVv~YW8N9s!xޓQ@Ztt-_o͛Fܴ񑵮|?xX"06q=ɩ<۷ $;MGV\\V]X,YX ^ڬo^pnaxxBgK$<46&1(Xkkqj=#Ć#=tC=h{$ v1X8֋W>A3=΂ NEbNy~6畯|wK}u'U$*&P_X$$*UG-JuGQtDP`:$fЄE BD9|-?~/ϙWaJF%*Zm?G%6~飏 dypօGW#<437HzILMC ܽso#'hK帛J)}U뎫\/ƛBu0S\̢WND/x ֋3 TMGGj޷36oYhQg׷^Fͽ44:8;F ymKԲ3T*vJLi+|>{bQi]ʆ9j?爿Ÿc npU콏9—96fJN+N`fԨ(8q(/'ϝSGǻ:2Xe.?=@*po,:+IՐ+T}oeVI>Bs,֏O>1&Xl!HE@XWR hf,ǟ_0Sըag}:~oL4.CiB,E{$ BV.L38|`ӌiY7> s=z!zZAg \1GK,1Ce#EMGPJ%Q,Vt(vǞKі˥fx.ÓϿ8 xItZزU=rX<3M/t=|.B3Ox_xݍ4VQ4͌zN B{.s`fsοܦ /?{#±+,>dN\hTԐLĢhGX՘B]so/ŷ{ϟ/q"*Xp)+p.$_#de Va+=̣'j̄is`*<'8|7۾u~sGβl8XOI^DS2.!//9.U%g=$ÖۋZ/7KTZo;_9_Qqk\pE%;H޺:(&.y&Ň%M[f]Ѝ 9ͯҒM}0jåL ʼtn aihH=b.Q:p}%Zk-N-j}ZG^oɏ|i$^,`αx~[l[!j;Oc8M,1rx0dy16_~:Ň}Gp m3fg` 0QwI B5 43r.og_ǯk/sq!$EmZ{b&7FwJ7xʌ/|.8W{/})^:0@:.eY01NXYؠM94DMU|2gwMGg`|hn2h{+rVT:rքO~lf xQwlFώ! jb́Kk޼<;~޺/q9W!8MNJ,C7QW%ݰ e҉D`4n?$z|ddgAExdͻo;ʫo^F7&pֶ_R5M)s֝\N|AK0Μ?3xI*:~Vׅ`=:sla?6^C>&yhFUqh1km{=- rضs7qVuSVX{Hrb2!sss U)j)He#j{qdKk.wbcaq F0w;RypmHsP\\C,Lpu~?*mk`M>vƹ/-m;ƫ](?t/jslMigS |g4}-kw9&\+VPnQ MV)^/D@A!h4 R^/G|c+Т:G/|o# Ƭ׬(e=ORʉ:bv\6-s`s(lɶ5c6{֘<]jiM6 i[ȆcK38󕤨4)+T~@m{Zyak dEк*4 e>9YE5!- sma!VY+B33mj$1SX^^/sp98v-ݔ+cloIު:T>_Ob+b?.Cٖ0Nyr1.olW!vuLn;͟IIoϘN®$}z՝6;:g瑭La>ol2c" f݂*뱁н@rJ /q%6U/!MK^su[z+:>P- \y:RdE>pR_Z 0KDR {ͱ _xai~j_j8su":k7hU,1{7^n㞩b奩ާZtDc["eS5P JC*)ϕ=@-Z,D/gB$DTGDf\ „ESAB)k;ߙq PɄ &9e _Dni#Z^[3igwA\^d}ϳT;_g.s1pg>o>{sU$yb:xV>.i鯋U.N LO19|n}M 7.p9klL b^}aׯ\wx[xQPiQ-grCW{rJϱ$lJADpJO'+6Gãマ2Uixٗ"M&e>m䡽]%/onma̽6'"5hK=rؕh| x4n"!ދrVx~>s鮮8F|RvMx +gxsMӽo 5c'ڠ H.{ɕIY)#UdF;XP*Rt]rRaN9?ƥZ۬~ށ +s09ɞ]jqq*K|G YJ2j yk.8lyia)ù:uO;l{`1g1k oU^A2} d Cy9ǣBDs1eړ?T|ܟBx 7|\ޚ @P{ 2' 0W:@MЌ6XTXNMF.^橗/W';='|7-y-W?*~gR,a\Yg.^t&geo m,rJ=u؟纴V)]Y-{yCLri'L]BJeقȴ*JHVB 7բ4'C4a&J-KWJgaIvB16\1϶✖C-p .8yh½NkO9sxMiO,b"*7F>avJSJ[R2k{ GU۬{shĮ6^ rKfۮZy* Q.4^xOn%{Ǚk]ͱ3om4)5[zF{gr1)ܸ fbz͚v,:G6s\}Ѽ,Wܨ1 373Z'KC &l[,? 맨q#FX4s-멵eOUf{ﻛS[O]<\؝*Kބ9&N.@1>3a(KgT2KoųӶ>Re#7^5{ї 3es!{uviƄfa/E-ܬL&b}Vsy< pLhTid™i\sϝf!QGɐ|Xʨjd}c$)FTWz4,'D$VV K ,*dZq%rEU%"opns0OELINL+f7!HB.!O/W7'&: :3rscU{Ypfg{%yXw{_h#LmfuWg9|HU!l#N_[U" qDfgjc~^dsNZrupi͐ZɿnXz}cY_>#Xx 2Bf)x|Zo1Zw]X82+RnGj8q7zft/DZ^^~,(frx鍷gXkeMx{Q?gRx;#j"]5,MǾ_tL{,]f#ݪ?[%Wqo*sk'ExGb[ R&.gl!z_m\3qݗַY/:7r,O_ށ<`Qa vU9UDKWvdcRl,,UUFqZŞa ǦʽJ=PMg޸plFHC(+83 fCquҔ!^$# 'Iˤ،KQcF1Bx],-dȘl5fE;bx{iH Մ< 0k͢+`*_\Y&ײoz}OUmqўE Q.첛ޖ\K-*V:tf/ִ${Lm;换A+ʹ߻ yv% WS0g=զ-o"\1 tk@={1eݦ["C4\w -\QW%PfyjqGojdISltA X]͹3 PQI"DZ g4C4¦9؅*ѴYw`Wj㽰Bl[׍R>Vխ-DnT]Zrm #x>ڨ.!$l3%0o\A,φt#t=Ej4/hsvK TT vȊa=Y~j`ѱ:~"B+ؤ\{&ЫF4MEt.LiÊqJ}X6(ft ZL,BYڬ+L-5hڿs1Yphxm'$q.[rO4U?!15C`g7 >$"ٺjdpW!:i z] u//&XhMAk%³-wtڿib¥&Bm Dd{Za7um0 bo.J r?c[ ֤$c[2dolVXq!V~!!_X!q* `+]OI ǨΝeZ kszFd#h q=ڼ;᳘L xR1*k,Z Q'&m|6>Gaܕ2@'E Ke3l23Pll@#گrbc0xuOw{;D"سS jX\Z0s4ㆱGĉPe|,>>V] PBL:RKJ]'T mT%I]>$\/6h;C72#6y~`%LF ŴM,4,:@h7xji\`\"d dB)yioy9Q/WDhq~#KVQc@sᶶWE5utN mθ4!"$R-rXԠ7g,Z)\z|̵l8 ,. 4KA xS| ; 3Ǯ_DS0ֽ 24GLki&T.BN|\Դu$*!*&NfSnN6b#"p#V9CÌ4-ō8{5/K{ŏimƪrfa4~=0u̶olcw\RO3́&UL@O!V첮D ìfU|wT+?+ p:i54WIͿW"b4X0Gc 'YY$  a#w5_RJ8u7&cMUbZ?~>VG4Uږ0C;oY7.",ջ! z\0Hz9 |c{~ 2^nufl{:0ȯai=#.q4f)ʆNTGزġ^[eU2"j2׎/Bgv}]Ola,I" f/`ܪ,U~ ǵDܯAшUe̒ `!4fzs,6,74yny5_i[~TBhc&{"X4p_5eXah"8g @-{Q_ɡ>yU:oZ,Lo՛,J|W$҂hJP e(ȮzAVVo:s8稜P@9QPT IXwIYy`5aN YmE"Tum?t7UIVU %F=_iZWW1la\d`BP])4zVz6 ܲ <ZF\{aZAd\x0ȫ Ȗso6@zK9!Ucka$ !(2mMFyQ1.Vn:Rb[.>Cyn9!ﶛl*ڄI$AJbBOwx>$|z?E"`yťIh0М,闣 lҭ1XGx^ K#ƚO;Zd*$6hdҐKFl Veo *IuyakAZV w't{S;a !kB4br3ߺtogOoI`!Zb2*L0jkZh7K&!ta$qL$NWrchfn8smz'T}8w:Ig"Hk7DUJf<>ǎ'7_[<kOY:+{(kC|F_ذ^KUMn{1nT/pG %XRmHOJBf gB=‚y,0uu\3*&2, fJÖ*ek9zU3ԹD],MqVh"sPk+Ls2aYZ$r`D֫{p)Ђք:ײo]q?@׬K:s̊uqGbd,UۖN*K|]4BQ$=(CqN!$x!ΨvG'ʳ89imP-5]eZU-poV>*+8~zt{mHO+`eVqGRJ>IOh׶ ⵫%+U̇`g{!xpqۊ6d EF_ir$PԐG Ϲr\j~nׯޜA% g*B,*]^nH0}O:1^Q:Kl-Lәh{16~{wr4ytc"D8* b" .|!uzW]n [Eas}M{mS3k2)| ng/$q=+;HVD"5F50Gؿ8`m;V3Y(ftkw`uR<.k=RXẀm˹aC9HZA!_} e]}BhұmN#QMfzce/./-xF/POJ6Ήp!4isd!|ma|Ri{)(BZ5B)5$]4Kh *,l&tk_mۦI7yQ̲~ezYJY!H:x$3BWIDq1l4EO/ /{n=9dq44Un:"kAضĊ|1۱|m<K!i53تy **W"i?~SV9Qv$n\i}=NwC20{czpT=#KJq_RUp.(u~yj &.{kgZ^oVDzk#X߲κ++RO|NHb*z}ֵC/G[ga<_}4~wC:2D `,=`ayehtGshj(D2sQ ׶•Rj ?L^>d(HPfK2V]9&Ko{M!ǿ|m&.bh{J蜛2˱Bj! 0NNbrA1 s~/rY)-z7:ȭ -߿ܝe_Y| 6xɟJaWb$0랄̤^#nlskX(c,S,җ;$<5TS91v&>WOJ%ʢ$]cAPgٍ $]/W!Z^2mߥ;pU6I!"XFp( gMJdL ou3ڱ5 xk^4gwzhIDwRxQC~hmb9mҢe_< ;&CJh.Qb4kGm㖫\E[)5m?Y! 1,WsVVι֊ԠU|T~6e7P0eX4n\Օ`Ajr+!h \+6bu()b}Q{~4j7r qެiUGk­9c,8<yN7ʍyD<QkiT)Uh_)XJ?$ 2k?j*kԴC 2XBZ E0zvb"ګr )ȺֲŌw~;jCVx llQ%z2Oj=BKQ(5Z՟nиUPT@+Ї|AfˤhkA zA]Uʧj~۝cЂ 6LzGT\| {6#vni{oU 6y Ihի3Ƭo m&~dwlgeo<ز:{ЍH$A$DPI#Ke+;$ %\%;3"X`9d E$I 16=g?>ӝ{zn{}Z[$ڨGDZdhq<kJή%س0VL =dPbZ;g !9$ R<$O^}U@ӟ#~FUm&g s1g6' 1(5Fq,mH) %W|s3!N<@2uUňƺjFL,ўƵhJmQmJ޿^OzR0Ǖ7TE)O{UɭgsƟ arɍPIrn@}`OL;KhnF9ύrƹ<'gfEB̪4Iz^T1ų9a>H=4{T-cpkkzϨf 〉^턆 j0>M,Y^kM"\e45nY9nv;l,2.!f>jHf]%Įi Tsu"jJ4*a9fEm0쬋=".-eY?_e5 .U#ab3>ǯ+y5rʱΚcd>2 R* pO}$ iXZb>z~I+&OJ|t 9UG%jfӍg52཯ts3vML}5JFG7jC9o0oU*0'qDnpCcfhUT*ej=3ii`},u;ߵ?mK? F+ъrڞ3+ N5'>ĞfNj&i5vR+7Gвg`f+hTUAzɡ4ǖ|&!r&=K<Ǟ&tbe3+VU#)mqh]`B X 幌 "OӰ$2!f\nV+hm%n~?h3AՊ\E\4ֿug)M"[_fVhި~C51}Q}~?{sGъ1EAlZȄ7Twf6Vo! qz}ȣOZk7ؙa#Izr9ٴmDcTxw&A2>/Zr^&@ۭ(4<&Ln3Ghl NF+a-W_e ( Dx4<)BO^Ѐ}ପq̴~^psmכe@gY{sb.&1קUa2~y^}s2 3kvk7Әc=ϦA+왴AL.iZ7 6Sa L9JNs=NIPe&\U'MdQlоt2!hUM&^=F8\M}sX,FM zw>%(7|ڃEKOylpıCO!1XF >2SAZUfپʾvu}ڊb\wfYh=qNj\J$H.3v<1.^ΊOۯʖcVnJ,>alvngܑVxfō6&|LR{$b44o>ua ducut oV};鸫TMYDSAJu,gn6|b&;X]eQFY'6ݚ$Lʐ(isi$j-L&d[ RaKՇOɁ(lo+D7Pہ}ː| #(1tj2Vߤ^݌1SUŶ 1ىB@s4 1BkkjŒP= 9 8+_z1Tj)=&g&<* ,{Җ9H/VL͟4YќoT_~V.: r9)0n$ٕT&juWKT(lt4ꗆ{k݅Ȧƅei.ضȍEz1Mʾh$ \G[sV BESVYu5Pݤ W5%[kbP+v`.UVڄkdn./ H 0*~@7" dk5o;<ȣ `bEZt²-V5BHԭi*#vB^V"9#'F_4XDN3F5 4J1>jd?Ѫsyig9HULhO~>dSvtc9d5B.xxyl7{SdhTtߌi۞)H*WZImˡ= I퍘3ȫDV.l٬7"$|m3[})Lp]3'6Rkr呧NTypت}7ǽo;>8g, g@,9HPvi>{ ɹEԒ>xV]u[qN;"# ],Ymaqgah 1lj q44$;QL[8ѥ*3$l?寓Kamp15dVryasS+u_? UhUi.fCv/ٽ;n+us ٕg3 ڨHDjl4a͟=t9h wm1aZx?բSFf ϭǞ[h*X=Ip&tfe;E<3QEr+IPVd6E™Ux们E/%{p~,Cu1mhA75bת IQ`|-/yNEXg02-Zo٢u,X( E=Z(,`aEhV=aָuK0(!?ήp[6j7RcY 1}=DM&óG6׏mp~k.ǼhwYTs-BQK\{p)7 Qui:aZv dD rEUݘ m.LynVw7ԃxuqrц R#jhs>{/ƣ&aўfTj3TکLS yhlg 7l_믧ZD( İ,Iq2I˅8toɤKGzXFEu,#9 *:=_͊daDk4n,2E5 ,wq1#N j!ĵPr4%`%( 5C^Ûh:cOsZ:Rr!͓m ɲVJ33c؜y[cٓ)kcMsB!.]݈ Loq.=.\u=![^'cmT0mS爮t&yrTOn6nr>^Я.nE}qMfcn8cϽ-/˕ģrS9oЌyggPEqC'ϰ"UXQ ‡0?LsXCtH!DQ|K2(O#ցH1@a5Jx4d.ଲ}K8`s^ Hmsܸyx~mQ jP\($j,]E4ezF yL|ÀxBr OVn{HToJFTSֈ-^uGDJ3ƾ,:ڃlap6j_ \,^6$V#6dB(,@aŕ)hh0;l$ 89 TL:ժo~#Ef<3H':1zLwFsY9N RlI: JC0G΃/q#"Dp%~]_~餙cO.)L}:RҤ1qc`V(+8Vs\޹>Wm,d]-.CO` $ieW*tkS=?ߛV}>A c7e֗'xwWyiyBc;&h4O~ gs8_C-vo[kڃf>I8:bF5d DM@B|"B pc{wMqNS2|٧xI8ɱ/g<2f늵]J5~$FQ WzGvc2Pr8cgkyM}߫ r1 s˖^dp#DmRR6%c8%-s^_<2.~|6*gCٌAk]?mЩyRrL͖a.t9f^cvYTH,R!ŤpٵLQP[%CWZd"}WaqZT13Bxym/|Gw3;5` _? 9g.L5UJh#Hb9C3/_mo檝;!]t'K\UNaT 6m$oD%"UU!ʜ,gr#=Go2<+<3|3ZnNw7V: Hɬ, ~9Dh_ F9 ׾ţW ˨:lG+OL uF[L7 PD)Ĵjmqwk YxݕW@" U5s^WM%\td]inVP}+IG1pב n:j9# + 5Vc'8W0&\dZq2i sӲr. zD!ðG]s}eP\4Y& 47j-܆g_2 oTGĢՇyGi >)lj'|[/_ə\dUjLAМ8qgYanܱC;=d!Q!{$Iȉ|2L҃$!w`A5X9,Ašy$i{wo۸-7)q{Ճ'g k#h5:a*{`Q(xd}!t~dmJ`[ژGVY``}?n#xEqU.| =aZK@>xF񦡏<}9JrT/:ꞧl=g}i'j]-ʭJFzR"E"';kI'(|LLu.Z:Tqc{\h)H'ZPҶ窼b(!yz}a~ d^Hckag^uv}kzdYF'1\o2 ؕ@Me8Ok h1b0VU]]T2"[]m\3 Pq:V"(;7\/?4c|?b{2ۉr8rw6(E{u 3rr9483/"RA(}XKOWFC55BMeβ4Pis8ٷ( U!u]Tι7!n5]z־:allnd8:A+yW\pr.T[F!Մ^MsZ9x!$6 ̗)gb)={J_b> gXPDߠ]]tv8z4co}ٌB'uuF}ݭ,{*:Nۍk6*j8 E4!X +k\g@Gsm <3Y޽mK]270!Z }% -I1.BWްM{ѓ9<+X6?p^jN9y VxfK_eJgc -1W1S(Ui2,jshޗ~%Rf0*c^] 9msDzS6nb"ZMd"QѬ,)e6ٓn`&';@4rF '}_lsX@$W|-"QqT:9ߛi{A6tw]y J9bMVfshl}}|'1s|e,}”VQI97b ߯aմ0! SguXxl'}Qa w<}y;Wśv=^Ȅhb9 o#Em ba83WVOgϞڐΜetU~? BК ǟoߵ\݋gT IDAT ߾ȁm۸~ܻ}B92@sP|(аzÜ0c'r+w>1y0Fp&CD9, [,}4v|^WM|MŐ<#*cIh/|tmM1CA`K VM_Oy_S۬kgt/f=~n[SGpZ{2;[/eS3@tv!EclJ~|VSjZy/ ơ5qrv>q֌2l ~n2k ?u}|yWru9c!8|f%:Xkp9V#f}XVk1.2zC_>~O?$.VؓJNu_ziVDU3Y.m큣Vpmz !4ۇgmGf'w( i&MȒ_Rv<ԑU2Mত?hI*C)z Kg!y:0㦥 _S+m+l؜kC*i /1jI3kᆫg:lƒXL%18 wBk ZpB j ' 7>ԇie}062P}yzZvQ}Cf"t%"3<#gN˧):]ְ# ZEA2!9V]$!Vm'޿6AY y sF~-]d6s0jT&I/UY&3sJ y4|jD@Ĵ*Ϛe1`R";I\eS4FpV0Uع̛Nç*) cfF|EedFpVp&6{tfͧnjqG5l;*&*zYvgTjd`)}sRU{^quTjoG+l̦vfpMLJd`E){ 0!2S'UEj(V/!V0VÏN>d^A fcƪ'f=ʚ麆`mW|HڽDBOH; u^:9k9~/񶛯o{ 001[VIP6BDIB=*j5WxE.&`D ƍ+4y244&""eO4Kl\Z…9qTzz:7\~=s`Z87&)N 7"yƙU康9.e*%x6ԾgFHcqYD:^>:jƸjFo&_KcttEg:Aԛ$C c } l4N<}o?JKOBQʼ(l }>cWU|OYG`LG쭖pc-qAT3fJC lk33B;`yеn{_{?7s͇9<'t zoMǠwњyg֟3E*sN "]Z&>j "BwNIZ #gx b<vvnj6}Ipe̝H9 a-|kٞ[kh4w-Yy4hc V&#aQ[Vf`d[clX%L,Z4ynĤtN0ux!E8/eZTW%R`t>9|8&~-U$溈'Š!&!/rxwNgV#&%%@ęO~rl [6~b!BxN x116k(k'DBV;qim%#Kb%Ps!6ӫg|k1G@l3Nspﭷr]i' ,QH;9TO30ɧ&jDž9V{_ӡL5qnePk6/HܥC6 59 Ԋdz(VؾTʹkn,J;1Qr̝M'2S;oԎ'5%Zv!E_X}G.K&xHƮ21vcSL4^]F۰┲?Uv>{hxl& tKx: 8:2?qW+& {]3\j_xq,'ȯ|Oyf=@o'%B Cu]}tM܉IOCH,A_4d ʨY cP}hEcs#,:>4cm!] {x2?#?vxyZ/^P+ה^R,Sd6:!`sKS4#Uf(9mì7/_IFCӘ )S UF XUϸU,h( py+8Ʉkď_}ouyo4ѫicJ!魏MQNFrb$P;5x8 S:bZYp=֎ R_M_qtT^ CO6Rp3B1ǘu5==WeK:Bz}YyN($?y~=w ?!nZZ`*}>AքjubX^o~yMף C<> Gɶ`WoJF02/Nz4ؠ%\mTB0 oH"rrY5VV_dmسOQ`J–ARprCX0=r?|aayE4BCUM6~߇j)79n˷>I̬}b΃_(T+&?X}}\Okt1edҍ(hnM /O Rb2ȍTg{ Y%K>7+tkM;e$K.ehRqIDZhG~X߶LRk\> I OGdx!M4՞VmۦG3jS!##5ls9i&pЬ q1>w=2=z#ruDJwbNи݈ ?^}Dq 9%/ĶU__+s}o]zA33r۬ܗ \V<TEtF2>~ 2O-P?ܱT(E>ADe_$M*D,2i&XD<keJ85,?sqt KfhNVG~0!F t3K /?wWs~ hVbaJⴆ'=:b1kXD}j1ԣkgCܼl-WB#*F:˾5\yN"(ɥ91mJ1i"{t*cZ;- f@4p&l/UGZi;/2 Jkqq4ҳ7 ^373Tn!D2p돲w5ClҌq(:2hc&y.twZS⬧GM@bH~p:!DQ#QX;(!\QF/M1pJ4JW2\pt7st#\.o"~#ܷZ^,90܂BӰ5rgI> 5*0.2ʛmO0ް$pgabi6OZWfrIb Zf^h3BG,#!|c7dk*\^ :hj|MCLB,OIc|mB%Bs3Z4 Sp/_/.ڶ+g6楨>/ES #9,`~o2c 5D;Fy^u $zHc$ƴ#s 0[y\s17X7bOZ}LJɿ"w[f;z 3L |=c'?<ů^`X),jzD,$I}UIޜ%Ř|.Ml?)'Orevoܞ3?q]3cŻ/U90pӧO7Ѿ%x>a0?tW>500%fP3r_g%yT]I5-UМ7_ۯ=DWj6>~/XgỸq {ȼ &/z;w_ɟ궣~!u}uZ2%鈖Ы4}C+6P6 WΏ: YC#lw/WZUf|I*YHd^%yaFЪ^cBRFBy0qCjG0lBfB&8n}XgI2Htߏ2%r es4R|<lրٌ* ܼwn<~ \VcEMg~3FJ?D'] +.1Ǩ̞u%~A{yWZL'`# 珽̯}P¡mҔ{z'u t@=m J*њa*PRg22q+t G~{u(WPL!c 3G0|ykv;،3Ͽę`;`aPFlt bRKo>> i/qEܼNf:2@No~%D ??ϳgsֳe-x|,>#; !PXFZFW? >})|!E/ĂDc5HXhVQ^4TZYDZβ)xMG9$,{\^ x+^Zfb6+O$sUF {V=\Q`H{a&5U؆>(YPj-L'_dtD96-g@Vf zl8wZ5 Ƙ/[9 XDKȼѽ۟ nعkXElwKirgfkN[Y df?xٯyC@Az0?} 2!k24x 1O?$_}>:7L-6(Pk ΰ0vt o<͇Y>Wxsjp3므tȲ +i!BGELMdݨ됿Lz<_ph2oMz!ba>* 騒-^?SΉ;VFMiт޴I5Iי=϶sh{S/dCZ\ ?ɧsФ|-i yF.F REM֭Fv3_xbY9>\N[DLTfJ6FDQ&ckNְ{\hw]Nv:ɐ r!gG_Xa-8QAhcRXzs,;[ovρ8LcmtM|2BX-v~ӏ'ѓٳ]YGÀ"B`k;$e\\gvtطk;^u57ǭa70V0Y|Ո/oE+2kYO|BZaIGW2Ǿ/|Sq8[GD#n+@8_+b7mx_ˣE zn>BF/n -9SD9WTF | Rq{tFHFEqJh!mEML%y Qn8YU2<']VZ`ɛ3޷7IۼLYl~Q{)n^)͙tk|mo?v?.A㰸6Eލ$K)>/qQNr:rWWo{wfqc k"CdE̟<C;z.x]Hn#X˚v~/!{wh37Ϥ IDAT!EC9ashۥKֵ\~*^b*?yǿޅ~4btp$W>6e!?tBuҷ '0,dցX(X^>'?Sgu(:= Ͱ! hE4a 8}Gaf't#fzIYKᇮ<)|eVBd2`zJE2Uۧ鞣xU,wZ2!m|3_ecehsx3m7OC.8\ UzzMz/Og7cS9 USBT J౑l7@d~nUt+n*߹Q@^7 []Ŀa36yGȞsP^ǿM9ƩGH5HzmDmUuEF)]e><;n<q绹9:Y:<%fM)\P>(d!(Iz>p!l̖)FDE3fkS=W_ n56[`UCJ.š'V_'ﳒ-! 'C[rI3A i!Ca:a=d|[x|ͷzjEm0>g EQ$*tŊ[n$Y4ש `Q: 2(v  (i3_gda mECRA1჏le(A!hg ߻V~YRSB^xZ正ﻗ?|FzB,DL:x$BTYtx[mc.SL%KT!&<ʶ}>[$!!$ KIe@ L`nwvV'镕8!keնC4m,141 $7Wuߗ?p=WTUUݪ{ϰ7߯7~Tri糳)"G6xVž#~S뽲[r ޴z{ɸfW\n;N]w w/B)fe;l(mK}> 4mo"M+^ MOK/_c`0dQ9_|t}?h?,2')NiCg!|=Wv8yiPBd"<_g}/#p' / غkF |K1 W!(/LIKEcr1ƙcz<~ŊYBBP7c8e!gE|l8g F8_J>nO7K02`X+oV,=^q!N5A9 e)d/.W#ga}Cy7aT%Amu&/2^R},FXZoՠ%d]27̮ #U䴂|| Wo|-?bi.؂No>xh?Č012Nk,yXqJ2`ENA(*x0~p _{q%Ot?eT@f3 YaA&󲎅HAgF33$ˡ?qgl}Fe\96݈H`@0{8Řc=_x+pXGi,+2%8PxCn Q/-ڸ_3>VgMct?W% oE t{zsN֯UxVyBFҐڜg I1eU5 fqp#Dskj1+]k ίƫRZDrpf=O{e;P`zQ_d H/-?z+~#.ƘDiO"t}%d4+\M3#, *PZF1˿8Id]TY)Zxَg~WGBG{d{`1OߋO~\pM#q~ZdeaR L2;3_ P@}=OX|+܋YeX` <}} q7*%3(;_bş!/s/ b_AXk—s60Vqx̸Kï+( 9D 0AHB^sLUJ, C@cKq5bNA-' (6̬C1.utfuXV6 Fƴ$tWfڿpɬQiщnhT1g-js3K6S`<ġ{bl9T0>o|sտ>8c !^pcFH,'량wR\9`q\A2 +7w cxhUWe2.nHt"߯q'>iQfc3©1ޏCFg_]}9n,=ϐf#Y މYO( 8 Ӝx =o=y+lq=a(Keo)ohNf|Wg_y;ghc "ЎӷulTn;]6^մins̓B_Қ>Snv&ʶCQ,bM^}ѥma}K%]=͏098H/Qd \By cO r$ &P8x-^}cI?8<ŧa [ߜ9s$I!2%Zi223/XI2$+\Ѫp+<>.=tW_~)݃+/$e bc5slG c'>/}^0r8coǛq0&pҕ8>)W{~8V)G>X5PRDz#^eeϑ% 74Xd|ÏpDj0& Q;BnAaQdNbN]΀L ?/~Ͼ|/NsQ5,!DWi*01I.^8 Qu]viZᷣ㜕1iEkVYnOd)sy!咊$}58(Y7+Kb_KZP~FjEPE=HU;{epciԱ%YJ-ivZ9S\=H5"fe#.6{+qϣo; 2X;eT48<q\qWv~\8(beקs |o <9&87$N7{́A'(#I$FeKnM4#{N vE1Pu,Hp??:C`@ "bU-i< 럅u1ã #2aP F(^++. :阆G4HЏM65VSV ܷR%8N)l㵴?Z~ɪ *|8]G1s 6%C"ڴ8ecXF;ڭ mPіzԁ*p.ϼ6 ?ĐNĂRYEVhQgS^["QhF \y*| Od+f0#JkE,9TKD e?3sx.5 i4uĝ NM&s;" _D*,QT{XtN/{Pk !Џb3 l})iz]e$Щx~9 g4]>9|? |W"5XX0 a,{"38N=_Q V1 Vׄl^9G•@`)AflnXqSoVR.y){i!G`"9#HwqjQ+gR GƔ!~mlpwSI t(#u{hp(g8j cxKƊ8 Y,P'+gwޓd9$60b٣j| 0xw?g ⅸo6ϕ~Q?67m"H^w3s$J' kQyZZYgk% pok' m luw+GRPntl8`ȱ9_X& / ,(px 3?l@#.G,MTg?ޟ6Y)#Juϫ*졽,^Ҝ Xu[zv=X~aU`$Yg:q3?lQs=F4D~ɱ)I}2bq0ïUp5_5{܈dN0ƒď30f~hWxpebpdPhW y9,EKgu~Y[(~^SBjNHUWaJQ<#H Cit\y4>}F4%YS0_ W~h7\ 'dBC{8c((a)XZnɗ?x3lޅ'>~a :@7eY B9T8e/x~sZ,-miWp禬MVEz?Jj! UJ?kDQyӔ8^YCp>Xfn0ߪ2I{+3\G'lP*#oPֵ{PS}X$ qz_x1 X!GKp7ۃ_}Ï 7^#g/^ "YY!$gYgZO3϶[#m:UgmhVϝ4s*gl\5RA]Vu"~<|P::#$V;ELɦ.svBbKCd5dsVpݫ>2 + uNxqx04BQ?xUD8[T[Zr+czE:eb4lUi,2?gBݟ6gXlDcտ&I>BC~'4%ps'1ۮ=xEXwYX!21 .>^sSA@n20Uj݃wbhExM7`y0@Fci`]6cD1ŷsX>4NW#Tڂf{c:֎T{̤h'{bJԙSn8*!P'WnQT5n\|8ԉ mh,$>j9`+6x~3 (ys cA2:p:5S1 Up| Ƈ9 ,„`a4`SD1Euw])T>1T>0u7Ͽ }^YN>yT)&9qqVAA9Nч/@"$n1b`AŽAD;Zz{nbP=p IDAT4kRFnCeSöƟvL;F'J!Qgڤ_5oS(vC wZ[Yl+K#Rñ|!V,i Xe 7~~n+"3ТJA½/?uraX,!2~Kg${RFGATZ$JÙL*Š3K}mHxe1y&QIk-+@qVt2ht䉣W[w^ / % ]^1 21QMP)pLk\<`L3.mPMƻh Ļ(/( +j bO-S*KeYrF/*~DJUb Q7W-!ZΧ+,v_`TqJֽġk!/t^ h$0J᏿5?u w̕ô@msN>ㄗs-3S_Fv>k JmdBNc꒑\/C;06R.{"}Lʶw5DԞEKуsD{eҳ2 t+3.bJ{(oU8uaX6é:DQxT/3CNp(44a$f ߣ5[J۪K{5nL}6O(%Ǿ:Ez 1{k5}Z iEwDZ]O+l1K@;cJ7/'GG) +8)@18}:Q[x :.@MűLdOz}4j~C$V ;B%fE O]VGG#Sw]Ytаϵg]un3)$Sw Ldl+TGidU%*#̍QԴJ;q;"Vde!'ߍ'p,ۋ:Xbϝ*_tޗYm>591rϘ˂&=.CrNoDKbr[CV;ؖpCՈ%HUE2d0pߣ_G'8 m4 \ įg#YK^ v0!UǶ:ˌTmk_AOmko6%'ٙnT /=?Y)k}T،dlC[]Ωħѿ!m^oA*9E 260 <|1|_8`!1YU-Vh.-s`mJ!ΑU"RmiCV)@`' ak~VxD\({M9}cjO^$וM$r֯]ޖH iKͰj3 GNw?t]POaÑʳ'Gr(I"3!E:q5!c獊|n=g֕i@݆pe@#;(I޻RvX랦٥k[Н3rUG{3-rd ` J0YnfݘJý 8zĎψ<46e0ğFCfB$<*{VIr2qlIɚDc\2P̄:r~ Sbb+ACa u>|yNjq:m 3Ȣ2?$,Tm*Zvm u|3k NnזHk>ֈT4h\DaMQZiDy,~@*,sK׌2ZT_ԑ/.Oii Əqt x/>fWꊼ2='G VAԆ(:o֒0LxK„3(La@(]ִkb*#Җ4!uiAZ Sv [F̏3T,_81i= RsCߚڬqesd"i_.p@;ֿ:H=+U}Į,Oϵ>8ԈRk(ݔ̍xQ:NVF+oyܛq(af@ {MOwkx{gi+؋6ז Ϫeޫ̺fn"P %PU2DA&|]]Jh~uk1MG6>?zmp=e)U.Yh@o"x6/xE`` A2!T4D@#V2Ǎi=y5N9bP/"PudbI 28U[HzN!î, [DRض!¾5 2Ag0'ؓK'߽gS IWZ Ruz&YīRfaڒ7 6 Z3b}qRu+d&vR_nwfdG).^ߞVa~)|kg7: ,#so{?|_,*ewL!3I,4%F瘒Tʷ>SYS`*3J`d8f&"LB[ԒBGȑ$!eύ@nCbQT[ӆG.»|-.=NȇBy9Z\~\KOǐ,=ZjƇf٪ieT$]TA{9³ khUV?@=ltf&rcm/@/E`}ͺ6ڟ? %D}]|y$Jr1hZ ^BZ|ݣ{!vlyt"D`ʜVqʡ{Wwo-0Ci k9X ϗ,9Pc|`ʼ ΧČ2u}*>[}6{~-?(c-Bϖ4E3vlDwľ:FȐèŐӇ/|~Ⓞ1a04 (HNDpQ¬E{ѶwLHgH .o]tмif'3 ug+4S.t -2V1aOa_f`T6'͔j/gpivfjל5:uD ?mNuij,_ qRcY<_,RʰB,vnyF-\M!S-1cvT~$n<px ߼x_o1|(h&C +AJjh9CDSM"4fe*5('P u2ERz˽wʂV#[:ųG 0b!휜&y"@cdQ,.û^Y+]rc|Қ-ɜfY\[A0)t8HMĮf()ռ2"FϨ!i &a@ ex ƓUd4F.p-77$ AH@\V^2_}4{ӵ;o5rah>OV]g+{ A|-bBF\f`Ba*g^YS9gy6cvos|%|B}`ɂa0(*^xO W Qɑ }2 ФMNJn#9FJ~jèpes7]bG%%-RI3yRjoyՐV2NI}"zU4>J?gY(#7=*f>4~R ΃lWZ~q-b}a3?ZeL o[9VNk*u$p%W8-䬵iؓM92q&3,ź<;l[V` N(P c2{x_l|Sg NX1& RP$oNC3+땾ۈΡ.?I{/eKuE-s;(fR14.e^>9xbf02`qC&{'c8OqlrZO|sLǺD,Kfs(lYU6x4@E+hME%}¶_S"ƹd9-6#e2# BЧ*?^5ژ?hE:( B`,aykɧ߇,a0(* 2PtԄ*rQ #&Wg~&rez_0^iꚴ0BJy%#0"J'ЮaִCQgƮO@r*y-9\eΡ+p5ạO#%<@sB`Aͅڲ.MPjil[eiIm6>u"҃WZj`S>W+ApF3E8+\qU>pHIU+1O]6X`4q<6M1fu{ 0(:P' \y(;_V| NCCBcq 0 ݀@;P*彩 D6 ׷TJˡP`+dAo/1e.rG$3~ޗRf /P)9\#\|uxW_ڗSgQC0eVJ-jj&gf)^>{Гk1틈-Ѷଁ.Ͽ@3 C*.G㒸twqslDӠYU` 'aQ ''p _W\u#;!syس(,>՚ƊCE:Ve Zik6ډ) og,oAZE=(I{ZpQm8j>G%2ɨ bYiDʽ2.T4͚{:2i%m_n_#jC9 q˥`=\tۋ[Ƨ? X<!4@ԃјyRypB;3 뜥D|*λQʃ+rt}NE W$No/b-i3h5 |OaaHdX,]?qW|?]F x0L3'5y%C=O-!ЊKkD3kN$E]o(CDwp|*aƵ/xwU808>ƞH+e#oqE+9@J]m؆y4 iw:"^j$kZbbZ-؃|:*Fp0 [5Y-ʲp=}YG5k by·Iׂ.&|~{_/JkJ9% (^\Go_W܁r,qÃytshVx4$\--`:Z3 UAA7\DCfYL(,JTja`WqhqW[x#d] IDATT4K:>UjE$܇D H} @Oў6f6ۡnL (`):1Z 3dtC%ڶp#QFn%\|>8 E#5mPY͎{>>R#c >\w'_^/k?x2X."gPY5*q80qZϠ֥R\mG+NY9-Gm{} ),V au80_;oy.^܃Ѹ_ŞAxeYscK4Vr Z""Ie 'd]#w]했KYAd,LEi<-"A:q8^^քT|{A1q絈+!3g%;9.;&T,vZ܇MݦyϨAvFCI9i&ba`h!x`-=={ 7<{'a!VQX$L+O:PLgmJC(/dl ؋k"s~Uu (&0F)rdlAr 'RQ3,)9\N=ghzxY||*Κl22Y ʽOLn/ulF;i6W5F2e mY#~nI&&r&xY0SXa*wU+,V`i8Qf3vS\09cge;6,3uӲk]~6s:{ >R{C?T%RanT`V+,Nݕ^D$ۀ؛{qSnpˏJ9!X`r>2\5U 9qÕ+/ƽOTv8PM(`ue+a:ͭY മyVkigӵ]kj{xoy&훜*VBHO '5P0&BX t3></kŃO<{?$?qGO3X-JV4DaLX0 028B@D" cfdc12,F8ϸ0.ػW qKMU€r`,)ظY LDV L@ۈS">:7DtFg:a\%9븚Po{>fjZ]չϺʩίyN(΅ έKxM SYK"E_칈;s>#;[O۫rrȶHSl<"\`| ʾG DȭˆbA #(V-pGOǏx <~>cgO,άd| GLO9u]ˮ c{y7^<‰f h#G{'70-Z "ShDnM4^ BQD+: {c`rVD,g24P:{l'!a1gRqY %^ -E3PƎ5]YUEּQԘ=4^)7*=:^1`9K]ds0L@A*MDNE0&B%X mh-thcg@qڮ`chO 8{23ԣo w&00` ȑuI'Wޥ6IM@|p:ӕܣ[v/yy.K'4 A]%CWry5}n,52vW >P†԰JazVun\@^߇]xFgOI"X+^D2o/M?W'Zsde*NN0 Bsժ)DF;ʠ)8Dvmki^n R"k1 (( G"F}^hisrԀX\M{{ض?*ox]WS-m۴ !SYLV "(A %tȌr0]eTq-@ wv۟Y?J;ZMl7:+5#Z<rɄ4N{ -<IdQvgH]inB;\TnSr@-S䅙ZoFڗdKjCi9R7\VeC1s!M5VH}àq[$×s聀p]B*2?N=ź a"~,Q5ϱFUd4itU桐4:_gaB[r_AlljD-ieټDw5 v`ts2oHoήlڭn u^;Ԭamm*3JfnŃ %tcf.50#G'QcVHh;[@?𧩛W#$nil w+pi4#SN/y?ִDdCrv\橒m}lb=koն'y+mw5]_dy?}$͵lS1*ytSl5fRYXDS7q_Ԇ @=WVn]mjɭ6GK?3޷ݹ6Id7UE[X7Ym(Zbu^||J?œ'XMhK:Mme*y<ۮo>ہQgm\<7&|Q8ϲ7t#6չUеjkYEER㶭E㉓)n=="_j;;:J9}hY׼Z3Yj_iTej$2Ө;2dc!ՊŁlCD0T7RYqhWuqLSi^+A40N/~'NuZ1DQ_;W$vs/ vGsͭ*mrf][=[ެ~DcsG1#-rnغ(LpvS3$QgU YeKJw ׏\ܼ(ؠ7znO֔6{\ΌN:}cW~l3Y9Ns Snk!u43=LtHΤ"_4Hs*Twѹ=v'ଷ<"n7sN{kia$D|D1Ϳk\EfaV'ʅƟZfS멤?l.{hO_<lcWwN8Z ׫NVU)JgC|{N_ic}GT>Fm (._|nC< #fy6 %m3֦NZY"uqjVc; ێUF]v&.{g~i5uP?u޶f4vLZ?:78-}ItlLtZF9O iy*sq7i+m 7:YC}xxq[yaeUN(OK6G7esێ8y3<IԒHY!Iˁ)dY̳=->bw|T3~4b:`5[5q!;ʞgtY:K n`h)۪Wچ \/By=OR@ д[l_lf>fqvkN4Y֫sh/k,{Y׹?˪yZ-u8ӳ!U2H!XĞj+%m{ōظvxTp[n{۹2{ۂY]]o*܄WڿמYj}F!*ۇEC4e+}GT`06GVIJ`ofYd'Y\ gv3Ν|ab#DڜlkioiV9w+9&\{RPrܶ=5꼚m4 B*T5֮<6a^ǹ,_cr\n{h` xizv35Ma\|D}3Q&߯r1 -L/ب>%ch^W)Qv{F.B!lofmEDek&7Yg^!sJ׼D98{lಾv9i,4mvlm]I=B1XRJ1ʵm+M%Z*֟xhaE?{k[o̽9U'mX:@0E% $R7/sAD7 |@J8vvvU]U^s|1ǘs>Sk9||tott Y9|wkn4Onbx=P(^aJiWWn:G(|jB KNg^}u磬Q6!da޶r>~-MƼf{ݽ]$zy{٭'5)4HjDۈR/R֕Ёk@9+Ajm5ū$k_G5S%KuO6CS{H֊_M z/tD*-ˎrra*tdhd8qiIii{=YT.aÍ3us` +n89^^Yo%~ywʏV)y,ΡUģέ~_~c뺅lKziK~'PDɆدjBYfLLCʁqB-s)ϱ{u{9~/=PHF<"sw[n8n0yIQV BSy7VfgiF5+piy8Jӎ׾b58x/s-XE1dz\ ¶ gAB>-9BγKw}Bs)ILd!bYO_ z=!ѵط"{tLsܽ^wש|g'J0̓".y]y.Y(j!$D.N@ǚKGx=q^~ӫJyOlykR7*7?У g^YOڏpҺSW=G7ZMQg0ȕk>Z)^h{}eX^gO΋HjEyŬC"E(f^,[C bU 9 DV^qaH}[zC+}cwExЇ?1(r%GP"46}=O++©ig "ΝL;4XLex7x¿%pR$Bo/s<.sstTHC2QW#e*#&7Clg Tpr>жSY@EXskR:8--^Tq%?xSE"uU#~RGב3UV֓V~k\Σ#)+QF>G1y$- G\r]b!Xe fq |?={IG_hD-һiT {f=m9U B 8"WB28*F5."%/$;h[9Ѷ7'(}嵇uZB.QխXa~!뾷@[ne7Oka1ૠH?^Ɂs[*NUF6^jJ'~ҽ'}*FT0%~gW[i),AF`oYx^5Ze/,q?^w̻~*(ɆI3GƔ7Y y S"1r3l^ $x^U1~"sn 9Z%wk/}nƛ=-_\\;~ݹ_퓹Rzz{mN_!5[ZJsLt0~+pՓA D+S Œ ^U"&u9;̱ suw G/_8_ZkQ:p>Sv]})%N C"Dhow7IEqƆyS7QFzbk||^nhԮ&77t{,b~oܑ>{WE"gگ;Ok`2gx ZPm9Lť1`G@!ԕ{+œy6ՂoQKTE׃aw§ZǮIpyz:cR{=>CQ9 s5ʙ4ӓ dzϩ§5vֲaC=PBrܨqZN]Ww*TR%n["G[ tVa<ԻWӤ#>{IxZ')u5IJ@ʎcV{Am=ܔ9j";fq$mްŰEKIui9__r/s|kEϏ/M'Ak9wυFP7 oNJc]gd7 !;JcqcFI%=MM6n+&xxZ|%ra9ճԝX}hoKkv^K([G`!1TW<<"i яioQzc~Wאt%Lk\,*敳Y^ 8FuDxM9f*[RraTjg3 ەA>8F%:&3K=uؕn[_+j>{X>{/=޽`,󲑫ۅS)ϛ:sdkvBCOfb'HrQ3QPAA]H(5s`8ۉKk6#r 0Sf)腿 vVFJl'r0^sIBWU^Tʳ]ziA^m=k Mjt^h!#()(n@)OR57Ubrqo+;Ew4{~!ĦUE9 k;SI#=4!ܘBxE6JqڈMxlxo*蜌<ޱw]O>MRDQBST{ 7 !*,^lL pp4{@%𕠻6Vk{~X8DzsA}ore,gzrPI83{h}I_"e 'U;irE{n'Vm`D…'3 Kc $8"A7P.ӑn}}VywH nؙ0Ca*9u躧pI꟯Q-R6#GΗVs12Inq2o4ȜݛQ6PPԜ7+Cs輩`y*gVt ~TD U0⁑0pjZ>Ň9Y^TU O)g`-D33CdnŠlzcFpJh y7f0.QSQ~AGah(l[)HD=:ua}ꆰG 2KD GBͥFJ p^}v `kwϧ\J8,hK5.kʱZ,U0t;砌s+6ֿ_n6ϙlJN5g-ʼiv4gPtecF(#hN ^^bWE>D}iEuGA)EJs4YI8@Ih0;́t!qz&ZQ=>^c8&İe-7~trײsq A/G-(u 4m>瀺[\_v<#rh:|n8fcYBY~$Oe$.'VJZ?4Fz.(a+Y%*PU؈IeLp-{B!|*-PP&1NI!U81~hMr\uHUu\}b{xElkʦshW2sBqS4BkOl=d]r82{%dX[LMb=w 题[ׯ )kye C:R X*cw*$Uk-}եaJ4l#i5X+eqZ"}-a.r!*"$"_[6mf%R\.VﭕZgPՑLZ5+Bco.7?.'_i{j@v[­LklcϨ3j;;T4T󵒔^6*SUqjFu.mxX_,'o& ljk/?۞;;veuβO2 TE4EV KP1IS%b2L8R9aa.a3TKՃ@`rLޝݼm5ŠW ߪ9 0S5&C IEZR sf#%[8Sdm32-%(5ۺ)7VP. -KF 8 LHؗz m|R+9u|UP[D.滝i=W|e&%#@ S>#{m N]7z{X߾C1?U:&ԯuSq-xflxm=Gu[ue]$R)Gx~ӵ$' Zy b%F&2|)01g RɕH1ZdA?Wcde0qԉR*QԆ4zV \[!f\_?pbA1GE o$bVZ՞AsR19TZtK |6*wCzO! {' aUaW{YtʆaD#k.,HS3Z߮-m(DΛ0 HG=="O]@f6[{P7؏T]@yq^LIa,1MVLf>9^B(c!P89'jWUxU* eYntORX)`wrk5d>`W8V"ҜX>830g+zMv!/ gCϴqa{cLĠ+[E+*W H,`ĺR*fhA=Ӯ/@ 7[[G lWs CͥiM1ަjT99^36X ³T"[R)SΩf u%sșPJk5?u^_&@AJ+㺤iOEb`IKLP5ho*Q5SP"/N+b}R8LdW{'mMƔ**Gv*W2 ZV<04 IDATLhbAO%b\db-z9]%+/vx Ir]W>ӞR3Q8Y엖G`<-cOμB #A} ;MAIL%Y XH@ <]}N U̒VIx~?Lr9<5$(MH1b[G\A|DQAr8hZ)]R~ Bior,Рij_~a1$)Bj-)]@?46 Ko8l~` +K+ S°p]9~JFV,l>zB1߈iۢ YJtpW;=q?1g@Ƒl3tεdp42CPaJtJB"ycCIU 'Blr]vYxtWP]59@d)P.6vZRCäf. 4C*>ШQ$f^~AyAryrߕ*bk*hyt"pParE~zTw@=!Jd@ƨT=SהKTb'/ :{ͪ1a ,Q'BA(xd"dw"*R+nӀ>`ƕ c]Z&C _uvػ6ᄑqvhl}UjssȌ܅k11c( `GP䩼엩7`=#>O~QHIWb7lSMM"#@gKkaM T'^c^FY_hk[wչ,hdxslkz[t$zؿլ?ݳDŽb-ٝ5^ U!-ء"" %Be{BiY*<B|ђ횵{y;`ݣNҧ^s^؃;mϐ7eN5C)j)103>P2I_1uZ_҄Yg(G)S^r>2Iߧ3v'z>vX@LumSaLuBQuwuNL?Fd5vY q'ޣPN*GRpZYBLZ[6|7Td^pO^y?VMPwY%*\t B}_D&5_:R5 ӕV kUs;tmmWB ZϺbtVoEo}:uiH/-J;(˦uJ ϒV#}{9Ғq$g5G[FqQ>*h kCB"5IDdV(-vdz)UT&0nQI0ˇC%qwXn[2h޽tJn!cY 3Y( ޷k~͖]d!?nk_6 "lEЕs5 0/b7tn1%vHХ V2\?mVDT#UtXoW8v֙@RԖe-\-HB#KP6-c~z Q^) 9|/6zޭy89v'{CKUՅٔD.P] Xn?_!65ߛBpo<=`iEd+|ҹ?쵉BTJVO9u$n_VTZ5">)}dR|xeݩ0h;|A&0I Z{1 {2ÈrM^]%S~ی?=ص8%eoS]+Uȅ\ʌCi 2^]pyy %7-U?xyb[ta.ooēP n)^I)cx^Zѵȸvl5Io{ tk9SuVx$ʘmτ[S3r+;[kXKSwF) 67Q7n]dcZ2 aѩ3Ό8ioG?cP_=fz N)̅{6[wɬFH S{Qah[T˹uC`ŭnmٞ'y6 ޞ` fF%ZN/yqxY[`ڱț*!< b]~U6eLo;YfLhrpp|O4O?'OIBϡ+т+/[Y7^W>vΉns{lTϵ/oK;϶_Z%f퇶CgQ}C$I/S]YPG~|9x9,>20zntTHE)A4_/9B"<ێDJ{' 8LoO˿or3jL?b~>_s?p} R8ǘu {RM7ì;"# u¶5ݦvéx*W|ʓ10^Ƕ*4c*" ׿u qmH:-(q@><dT:‹-URFn-*@;Ռ͍0,L[u[lm2ȶz55FܢPDEjfkh\j ҥ3hM^68iܝjs ǒ5(H/ wo.E^PF!K|N>c0;j@s@rlhX=u=ώyi3GDFZFoGN')<;TkgTٕ[;.J,]1zu`[9[Rox|1B?B+3?ʀGr{yI'k#)n<@Ҍ.裏㳟q-ẹi~ԈȊr/`3;OF)6dTE\>T-i/Έ^V19ɰ 5L )-=9OTWCk\xccx}e$]R(J~ ߭aZszc4#R[i\Yʃq¹ Uc9x{ #K4pxk(%<>GxjW ЕEC6ƼC]C˾{ݾ7Xߎ-zǣDy3J~kL]c ´O#hq}}1s&Am e,`"*&Z G]\\>~<S^Ph=3ٍ-ڒ)x|瘯=XZ<Kw~o$W_vs:u{kuNx=jAǕk5c]pU3ϕtBO=P~(_3pN*&#r @iµx#NO[;CS$v= L5͹2֍cREmscSI7Gwݳs={Ƿ >@W;5{BGH;~)?khm Y<# Ҫ7ECW~8wY81>Oѽ{?J8WQEh cѣ>/^VUfvbܰ^/̅BK1eO?=kz#>"[g)/ W8PHBNgmB@5Zϔ߸nO ُ[$u6o9PM -+%ݮ2Ir&k43*>BsQj]v$J`q-Q^[1Fsi.5auKLh'E.'ٗy ٓ.:!,Uu)}?y m]]z ן܄ǿo{x}zK4&q̡3H<B@cO?*.pD.pmuڋUVNfoBa^ApJ?ѓ%1}22XDtm=ǖj~t]4AC%Mds_N1^?gQ+Q<{<M%hIQ^Y-5ӺӷISn DGO?p:ձT4s%u%PU Y6-((.UYdl&敷ڴӛ+ϮrH#!b+)h3h5OmTCcT_Ӱlo/ yٛ%Ygߢ|}oEqOo ߿O?L#=L~Ÿg,?'g؟.)p=?7^y|*Z6m:v@uzt/0$K.HIZ2=e9l~SD[qgd1S*[2!Zʭ<ϩO(vv>"D?CyMEQ9VgOTi>'JyuauP6#WnNxF~ JTo-UʪW/Q%j#*ӕ&aGֲ9O$zTc$Zr[.;B7 ? ֛?3G>\:{F9}I@J8׵%Wے8.&1L$s i>z V1tcU673B [c*1|} l mZ[afmr `l|8ڑFn)v~aٜU4VH|?Guc-4r8@!KWReR 9֋1)(iq}8-fq-WF4tv\O;oMhk4GrtHt;O\ c2C&w8_<%u?0?1R7y?PV* 4y iwzyyi*CZU=AL#R(X /RJ Bqm)x}iGѳЉ(nzdž`spmw/SI*ֻU!6 IDAT*L;uYM`V)y;vw (c`qMq_zO< ̳bY9 {#5c9DDգ /Gɑ~/>(~r$k(RH,(چ =?N'n:gB A 4n*T p\O0y^}݇#A#rMNIۜ޶YR(z.uѱh&w6Ce{,^=0c2fiT!99aWXx|ȤY}t9H-Vm3yo*QŵꡘR7{ڼR^c d K97@ Wp865Tte\*<<+n`rEiS Dy'(z.aEYQZ,ÒzqA_NҨK)}HG&XK!LZUq8Gz޽8idj=q\n;Z<kqٱFZiZ.*@E YԞ tBʠ+y1bdekٰy_Q~wkϷ7BF!t1TVg?^Y%x `vЙ@ K G~'"pUxkڒ“ AoB?ir=)81AS_d:*\2B n ]QsP?CBh*<^GB#EߛbEg8jycEZI=sd~q۽nA;MIJby6sǾ/j9WoQu<=ۜir1<2񨴼$Ti%G sc4ǫ|8{ _ `+&Y`5鍬<<4uxyA`.g...t޽/p:dCYHdw'BGw -rEP!ꞌ}ت>Ӯ<螉ۗX"~$k $7LM',u=`X]pb.ʔ%pgsnIFi%v0ITUIjҭCK @ZTdvWX>#E} <πu 1T#e<_"]@9udR3=ʼ?g BqqjCuR3l002Lگ<{ޅMYzrri;q,)JMKjϞ_/x(!cH@}hϹHd2 ՋZڕ'2f†E=]{o~[xi՞LEv"U#~LP3fV!% HY4+T݈}ލ;^]U}DD6Dm5cQz{Zlߣg= R>g]"~{oTU]aGXG>RGyݲv<$%>$0t/Tb8H7ϙrH$]!r3pJ)=-bݘHF e'Fi3pQX{Sbz,rabXȧ3?/+N뤴f Uy?{,@}Gg?=FxÕ} q'0OrU}L_"%({bB|ѐȲ n.]"=ɱ}HpFYs+-d?xcpsˠEB@] *Kb@ywp}}-Մ~*aY$YV^1~g=mI8]b:B0R ]"2[*u?.҂'V#/Q[F}TUʳ{p͗ `Hϟ˓޸ "`4əo!&^]1n輴@:NԚlG^-[Q0y̏bILkU:ǴJ4>V`w&5ʜߋBg `ЧPR7j̗ؼwf I% y`r§x{,ڴ)2VEy;酊X=IӾ[H ΄93KZj5`^`Qah-Ү#k-fMp+ǰWȥ?{_W H:eHb\u000&I Յ$j R9ixP%(;i@+O%=yo~[#;\&d"2l#FsKU9<\.AAtbm-#/QnT6 ZjbÑMʷ.O FjAmF5e $}aq﹈ǯazrlaІjx0ekɅfZI&y3nj) 4wHps'Ϟ<׿ (B&EG^a٧1DyIV2 kg֒M*j#PɒsY$Tn5hh3Z8mlXJ}$YoS:goO?<8j7Sm- 86+G$e{>_Sc /r~8IҺUXڿ Kbo!luOaoKcpUEބ|`qsgi+@!Oݲ _7X8"Ὧb7ƍ^k8ysPwYqP H܌KE5[J,4Tj•{,V7+D`sR(dщak q+(hʵ6*<[C_5V@k"zӎ/ޭQr 4lCL~j?#tΧE bܛxgpL}W].#7dkFSϥtG[G^jXVX?^3-8lMD;`}7- xkԚT v!N3Nr!awtIJodQfk/9P O%P@I{^kZ% ңA[[pMǫܞWϓojKILwEJtuNݤjjL851ZPb"(ٳg|E/.UEYчRf@ȭ}Md ~֪&r=jqʳ\6T gS*p~'|_e?甍"b<7#jwg f.Ct|֭~vdl׷RKjRvE1G-t> rg8썎cy){T CD Sbj`1"p>2PNiWnrﱸs\[>Mvr%4Yiz`n LԒ" m5JZ|=lE$>ܿwK_׿>\sg @W򓁤=J[Nj]5)2(鲑@V9aUliU.ȹ?s4RhaJ>k<9x **n'&WVJc蘬}ȢJ?y.V:# eY].T<#-=9ٛ'<[$ yKyݛQ#o&گ 7}_2./a~n:m.-/QFxރr*D*bЇzؒX(ۃpZa%eA}$E'|_CG<; ;t'AW%#(U7Ey j1d&%ТvL람eds%|U7kIXvb<(hH!JԽ8e*5=m3=SB fU2 "ZE!|4M2.`F6E5:wQuO8Ҙ*пM ($Rd4@ȍ02$8͸w~{1}4ׇK鎡EpDY27°/"BT ORy6uV8hILXCz`+d h%#e5xȦ^7km6%`& FfDU'v ͦJbLq<p<[L8s8 qݢ5F6tXiEns0[yok=ZMV;]owpy/MKc%te(18a|Fm`{Yk?Koٻ_2Dc*Q6)EͶo|c&]{z]97=c=a}:!_ 9QeZ1Ɉ2 }k7UoN+Mĺ[b?v Po:eFI#񚭺`GA rPR}}{;/aI_[[isoђk8|Ivϻ.Fa!zH:YU'"xSί wq0 f0TH(8S7 ơN'DPr "|6nحLm&V}-U c ֪ s=UI']M%7ϟ]e$*Gܠk栁hW^S<"_OL6VnKwT|x̓ ϒfmvbYCm7-|OzB ZrgMiVTKh 6HqoXi= ۔BV:]|KceK԰HQrl(̒NbҐBq3Ef:r#$?3P]V? uaTT@c+}[$#ޫ=IdlE1Sԍ*t]}ulF-$-a[4K|xqBDZ=iE(m 7 Ή Lo~oG}Es뱯4!x^׹N?n\恻U"b i+ _:a ɮ!u4vz*Յ92,SJ'܍%Xg' mLgaf=i*]aW [W-ᾦC|eزqd,iFDxv_7D0"r.*O|]6"F09nZԥRlLy|Zla+H"cvMD*76l [JԢujلO VNl)Eu,-5J Om#s2ɕёZ!iMOZBtUPq֪rw1_yQzB:7GMJ䁰<,DK'o YnG\4%\ϼajc7ɥz:5s _~x" gk 'Gxx(cDip%Scxt(׼^Z!MCUF3E/Duk\j#f?í,;+c6vuz=nz9ydέKQH' Me"Yh\N SXa읲:́M%*HM5JMz2ġœ&֓퓥 7#?9}<5:B"h;aN$}Vnm[gVZpʢ_nwlnV٦H}C >fYPx|k_ yY+I6MJ9J#F87K&:sC|U2s>jQjдmȔ)5fTڣ*2AM)hs~BDӤ3’fR3]MfQ>gA"AH&,",c"eر@b8$xb+IǞ=]Ţޱ͕F~tWW=i?S77gD5poJ4>NާƚB*p V.oLJ\w}{q k+t%x+GQFV*h0{c/`4|+۬/܂Ujd=~r 82$ fZi:5xwޘ iaY΍A ~wed]W E՚C6GsXt#0F ˳8᲻M]wp`Yj\?'O>*1F6FьT)ZAT &ܮ M$PA|}Q&>;γ4aޣ]y ˲ _]9sIBF|PۻbѶo}3yiYū//!5E8Mq/݉%f[['?T|ٗ^N4*K!cUQu ^śR*ԝkߜ0$sEQ頉T\9gbۥ*Nd1eVvŃê%7=eJu)ԥ& gC%:Y.bKC`ASpwal3ښ V>3ܿ_~gy V]XiUֳ kOep zTnnCt* V-i&K~(s٪V"](p%qJ~u?̅肏bzB 7+,N3cyB‘ D.prـ4FW Yhc2Ft_O VpwAcL N~A[؈H_BCQ%H.x{`@tQvb&sB\ ۞-ԁyϹk9TxkԲ !f'Gvq9+PQ+d\PA~'%}9|KTgQ+BO-6YBw&QXC$Sڎ@4ZME تMuׂcjbYgW/ Ў:{ %R>C͉;,C\]3x}xx +A,&&#aŶ z {KčMo",K.ahJ׿c oH%*ߛ2x<9 ܁hv<-h !}DfD$D@0J+Cu`G #\b`FJ, WM;?iA&^,xR؟ =УI!i^3h\lQiIgj1W2RdI]o!Cdž"p0k߰Wc@bMDp^rUIR6n[E&IGz%vs*oN(Jޔ:Y5_[?u^kGm]A$\h4iCc\ TQfN+g~TΪF@MKCUX]dB5(jzvY$Q]eHF`Omxz7V5މ^cG-H")Ew)f}2  >nM3,ݧ~_}ۧ1M"A}q"d%j^IHD^_4]6f]Hb%ۛfi0#T4'k LؠʂD*7"de(Fq6~LfltJIGWY6Mm)#M R \$!<0roΦi*ov}]1.{<yuvhjÐϲlʗ}txm___Ë/Z6.9y, !A*˷F>ONYhl6"xgpײjJB{s=,2,59ٻYϣM>Q.s5zZ,>;\t zb`0j{6)ړ:TA!AJkU E? ?} )\]@TAjH}ٳr>Z"fjp_+˸9BWLR'!,DDg+xj }t:;20+AΕ.\]ŧ7՜DߓfMdݽ "M/lM\hfs6n)1:Jht ҋ5`jY V b܌ՆŶ!ʻ(Ք5HE`Ͷ AZ]G<|$fsqP. y_fiѨ3E_׼ S.* C@~ϿTQLx5kItyfeV$.U-Aj9ӏx}{$XUVS1NO/: .S/gGcۯ95z%4X3#,4&kӭʞqţX Ұt/_^ًA.^dA"ؠU.MA-pFýRZf^:u]_{Cu `x;77ZXDoafr 5LJ*bBZ] rSK2ˆ`r7oB6cHlrzT5KӍ#c7n]"E|>_O!#:76(+3,D9\u|#jÈCR|ՠN˲xd*庮90gn\[ #Ϸ, NWpssSú - ^Ͽxn޸~0-LRlxGh-ĹU!jhUWZig^|).Wi3hO^}ut>MG'x`9{|!% !_bGFVn O=JNir>tFE!n*O)nٯ<3O.ǥO9M^?6'ge*Ol3 0BX,!'@F]gCyԃ_ [ٚKt l}]Cyӏ kRj=!"jrw*@r_Xd)%m@ <}kºZV9/WW ȭ^^oRA" )mB8 k͜N =6Rc%e/)9F/5A3tI6V "J.ih~]Vڜu2cd3- w񐛛Zr |>F|a퇙&m$3В\2@ sJ@܏Razai8: !7 9҂p>t*s׈z JD ¶$ñ6l=Ҏ`hܬ,K=x-.g#1&<=On-o"q}t D!)P)∔ WÃ{P[y?ɉ68ZVo:' W/?RQrxvS6%%kV "8]'o?, [(AAA.5= J* Cra[R` S0q _*tjɥ.VtiaF@>3[M̨!UKes39,Үo iLHQ#-&FPXj%Wy4L0%&˲Q ɫ9*+ X_\ZL$dHX>ΔRI^6Ul+ nn^ޅ@ i(t1b033s9 >]{-Vo{L\ #WM۪6yыEv,=ȏp8Tlٶm FRʂX޽{˾k_-"30A_CaZمk͹f K>T F[Uɖ˽[ON^էlP F)Eᗖփ+5xCMv4->_`DKDUqHg G?Z*[K?o '0/vzvRY3׫,M!JM奬m2xflUFFyYs>"Zt9 qN'_ooQѕr p"p>e08`AޟU7Y TT)=FQ,Lk%6޵o 1Ã}ᇿ?@>6 3]4 I?/mx' !>SA K -0bgϞRfw!DN^vpb:S^sm OYc2Q56̽d-mF2Đ1y.aRNsJ^j9?@_ |-\N P ZN;H"P?U)% eL^zLQ 4 ). V<>. ~$q,휧U^QQZ-| R|Bc!e H5PY"CRYc !VJ k,s%Nqy.7D^U{9 }jݬߦMqpD?  0oOQ@8Bc~ڴnk# jӚ v#k98i:uamXvr܇ug%a N?٫ln>#8Ϧ6W[씤ڈ:.^#ZxAބzxGܸ<xv?b?FkhH:̧'Qо 3?8teV{wׁ /~paAUih<EOa;[OqiP >{$Z9<݋amc|--}x>~o?_z{)) ж8RJYfJv2UF'va<>f}P@ HK=GunŖ1a3e0FghDImbT3Kƃ.~UI{I5xy@JzB T)lx4zR򨏃7% 1iJ{8W!aӼǸѪڅGW&jqmצ\g"9xviͬ'PEQ%xW"s/e"܈ es.0DA{v^׎p8<'IENDB`rdata-0.11.2/docs/_static/switcher.json000066400000000000000000000004271457133752200200030ustar00rootroot00000000000000[ { "name": "dev", "version": "dev", "url": "https://rdata.readthedocs.io/en/latest/" }, { "name": "0.10.0 (stable)", "version": "stable", "url": "https://rdata.readthedocs.io/en/stable/", "preferred": true } ]rdata-0.11.2/docs/_templates/000077500000000000000000000000001457133752200157645ustar00rootroot00000000000000rdata-0.11.2/docs/_templates/autosummary/000077500000000000000000000000001457133752200203525ustar00rootroot00000000000000rdata-0.11.2/docs/_templates/autosummary/base.rst000066400000000000000000000001501457133752200220120ustar00rootroot00000000000000{{ objname | escape | underline}} .. currentmodule:: {{ module }} .. auto{{ objtype }}:: {{ objname }}rdata-0.11.2/docs/_templates/autosummary/class.rst000066400000000000000000000007531457133752200222160ustar00rootroot00000000000000{{ objname | escape | underline}} .. currentmodule:: {{ module }} .. autoclass:: {{ objname }} {% block methods %} {% if methods %} .. rubric:: {{ _('Methods') }} .. autosummary:: {% for item in methods %} {% if item != "__init__" %} ~{{ name }}.{{ item }} {% endif %} {%- endfor %} {% endif %} {% for item in methods %} {% if item != "__init__" %} .. automethod:: {{ item }} {% endif %} {%- endfor %} {% endblock %}rdata-0.11.2/docs/apilist.rst000066400000000000000000000035151457133752200160320ustar00rootroot00000000000000API List ======== List of functions and structures -------------------------------- A complete list of all functions and structures provided by rdata. Convenience functions ^^^^^^^^^^^^^^^^^^^^^ Functions that read and transform a `.rds` or `.rda` file, performing parsing and conversion with one line of code. .. autosummary:: :toctree: modules rdata.read_rds rdata.read_rda Parse :code:`.rda` format ^^^^^^^^^^^^^^^^^^^^^^^^^ Functions for parsing data in the :code:`.rda` format. These functions return a structure representing the contents of the file, without transforming it to more appropriate Python objects. Thus, if a different way of converting R objects to Python objects is needed, it can be done from this structure. .. autosummary:: :toctree: modules rdata.parser.parse_file rdata.parser.parse_data Conversion of the R objects ^^^^^^^^^^^^^^^^^^^^^^^^^^^ These objects and functions convert the parsed R objects to appropriate Python objects. The Python object corresponding to a R object is chosen to preserve most original properties, but it could change in the future, if a more fitting Python object is found. .. autosummary:: :toctree: modules rdata.conversion.Converter rdata.conversion.SimpleConverter rdata.conversion.convert rdata.conversion.DEFAULT_CLASS_MAP Auxiliary structures ^^^^^^^^^^^^^^^^^^^^ These classes are used to represent R objects which have no clear analog in Python, so that the information therein can be retrieved. .. autosummary:: :toctree: modules rdata.conversion.RBuiltin rdata.conversion.RBytecode rdata.conversion.RFunction rdata.conversion.REnvironment rdata.conversion.RExpression rdata.conversion.RExternalPointer rdata.conversion.RLanguage rdata.conversion.SrcFile rdata.conversion.SrcFileCopy rdata.conversion.SrcRef rdata-0.11.2/docs/conf.py000066400000000000000000000163131457133752200151320ustar00rootroot00000000000000"""Configuration of the Sphinx documentation.""" # rdata documentation build configuration file, created by # sphinx-quickstart on Tue Aug 7 12:49:32 2018. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import importlib.metadata import os import sys import textwrap import rdata # General information about the project. project = "rdata" author = "Carlos Ramos Carreño" copyright = "2018, Carlos Ramos Carreño" # noqa: A001 github_url = "https://github.com/vnmabus/rdata" rtd_version = os.environ.get("READTHEDOCS_VERSION") rtd_version_type = os.environ.get("READTHEDOCS_VERSION_TYPE") switcher_version = rtd_version if switcher_version == "latest": switcher_version = "dev" elif rtd_version_type not in {"branch", "tag"}: switcher_version = rdata.__version__ rtd_branch = os.environ.get(" READTHEDOCS_GIT_IDENTIFIER", "develop") language = "en" try: release = importlib.metadata.version("rdata") except importlib.metadata.PackageNotFoundError: print( # noqa: T201 f"To build the documentation, The distribution information of\n" f"{project} has to be available. Either install the package\n" f"into your development environment or run 'setup.py develop'\n" f"to setup the metadata. A virtualenv is recommended!\n", ) sys.exit(1) version = ".".join(release.split(".")[:2]) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "myst_parser", "sphinx_codeautolink", "sphinx.ext.autodoc", "sphinx.ext.autosummary", "sphinx.ext.intersphinx", "sphinx.ext.mathjax", "sphinx.ext.napoleon", "sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx_gallery.gen_gallery", "jupyterlite_sphinx", # Move after sphinx gallery ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. source_suffix = ".rst" # The master toctree document. master_doc = "index" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" add_module_names = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "pydata_sphinx_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = { "use_edit_page_button": True, "github_url": github_url, "switcher": { "json_url": ( "https://rdata.readthedocs.io/en/latest/_static/switcher.json" ), "version_match": switcher_version, }, "show_version_warning_banner": True, "navbar_start": ["navbar-logo", "version-switcher"], "icon_links": [ { "name": "PyPI", "url": "https://pypi.org/project/rdata", "icon": "https://avatars.githubusercontent.com/u/2964877", "type": "url", }, { "name": "Anaconda", "url": "https://anaconda.org/conda-forge/rdata", "icon": "https://avatars.githubusercontent.com/u/3571983", "type": "url", }, ], "logo": { "text": "🗃 rdata", }, } html_context = { "github_user": "vnmabus", "github_repo": "rdata", "github_version": "develop", "doc_path": "docs", } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # -- Options for LaTeX output --------------------------------------------- latex_engine = "lualatex" # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "rdata.tex", "rdata Documentation", author, "manual"), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, "rdata", "rdata Documentation", [author], 1), ] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "rdata", "rdata documentation", author, "rdata", "Read R datasets from Python.", "Miscellaneous", ), ] # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright # A list of files that should not be packed into the epub file. epub_exclude_files = ["search.html"] # -- Options for "sphinx.ext.autodoc.typehints" -- autodoc_preserve_defaults = True autodoc_typehints = "description" # -- Options for "sphinx.ext.autosummary" -- autosummary_generate = True # -- Options for "sphinx.ext.intersphinx" -- intersphinx_mapping = { "igraph": ("https://python.igraph.org/en/stable/api", None), "matplotlib": ("https://matplotlib.org/stable", None), "numpy": ("https://numpy.org/doc/stable", None), "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), "python": (f"https://docs.python.org/{sys.version_info.major}", None), "scipy": ("https://docs.scipy.org/doc/scipy", None), "sklearn": ("https://scikit-learn.org/stable", None), "xarray": ("http://xarray.pydata.org/en/stable/", None), } # -- Options for "sphinx.ext.todo" -- todo_include_todos = True # -- Options for "sphinx_gallery.gen_gallery" -- sphinx_gallery_conf = { "examples_dirs": ["../examples"], "gallery_dirs": ["auto_examples"], "reference_url": { "rdata": None, }, "doc_module": "rdata", "jupyterlite": { "use_jupyter_lab": True, }, "first_notebook_cell": textwrap.dedent("""\ %pip install lzma %pip install rdata %pip install ipywidgets import pyodide_http pyodide_http.patch_all() """), } rdata-0.11.2/docs/contributors.md000066400000000000000000000000431457133752200167030ustar00rootroot00000000000000```{include} ../CONTRIBUTORS.md ```rdata-0.11.2/docs/conversions.rst000066400000000000000000000105331457133752200167330ustar00rootroot00000000000000Default conversions =================== This page list the default conversions applied to R objects to convert them to Python objects. Basic types ----------- The conversion of basic types is performed directly by the :class:`~rdata.conversion.Converter` used. Thus, changing the conversion for basic types currently requires creating a custom :class:`~rdata.conversion.Converter` class. The default :class:`~rdata.conversion.SimpleConverter` realizes the following conversions: ================== ================================================================================================ R object type Python conversion ================== ================================================================================================ builtin function :class:`rdata.conversion.RBuiltin`. bytecode :class:`rdata.conversion.RBytecode`. char (internal) :class:`str` or :class:`bytes` (depending on the encoding flags). closure :class:`rdata.conversion.RFunction`. complex :class:`numpy.ndarray` with 128-bits complex dtype. :class:`numpy.ma.MaskedArray` with 128-bits complex dtype if it contains NA values. :class:`xarray.DataArray` if it contains labeled dimensions. environment :class:`rdata.conversion.REnvironment`. There are three special cases: the empty, base and global environments, which are all empty by default. The base and global environments may be supplied to the converter. expression :class:`rdata.conversion.RExpression`. external pointer :class:`rdata.conversion.RExternalPointer`. integer :class:`numpy.ndarray` with 32-bits integer dtype. :class:`numpy.ma.MaskedArray` with 32-bits integer dtype if it contains NA values. :class:`xarray.DataArray` if it contains labeled dimensions. language :class:`rdata.conversion.RLanguage`. list :class:`list` (if untagged). :class:`dict` (if tagged). Empty lists are considered tagged. logical (boolean) :class:`numpy.ndarray` with boolean dtype. :class:`numpy.ma.MaskedArray` with boolean dtype if it contains NA values. :class:`xarray.DataArray` if it contains labeled dimensions. missing argument :data:`NotImplemented`. NULL :data:`None`. real :class:`numpy.ndarray` with 64-bits floating point dtype. :class:`numpy.ma.MaskedArray` with 64-bits floating point dtype if it contains NA values. :class:`xarray.DataArray` if it contains labeled dimensions. reference The referenced value, that is, an object already converted. S4 object :class:`types.SimpleNamespace`. special function :class:`rdata.conversion.RBuiltin`. string :class:`numpy.ndarray` with suitable fixed-length string dtype. symbol :class:`str`. vector :class:`list` (if untagged). :class:`dict` (if tagged). Empty lists are considered tagged. ================== ================================================================================================ Custom classes -------------- In addition, objects containing a `"class"` attribute are passed to a "constructor function", if one is available. A dictionary of constructor functions can be supplied to the converter, where the key of each element corresponds to the class name. When the `"class"` attribute contains several class names, these are tried in order. The default constructor dictionary allows to convert the following R classes: ================== ================================================================================================ R class Python conversion ================== ================================================================================================ data.frame :class:`pandas.DataFrame`. factor :class:`pandas.Categorical`. ordered :class:`pandas.Categorical` (with ordered categories). srcfile :class:`rdata.conversion.SrcFile`. srcfilecopy :class:`rdata.conversion.SrcFileCopy`. srcref :class:`rdata.conversion.SrcRef`. ts :class:`pandas.Series`. ================== ================================================================================================ rdata-0.11.2/docs/index.rst000066400000000000000000000046541457133752200155010ustar00rootroot00000000000000rdata version |version| ======================= |build-status| |docs| |coverage| |pypi| |zenodo| The package rdata offers a lightweight way to import R datasets/objects stored in the ".rda" and ".rds" formats into Python. Its main advantages are: - It is a pure Python implementation, with no dependencies on the R language or related libraries. Thus, it can be used anywhere where Python is supported, including the web using `Pyodide `_. - It attempt to support all R objects that can be meaningfully translated. As opposed to other solutions, you are no limited to import dataframes or data with a particular structure. - It allows users to easily customize the conversion of R classes to Python ones. Does your data use custom R classes? Worry no longer, as it is possible to define custom conversions to the Python classes of your choosing. - It has a permissive license (MIT). As opposed to other packages that depend on R libraries and thus need to adhere to the GPL license, you can use rdata as a dependency on MIT, BSD or even closed source projects. .. toctree:: :maxdepth: 4 :hidden: :caption: Contents: installation simpleusage apilist auto_examples/index Try online! conversions contributors The package rdata is developed `on Github `_. Please report `issues `_ there as well. .. |build-status| image:: https://github.com/vnmabus/rdata/actions/workflows/main.yml/badge.svg?branch=master :alt: build status :scale: 100% :target: https://github.com/vnmabus/rdata/actions/workflows/main.yml .. |docs| image:: https://readthedocs.org/projects/rdata/badge/?version=latest :alt: Documentation Status :scale: 100% :target: https://rdata.readthedocs.io/en/latest/?badge=latest .. |coverage| image:: http://codecov.io/github/vnmabus/rdata/coverage.svg?branch=develop :alt: Coverage Status :scale: 100% :target: https://codecov.io/gh/vnmabus/rdata/branch/develop .. |pypi| image:: https://badge.fury.io/py/rdata.svg :alt: Pypi version :scale: 100% :target: https://pypi.python.org/pypi/rdata/ .. |zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.6382237.svg :alt: Zenodo DOI :scale: 100% :target: https://doi.org/10.5281/zenodo.6382237 rdata-0.11.2/docs/installation.rst000066400000000000000000000007011457133752200170600ustar00rootroot00000000000000Installation ============ rdata is on PyPi and can be installed using :code:`pip`: .. code:: pip install rdata It is also available for :code:`conda` using the :code:`conda-forge` channel: .. code:: conda install -c conda-forge rdata Installing the develop version ------------------------------ The current version from the develop branch can be installed as .. code:: pip install git+https://github.com/vnmabus/rdata.git@developrdata-0.11.2/docs/make.bat000066400000000000000000000014511457133752200152350ustar00rootroot00000000000000@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build set SPHINXPROJ=rdata if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% :end popd rdata-0.11.2/docs/simpleusage.rst000066400000000000000000000065211457133752200167030ustar00rootroot00000000000000Simple usage ============ Read a R dataset ---------------- The common way of reading an R dataset is the following one: .. code:: python import rdata converted = rdata.read_rda(rdata.TESTDATA_PATH / "test_vector.rda") converted which results in .. code:: {'test_vector': array([1., 2., 3.])} Under the hood, this is equivalent to the following code: .. code:: python import rdata parsed = rdata.parser.parse_file(rdata.TESTDATA_PATH / "test_vector.rda") converted = rdata.conversion.convert(parsed) converted This consists on two steps: #. First, the file is parsed using the function :func:`rdata.parser.parse_file`. This provides a literal description of the file contents as a hierarchy of Python objects representing the basic R objects. This step is unambiguous and always the same. #. Then, each object must be converted to an appropriate Python object. In this step there are several choices on which Python type is the most appropriate as the conversion for a given R object. Thus, we provide a default :func:`rdata.conversion.convert` routine, which tries to select Python objects that preserve most information of the original R object. For custom R classes, it is also possible to specify conversion routines to Python objects. Convert custom R classes ------------------------ The basic :func:`~rdata.conversion.convert` routine only constructs a :class:`~rdata.conversion.SimpleConverter` object and calls its :meth:`~rdata.conversion.SimpleConverter.convert` method. All arguments of :func:`~rdata.conversion.convert` are directly passed to the :class:`~rdata.conversion.SimpleConverter` initialization method. It is possible, although not trivial, to make a custom :class:`~rdata.conversion.Converter` object to change the way in which the basic R objects are transformed to Python objects. However, a more common situation is that one does not want to change how basic R objects are converted, but instead wants to provide conversions for specific R classes. This can be done by passing a dictionary to the :class:`~rdata.conversion.SimpleConverter` initialization method, containing as keys the names of R classes and as values, callables that convert a R object of that class to a Python object. By default, the dictionary used is :data:`~rdata.conversion.DEFAULT_CLASS_MAP`, which can convert commonly used R classes such as `data.frame `_ and `factor `_. As an example, here is how we would implement a conversion routine for the factor class to :class:`bytes` objects, instead of the default conversion to Pandas :class:`~pandas.Categorical` objects: .. code:: python import rdata def factor_constructor(obj, attrs): values = [bytes(attrs['levels'][i - 1], 'utf8') if i >= 0 else None for i in obj] return values new_dict = { **rdata.conversion.DEFAULT_CLASS_MAP, "factor": factor_constructor } converted = rdata.read_rda( rdata.TESTDATA_PATH / "test_dataframe.rda", constructor_dict=new_dict, ) converted which has the following result: .. code:: {'test_dataframe': class value 1 b'a' 1 2 b'b' 2 3 b'b' 3}rdata-0.11.2/examples/000077500000000000000000000000001457133752200145155ustar00rootroot00000000000000rdata-0.11.2/examples/README.txt000066400000000000000000000000721457133752200162120ustar00rootroot00000000000000Examples ======== Examples of the package functionality. rdata-0.11.2/examples/__init__.py000066400000000000000000000000361457133752200166250ustar00rootroot00000000000000"""Documentation examples.""" rdata-0.11.2/examples/plot_cran.py000066400000000000000000000124261457133752200170550ustar00rootroot00000000000000""" Loading a RDA file with custom types from CRAN ============================================== A more advanced example showing how to read a dataset in the RDATA format from the CRAN repository of R packages that include custom R types. """ # %% # We will show how to load the graph of the classical # `seven bridges of Königsberg problem # `_ from the # R package igraphdata. # # .. warning:: # This is for illustration purposes only. If you plan to use the same # dataset repeatedly it is better to download it, or to use a package that # caches it, such as # `scikit-datasets `_. # # We will make use of the function # :external+python:func:`urllib.request.urlopen` to load the url, as well as # the package rdata. # The package is a tar file so we need also to import the # :external+python:mod:`tarfile` module. # We will use the package `igraph `_ for # constructing the graph in Python. # Finally, we will import some plotting routines from Matplotlib. import tarfile from urllib.request import urlopen import igraph import igraph.drawing import matplotlib.pyplot as plt from matplotlib.colors import to_hex import rdata # %% # The following URL contains the link to download the package from CRAN. pkg_url = ( "https://cran.r-project.org/src/contrib/Archive/" "igraphdata/igraphdata_1.0.0.tar.gz" ) # %% # The dataset is contained in the "data" folder, as it is common for # R packages. # The file is named Koenisberg and it is in the RDATA format # (.rda extension). data_path = "igraphdata/data/Koenigsberg.rda" # %% # We proceed to open the package using # :external+python:func:`~urllib.request.urlopen` # and :external+python:mod:`tarfile`. with urlopen(pkg_url) as package: with tarfile.open(fileobj=package, mode="r|gz") as package_tar: for member in package_tar: if member.name == data_path: dataset = package_tar.extractfile(member) assert dataset with dataset: parsed = rdata.parser.parse_file(dataset) break # %% # We could try to convert this dataset to Python objects. converted = rdata.conversion.convert(parsed) print(converted) # %% # From this representation, we can see that .rda files contain a mapping # of variable names to objects, and not just one object as .rds files. # In this case there is just one variable called "Koenigsberg", as the # dataset itself, but that is not necessarily always the case. # %% # We can also see that there is no default conversion for the "igraph" # class, representing a graph. # Thus, the converted object is a list of the underlying vectors used # by this type. # %% # It is however possible to define our own conversion routines for R classes # using the package rdata. # For that purpose we need to create a "constructor" function, that accepts # as arguments the underlying object to convert and its attributes, and # returns the converted object. # %% # In this example, the object will be received as a list, corresponding to # the `igraph_t structure defined by the igraph package # `_. # We will convert it to a :external+igraph:class:`~igraph.Graph` object from # the `Python version of the igraph package # `_. # The attrs dict is empty and will not be used. def graph_constructor(obj, attrs): """Construct graph object from R representation.""" n_vertices = int(obj[0][0]) is_directed = obj[1] edge_from = obj[2].astype(int) edge_to = obj[3].astype(int) # output_edge_index = obj[4] # input_edge_index = obj[5] # output_vertex_edge_index = obj[6] # input_vertex_edge_index = obj[7] graph_attrs = obj[8][1] vertex_attrs = obj[8][2] edge_attrs = obj[8][3] return igraph.Graph( n=n_vertices, directed=is_directed, edges=list(zip(edge_from, edge_to)), graph_attrs=graph_attrs, vertex_attrs=vertex_attrs, edge_attrs=edge_attrs, ) # %% # We create a dict with all the constructors that we want to apply. # In this case, we include first the default constructors (which # provide transformations for common R classes) and our newly created # constructor. # The key used for the dictionary entries should be the name of the # corresponding R class. constructor_dict = { **rdata.conversion.DEFAULT_CLASS_MAP, "igraph": graph_constructor, } # %% # We can now call the :func:`rdata.conversion.convert` functtion, supplying # the dictionary of constructors to use. converted = rdata.conversion.convert(parsed, constructor_dict=constructor_dict) # %% # Finally, we check the constructed graph by plotting it using the # :external+igraph:func:`igraph.drawing.plot` function. fig, axes = plt.subplots() plt.subplots_adjust(left=0, right=1, bottom=0, top=1) igraph.drawing.plot( converted["Koenigsberg"], target=axes, vertex_label=converted["Koenigsberg"].vs["name"], vertex_label_size=8, vertex_size=120, vertex_color=to_hex("tab:blue"), edge_label=converted["Koenigsberg"].es["name"], edge_label_size=8, ) plt.show() rdata-0.11.2/examples/plot_example.py000066400000000000000000000011411457133752200175550ustar00rootroot00000000000000# fmt: off """ R data loading ============== Use the file uploader to convert files to Python. """ # sphinx_gallery_thumbnail_path = '_static/R_logo.svg' from ipywidgets import FileUpload, interact import rdata @interact(files=FileUpload(accept="*.rd*", multiple=True)) def convert_from_file(files): """Open a rds or rdata file and display its contents as Python objects.""" for f in files: parsed = rdata.parser.parse_data(f.content) converted = rdata.conversion.convert(parsed) for key, value in converted.items(): print(f"{key}:") print(value) rdata-0.11.2/examples/plot_zenodo.py000066400000000000000000000046071457133752200174320ustar00rootroot00000000000000""" Loading a RDS file from a URL ============================= A simple example showing how to read a dataset in the RDS format from a URL. """ # sphinx_gallery_thumbnail_path = '_static/download.png' # %% # If the data to read is accesible at a particular URL, we can open it as a # file using the function :external+python:func:`urllib.request.urlopen`. # Thus, we need to import that function as well as the rdata package. from urllib.request import urlopen import rdata # %% # For this example we will use a dataset hosted at # `Zenodo `_. # This is a small dataset containing information about some fungal pathogens. # dataset_url = ( "https://zenodo.org/records/7425539/files/core_fungal_pathogens.rds" ) # %% # The object resulting from calling # :external+python:func:`~urllib.request.urlopen` can be then # passed to :func:`~rdata.parser.parse_file` as if it were a normal file. with urlopen(dataset_url) as dataset: parsed = rdata.parser.parse_file(dataset) # %% # RDS files do not have a special magic number that identifies them. # Thus, when reading a RDS file, rdata has to suppose that the file is a valid # RDS file, and warns about that. # We can omit this warning by passing manually the extension of the file # instead. with urlopen(dataset_url) as dataset: parsed = rdata.parser.parse_file(dataset, extension=".rds") # %% # This parsed object contains a lossless representation of the internal data # contained in the file. # This data mimics the internal format used in R, and is thus not directly # usable. # However, we can retrieve some information about the file that will be lost # after the conversion to a Python object, such as the version of the format # employed or the encoding used for the strings. print(parsed.versions.format) print(parsed.extra.encoding) # %% # In order to convert it to Python objects we need to use the function # :func:`rdata.conversion.convert`. converted = rdata.conversion.convert(parsed) # %% # RDS files contain just one R object. # In this particular case, it is a R dataframe object, that will be converted # to a Pandas dataframe by default. converted # %% # As usually we just want to parse and convert a given dataset, the convenience # functions :func:`rdata.read_rds` and :func:`rdata.read_rda` can be used with # that purpose. with urlopen(dataset_url) as dataset: data = rdata.read_rds(dataset) data rdata-0.11.2/pyproject.toml000066400000000000000000000066771457133752200156330ustar00rootroot00000000000000[project] name = "rdata" description = "Read R datasets from Python." readme = "README.rst" requires-python = ">=3.9" license = {file = "LICENSE"} keywords = [ "rdata", "r", "dataset", ] authors = [ {name = "Carlos Ramos Carreño", email = "vnmabus@gmail.com"}, ] maintainers = [ {name = "Carlos Ramos Carreño", email = "vnmabus@gmail.com"}, ] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: MIT License", "Natural Language :: English", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: File Formats", "Topic :: Scientific/Engineering :: Mathematics", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", ] dynamic = ["version"] dependencies = [ "numpy", "xarray", "pandas", "typing_extensions>4.4", ] [project.optional-dependencies] docs = [ "igraph", "ipywidgets", "jupyterlite-sphinx", "jupyterlite-pyodide-kernel", "matplotlib", "myst-parser", "pydata-sphinx-theme", "sphinx>=3.1", "sphinx-codeautolink", "sphinx-gallery", ] typing = [ "matplotlib>=3.8", "mypy", "pandas-stubs", ] test = [ "pytest", "pytest-cov", "numpy>=1.14", ] [project.urls] homepage = "https://github.com/vnmabus/rdata" documentation = "https://rdata.readthedocs.io" repository = "https://github.com/vnmabus/rdata" [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [tool.isort] multi_line_output = 3 include_trailing_comma = true use_parentheses = true combine_as_imports = true skip_glob = "**/plot_*.py plot_*.py" [tool.mypy] strict = true strict_equality = true implicit_reexport = true [[tool.mypy.overrides]] module = [ "igraph.*", "ipywidgets.*", ] ignore_missing_imports = true [[tool.mypy.overrides]] module = "examples.*" disallow_untyped_defs = false [tool.pytest.ini_options] addopts = "--doctest-modules --doctest-glob='*.rst'" doctest_optionflags = "NORMALIZE_WHITESPACE ELLIPSIS" norecursedirs = ".* build dist *.egg venv .svn _build docs/auto_examples examples asv_benchmarks" [tool.ruff.lint] select = [ "ALL", ] ignore = [ "ANN101", # self does not need to be typed "D212", # incompatible with D213, which is our preferred style for multiline docstrings "Q003", # do not change quotation marks to avoid escaping "PLC0414", # allow explicit re-exports "S101", # assert is allowed "TID252", # relative imports allowed ] [tool.ruff.lint.per-file-ignores] "plot_*.py" = [ "ANN", # no type hints in examples "ARG001", # Some unused args are needed "B018", # single object expressions are not useless in examples (they display the object) "D205", # examples do not have a blank line in docstring "D415", # first line in examples does not end with period "ERA001", # Commented code may be useful for the reader "S310", # URLs in examples have been validated "T201", # print allowed in examples ] "plot_cran.py" = [ "SIM117", # multiple with necessary for now ] [tool.ruff.lint.isort] combine-as-imports = true [tool.ruff.lint.pydocstyle] convention = "google" [tool.ruff.lint.pylint] max-args = 7 [tool.setuptools.packages.find] include = ["rdata*"] [tool.setuptools.dynamic] version = {attr = "rdata.__version__"}rdata-0.11.2/rdata/000077500000000000000000000000001457133752200137725ustar00rootroot00000000000000rdata-0.11.2/rdata/__init__.py000066400000000000000000000010651457133752200161050ustar00rootroot00000000000000"""rdata: Read R datasets from Python.""" from __future__ import annotations from importlib.resources import files from typing import TYPE_CHECKING, Final from . import conversion as conversion, parser as parser, testing as testing from ._read import read_rda as read_rda, read_rds as read_rds if TYPE_CHECKING: from .parser._parser import Traversable def _get_test_data_path() -> Traversable: return files(__name__) / "tests" / "data" TESTDATA_PATH: Final[Traversable] = _get_test_data_path() """ Path of the test data. """ __version__ = "0.11.2" rdata-0.11.2/rdata/_read.py000066400000000000000000000160731457133752200154250ustar00rootroot00000000000000"""Functions to perform parsing and conversion in one step.""" from __future__ import annotations from typing import TYPE_CHECKING, Any from .conversion._conversion import DEFAULT_CLASS_MAP, ConstructorDict, convert from .parser._parser import ( DEFAULT_ALTREP_MAP, AcceptableFile, AltRepConstructorMap, Traversable, parse_file, ) if TYPE_CHECKING: import os from collections.abc import MutableMapping def read_rdata( # noqa: PLR0913 file_or_path: AcceptableFile | os.PathLike[Any] | Traversable | str, *, expand_altrep: bool = True, altrep_constructor_dict: AltRepConstructorMap = DEFAULT_ALTREP_MAP, extension: str | None = None, constructor_dict: ConstructorDict = DEFAULT_CLASS_MAP, default_encoding: str | None = None, force_default_encoding: bool = False, global_environment: MutableMapping[str, Any] | None = None, base_environment: MutableMapping[str, Any] | None = None, ) -> Any: # noqa: ANN401 parsed = parse_file( file_or_path=file_or_path, expand_altrep=expand_altrep, altrep_constructor_dict=altrep_constructor_dict, extension=extension, ) return convert( parsed, constructor_dict=constructor_dict, default_encoding=default_encoding, force_default_encoding=force_default_encoding, global_environment=global_environment, base_environment=base_environment, ) def read_rds( # noqa: PLR0913 file_or_path: AcceptableFile | os.PathLike[Any] | Traversable | str, *, expand_altrep: bool = True, altrep_constructor_dict: AltRepConstructorMap = DEFAULT_ALTREP_MAP, constructor_dict: ConstructorDict = DEFAULT_CLASS_MAP, default_encoding: str | None = None, force_default_encoding: bool = False, global_environment: MutableMapping[str, Any] | None = None, base_environment: MutableMapping[str, Any] | None = None, ) -> Any: # noqa: ANN401 """ Read an RDS file, containing an R object. This is a convenience function that wraps :func:`rdata.parser.parse_file` and :func:`rdata.parser.convert`, as it is the common use case. Args: file_or_path: File in the RDS format. expand_altrep: Whether to translate ALTREPs to normal objects. altrep_constructor_dict: Dictionary mapping each ALTREP to its constructor. constructor_dict: Dictionary mapping names of R classes to constructor functions with the following prototype: .. code-block :: python def constructor(obj, attrs): ... This dictionary can be used to support custom R classes. By default, the dictionary used is :data:`~rdata.conversion._conversion.DEFAULT_CLASS_MAP` which has support for several common classes. default_encoding: Default encoding used for strings with unknown encoding. If `None`, the one stored in the file will be used, or ASCII as a fallback. force_default_encoding: Use the default encoding even if the strings specify other encoding. global_environment: Global environment to use. By default is an empty environment. base_environment: Base environment to use. By default is an empty environment. Returns: Contents of the file converted to a Python object. See Also: :func:`read_rda`: Similar function that parses a RDA or RDATA file. Examples: Parse one of the included examples, containing a dataframe >>> import rdata >>> >>> data = rdata.read_rds( ... rdata.TESTDATA_PATH / "test_dataframe.rds" ... ) >>> data class value 1 a 1 2 b 2 3 b 3 """ return read_rdata( file_or_path=file_or_path, expand_altrep=expand_altrep, altrep_constructor_dict=altrep_constructor_dict, extension=".rds", constructor_dict=constructor_dict, default_encoding=default_encoding, force_default_encoding=force_default_encoding, global_environment=global_environment, base_environment=base_environment, ) def read_rda( # noqa: PLR0913 file_or_path: AcceptableFile | os.PathLike[Any] | Traversable | str, *, expand_altrep: bool = True, altrep_constructor_dict: AltRepConstructorMap = DEFAULT_ALTREP_MAP, constructor_dict: ConstructorDict = DEFAULT_CLASS_MAP, default_encoding: str | None = None, force_default_encoding: bool = False, global_environment: MutableMapping[str, Any] | None = None, base_environment: MutableMapping[str, Any] | None = None, ) -> dict[str, Any]: """ Read an RDA or RDATA file, containing an R object. This is a convenience function that wraps :func:`rdata.parser.parse_file` and :func:`rdata.parser.convert`, as it is the common use case. Args: file_or_path: File in the RDA format. expand_altrep: Whether to translate ALTREPs to normal objects. altrep_constructor_dict: Dictionary mapping each ALTREP to its constructor. constructor_dict: Dictionary mapping names of R classes to constructor functions with the following prototype: .. code-block :: python def constructor(obj, attrs): ... This dictionary can be used to support custom R classes. By default, the dictionary used is :data:`~rdata.conversion._conversion.DEFAULT_CLASS_MAP` which has support for several common classes. default_encoding: Default encoding used for strings with unknown encoding. If `None`, the one stored in the file will be used, or ASCII as a fallback. force_default_encoding: Use the default encoding even if the strings specify other encoding. global_environment: Global environment to use. By default is an empty environment. base_environment: Base environment to use. By default is an empty environment. Returns: Contents of the file converted to a Python object. See Also: :func:`read_rds`: Similar function that parses a RDS file. Examples: Parse one of the included examples, containing a dataframe >>> import rdata >>> >>> data = rdata.read_rda( ... rdata.TESTDATA_PATH / "test_dataframe.rda" ... ) >>> data {'test_dataframe': class value 1 a 1 2 b 2 3 b 3} """ return read_rdata( # type: ignore[no-any-return] file_or_path=file_or_path, expand_altrep=expand_altrep, altrep_constructor_dict=altrep_constructor_dict, extension=".rda", constructor_dict=constructor_dict, default_encoding=default_encoding, force_default_encoding=force_default_encoding, global_environment=global_environment, base_environment=base_environment, ) rdata-0.11.2/rdata/conversion/000077500000000000000000000000001457133752200161575ustar00rootroot00000000000000rdata-0.11.2/rdata/conversion/__init__.py000066400000000000000000000015461457133752200202760ustar00rootroot00000000000000"""Utilities for converting R objects to Python ones.""" from ._conversion import ( DEFAULT_CLASS_MAP as DEFAULT_CLASS_MAP, Converter as Converter, RBuiltin as RBuiltin, RBytecode as RBytecode, REnvironment as REnvironment, RExpression as RExpression, RExternalPointer as RExternalPointer, RFunction as RFunction, RLanguage as RLanguage, SimpleConverter as SimpleConverter, SrcFile as SrcFile, SrcFileCopy as SrcFileCopy, SrcRef as SrcRef, convert as convert, convert_array as convert_array, convert_attrs as convert_attrs, convert_char as convert_char, convert_list as convert_list, convert_symbol as convert_symbol, convert_vector as convert_vector, dataframe_constructor as dataframe_constructor, factor_constructor as factor_constructor, ts_constructor as ts_constructor, ) rdata-0.11.2/rdata/conversion/_conversion.py000066400000000000000000000634241457133752200210660ustar00rootroot00000000000000from __future__ import annotations import abc import warnings from collections import ChainMap from collections.abc import Callable, Mapping, MutableMapping, Sequence from dataclasses import dataclass from fractions import Fraction from types import MappingProxyType, SimpleNamespace from typing import Any, Final, NamedTuple, Union, cast import numpy as np import pandas as pd import xarray from typing_extensions import override from .. import parser ConversionFunction = Callable[[Union[parser.RData, parser.RObject]], Any] class RLanguage(NamedTuple): """R language construct.""" elements: list[Any] attributes: Mapping[str, Any] class RExpression(NamedTuple): """R expression.""" elements: list[RLanguage] @dataclass class RBuiltin: """R builtin.""" name: str @dataclass class RFunction: """R function.""" environment: Mapping[str, Any] formals: Mapping[str, Any] | None body: RLanguage attributes: Mapping[str, Any] @property def source(self) -> str: return "\n".join(self.attributes["srcref"].srcfile.lines) @dataclass class RExternalPointer: """R bytecode.""" protected: Any tag: Any @dataclass class RBytecode: """R bytecode.""" code: xarray.DataArray constants: Sequence[Any] attributes: Mapping[str, Any] class REnvironment(ChainMap[str, Any]): """R environment.""" def __init__( self, *maps: MutableMapping[str, Any], frame: Mapping[str, Any] | None = None, ) -> None: super().__init__(*maps) self.frame = frame def convert_list( r_list: parser.RObject, conversion_function: ConversionFunction, ) -> Mapping[str, Any] | list[Any]: """ Expand a tagged R pairlist to a Python dictionary. Args: r_list: Pairlist R object, with tags. conversion_function: Conversion function to apply to the elements of the list. By default is the identity function. Returns: A dictionary with the tags of the pairwise list as keys and their corresponding values as values. See Also: convert_vector """ if r_list.info.type is parser.RObjectType.NILVALUE: return {} if r_list.info.type not in { parser.RObjectType.LIST, parser.RObjectType.LANG, }: msg = "Must receive a LIST, LANG or NILVALUE object" raise TypeError(msg) tag = None if r_list.tag is None else conversion_function(r_list.tag) cdr = conversion_function(r_list.value[1]) if tag is not None: if cdr is None: cdr = {} return {tag: conversion_function(r_list.value[0]), **cdr} if cdr is None: cdr = [] return [conversion_function(r_list.value[0]), *cdr] def convert_env( r_env: parser.RObject, conversion_function: ConversionFunction, ) -> REnvironment: """Convert environment objects.""" if r_env.info.type is not parser.RObjectType.ENV: msg = "Must receive a ENV object" raise TypeError(msg) frame = conversion_function(r_env.value.frame) enclosure = conversion_function(r_env.value.enclosure) hash_table = conversion_function(r_env.value.hash_table) dictionary = {} if hash_table is not None: for d in hash_table: if d is not None: dictionary.update(d) return REnvironment(dictionary, enclosure, frame=frame) def convert_attrs( r_obj: parser.RObject, conversion_function: ConversionFunction, ) -> Mapping[str, Any]: """ Return the attributes of an object as a Python dictionary. Args: r_obj: R object. conversion_function: Conversion function to apply to the elements of the attribute list. By default is the identity function. Returns: A dictionary with the names of the attributes as keys and their corresponding values as values. See Also: convert_list """ if r_obj.attributes: attrs = cast( Mapping[str, Any], conversion_function(r_obj.attributes), ) else: attrs = {} return attrs def convert_vector( r_vec: parser.RObject, conversion_function: ConversionFunction, attrs: Mapping[str, Any] | None = None, ) -> list[Any] | Mapping[str, Any]: """ Convert a R vector to a Python list or dictionary. If the vector has a ``names`` attribute, the result is a dictionary with the names as keys. Otherwise, the result is a Python list. Args: r_vec: R vector. conversion_function: Conversion function to apply to the elements of the vector. By default is the identity function. attrs: Attributes of the vector. Returns: A dictionary with the ``names`` of the vector as keys and their corresponding values as values. If the vector does not have an argument ``names``, then a normal Python list is returned. See Also: convert_list """ if attrs is None: attrs = {} if r_vec.info.type not in { parser.RObjectType.VEC, parser.RObjectType.EXPR, }: msg = "Must receive a VEC or EXPR object" raise TypeError(msg) value: list[Any] | Mapping[str, Any] = [ conversion_function(o) for o in r_vec.value ] # If it has the name attribute, use a dict instead field_names = attrs.get("names") if field_names is not None: value = dict(zip(field_names, value)) return value def safe_decode(byte_str: bytes, encoding: str) -> str | bytes: """Decode a (possibly malformed) string.""" try: return byte_str.decode(encoding) except UnicodeDecodeError as e: warnings.warn( # noqa: B028 f"Exception while decoding {byte_str!r}: {e}", ) return byte_str def convert_char( r_char: parser.RObject, *, default_encoding: str | None = None, force_default_encoding: bool = False, ) -> str | bytes | None: """ Decode a R character array to a Python string or bytes. The bits that signal the encoding are in the general pointer. The string can be encoded in UTF8, LATIN1 or ASCII, or can be a sequence of bytes. Args: r_char: R character array. default_encoding: Default encoding to apply when encoding info is not available. force_default_encoding: Always use the default encoding. Returns: Decoded string. See Also: convert_symbol """ if r_char.info.type is not parser.RObjectType.CHAR: msg = "Must receive a CHAR object" raise TypeError(msg) if r_char.value is None: return None assert isinstance(r_char.value, bytes) encoding = None if not force_default_encoding: if r_char.info.gp & parser.CharFlags.UTF8: encoding = "utf_8" elif r_char.info.gp & parser.CharFlags.LATIN1: encoding = "latin_1" elif r_char.info.gp & parser.CharFlags.ASCII: encoding = "ascii" elif r_char.info.gp & parser.CharFlags.BYTES: encoding = "bytes" if encoding is None: if default_encoding: encoding = default_encoding else: # Assume ASCII if no encoding is marked warnings.warn("Unknown encoding. Assumed ASCII.") # noqa: B028 encoding = "ascii" return ( r_char.value if encoding == "bytes" else safe_decode(r_char.value, encoding) ) def convert_symbol( r_symbol: parser.RObject, conversion_function: ConversionFunction, ) -> str | bytes: """ Decode a R symbol to a Python string or bytes. Args: r_symbol: R symbol. conversion_function: Conversion function to apply to the char element of the symbol. By default is the identity function. Returns: Decoded string. See Also: convert_char """ if r_symbol.info.type is parser.RObjectType.SYM: symbol = conversion_function(r_symbol.value) assert isinstance(symbol, str) return symbol msg = "Must receive a SYM object" raise TypeError(msg) def convert_array( r_array: parser.RObject, attrs: Mapping[str, Any] | None = None, ) -> np.ndarray[Any, Any] | xarray.DataArray: """ Convert a R array to a Numpy ndarray or a Xarray DataArray. If the array has attribute ``dimnames`` the output will be a Xarray DataArray, preserving the dimension names. Args: r_array: R array. attrs: Attributes of the array. Returns: Array. See Also: convert_vector """ if attrs is None: attrs = {} if r_array.info.type not in { parser.RObjectType.LGL, parser.RObjectType.INT, parser.RObjectType.REAL, parser.RObjectType.CPLX, }: msg = "Must receive an array object" raise TypeError(msg) value = r_array.value shape = attrs.get("dim") if shape is not None: # R matrix order is like FORTRAN value = np.reshape(value, shape, order="F") dimension_names = None coords = None dimnames = attrs.get("dimnames") if dimnames: if isinstance(dimnames, Mapping): dimension_names = list(dimnames.keys()) coords = dimnames else: dimension_names = [f"dim_{i}" for i, _ in enumerate(dimnames)] coords = { dimension_names[i]: d for i, d in enumerate(dimnames) if d is not None } value = xarray.DataArray( value, dims=dimension_names, coords=coords, ) return value # type: ignore [no-any-return] R_INT_MIN = -2**31 def _dataframe_column_transform(source: Any) -> Any: # noqa: ANN401 if isinstance(source, np.ndarray): if np.issubdtype(source.dtype, np.integer): return pd.Series(source, dtype=pd.Int32Dtype()).array if np.issubdtype(source.dtype, np.bool_): return pd.Series(source, dtype=pd.BooleanDtype()).array if np.issubdtype(source.dtype, np.str_): return pd.Series(source, dtype=pd.StringDtype()).array return source def dataframe_constructor( obj: Mapping[str, Any], attrs: Mapping[str, Any], ) -> pd.DataFrame: row_names = attrs["row.names"] obj = {key: _dataframe_column_transform(val) for key, val in obj.items()} # Default row names are stored as [R_INT_NA, -len] default_row_names_len = 2 index: pd.RangeIndex | tuple[str, ...] = ( pd.RangeIndex(1, abs(row_names[1]) + 1) if ( len(row_names) == default_row_names_len and isinstance(row_names, np.ma.MaskedArray) and row_names.mask[0] ) else tuple(row_names) ) return pd.DataFrame(obj, columns=obj, index=index) def _factor_constructor_internal( obj: np.ndarray[Any, np.dtype[np.integer[Any]]], attrs: Mapping[str, Any], *, ordered: bool, ) -> pd.Categorical: values = [attrs["levels"][i - 1] if i >= 0 else None for i in obj] return pd.Categorical(values, attrs["levels"], ordered=ordered) def factor_constructor( obj: np.ndarray[Any, np.dtype[np.integer[Any]]], attrs: Mapping[str, Any], ) -> pd.Categorical: """Construct a factor objects.""" return _factor_constructor_internal(obj, attrs, ordered=False) def ordered_constructor( obj: np.ndarray[Any, np.dtype[np.integer[Any]]], attrs: Mapping[str, Any], ) -> pd.Categorical: """Contruct an ordered factor.""" return _factor_constructor_internal(obj, attrs, ordered=True) def ts_constructor( obj: np.ndarray[Any, Any], attrs: Mapping[str, Any], ) -> pd.Series[Any]: """Construct a time series object.""" start, end, frequency = attrs["tsp"] frequency = int(frequency) real_start = Fraction(int(round(start * frequency)), frequency) real_end = Fraction(int(round(end * frequency)), frequency) index: np.ndarray[Any, Any] = np.arange( real_start, real_end + Fraction(1, frequency), Fraction(1, frequency), ) if frequency == 1: index = index.astype(int) return pd.Series(obj, index=index) @dataclass class SrcRef: """Reference to a source file location.""" first_line: int first_byte: int last_line: int last_byte: int first_column: int last_column: int first_parsed: int last_parsed: int srcfile: SrcFile def srcref_constructor( obj: tuple[int, int, int, int, int, int, int, int], attrs: Mapping[str, Any], ) -> SrcRef: return SrcRef(*obj, srcfile=attrs["srcfile"]) @dataclass class SrcFile: """Source file.""" filename: str file_encoding: str | None string_encoding: str | None def srcfile_constructor( obj: REnvironment, attrs: Mapping[str, Any], # noqa: ARG001 ) -> SrcFile: frame = obj.frame assert frame is not None filename = frame["filename"][0] file_encoding = frame.get("encoding") string_encoding = frame.get("Enc") return SrcFile( filename=filename, file_encoding=file_encoding, string_encoding=string_encoding, ) @dataclass class SrcFileCopy(SrcFile): """Source file with a copy of its lines.""" lines: Sequence[str] def srcfilecopy_constructor( obj: REnvironment, attrs: Mapping[str, Any], # noqa: ARG001 ) -> SrcFileCopy: frame = obj.frame assert frame is not None filename = frame["filename"][0] file_encoding = frame.get("encoding", (None,))[0] string_encoding = frame.get("Enc", (None,))[0] lines = frame["lines"] return SrcFileCopy( filename=filename, file_encoding=file_encoding, string_encoding=string_encoding, lines=lines, ) Constructor = Callable[[Any, Mapping[str, Any]], Any] ConstructorDict = Mapping[ Union[str, bytes], Constructor, ] default_class_map_dict: Final[ConstructorDict] = { "data.frame": dataframe_constructor, "factor": factor_constructor, "ordered": ordered_constructor, "ts": ts_constructor, "srcref": srcref_constructor, "srcfile": srcfile_constructor, "srcfilecopy": srcfilecopy_constructor, } #: Default mapping of constructor functions. DEFAULT_CLASS_MAP: Final = MappingProxyType(default_class_map_dict) class Converter(abc.ABC): """Interface of a class converting R objects in Python objects.""" @abc.abstractmethod def convert(self, data: parser.RData | parser.RObject) -> Any: # noqa: ANN401 """Convert a R object to a Python one.""" @dataclass class UnresolvedReference: references: MutableMapping[int, Any] index: int class SimpleConverter(Converter): """ Class converting R objects to Python objects. Args: constructor_dict: Dictionary mapping names of R classes to constructor functions with the following prototype: .. code-block :: python def constructor(obj, attrs): ... This dictionary can be used to support custom R classes. By default, the dictionary used is :data:`~rdata.conversion._conversion.DEFAULT_CLASS_MAP` which has support for several common classes. default_encoding: Default encoding used for strings with unknown encoding. If `None`, the one stored in the file will be used, or ASCII as a fallback. force_default_encoding: Use the default encoding even if the strings specify other encoding. global_environment: Global environment to use. By default is an empty environment. base_environment: Base environment to use. By default is an empty environment. """ def __init__( self, constructor_dict: ConstructorDict = DEFAULT_CLASS_MAP, *, default_encoding: str | None = None, force_default_encoding: bool = False, global_environment: MutableMapping[str, Any] | None = None, base_environment: MutableMapping[str, Any] | None = None, ) -> None: self.constructor_dict = constructor_dict self.default_encoding = default_encoding self.force_default_encoding = force_default_encoding self.global_environment = REnvironment( {} if global_environment is None else global_environment, ) self.base_environment = REnvironment( {} if base_environment is None else base_environment, ) self.empty_environment: Mapping[str, Any] = REnvironment({}) self._reset() def _reset(self) -> None: self.references: MutableMapping[int, Any] = {} self.default_encoding_used = self.default_encoding @override def convert( self, data: parser.RData | parser.RObject, ) -> Any: self._reset() return self._convert_next(data) def _convert_next( # noqa: C901, PLR0912, PLR0915 self, data: parser.RData | parser.RObject, ) -> Any: # noqa: ANN401 """Convert a R object to a Python one.""" obj: parser.RObject if isinstance(data, parser.RData): obj = data.object if self.default_encoding is None: self.default_encoding_used = data.extra.encoding else: obj = data attrs = convert_attrs(obj, self._convert_next) reference_id = id(obj) # Return the value if previously referenced value: Any = self.references.get(id(obj)) if value is not None: pass if obj.info.type == parser.RObjectType.SYM: # Return the internal string value = convert_symbol(obj, self._convert_next) elif obj.info.type == parser.RObjectType.LIST: # Expand the list and process the elements value = convert_list(obj, self._convert_next) elif obj.info.type == parser.RObjectType.CLO: assert obj.tag is not None assert obj.attributes is not None environment = self._convert_next(obj.tag) formals = self._convert_next(obj.value[0]) body = self._convert_next(obj.value[1]) attributes = self._convert_next(obj.attributes) value = RFunction( environment=environment, formals=formals, body=body, attributes=attributes, ) elif obj.info.type == parser.RObjectType.ENV: # Return a ChainMap of the environments value = convert_env(obj, self._convert_next) elif obj.info.type == parser.RObjectType.LANG: # Expand the list and process the elements, returning a # special object rlanguage_list = convert_list(obj, self._convert_next) assert isinstance(rlanguage_list, list) attributes = self._convert_next( obj.attributes, ) if obj.attributes else {} value = RLanguage(rlanguage_list, attributes) elif obj.info.type in { parser.RObjectType.SPECIAL, parser.RObjectType.BUILTIN, }: value = RBuiltin(name=obj.value.decode("ascii")) elif obj.info.type == parser.RObjectType.CHAR: # Return the internal string value = convert_char( obj, default_encoding=self.default_encoding_used, force_default_encoding=self.force_default_encoding, ) elif obj.info.type in { parser.RObjectType.LGL, parser.RObjectType.INT, parser.RObjectType.REAL, parser.RObjectType.CPLX, }: # Return the internal array value = convert_array(obj, attrs=attrs) elif obj.info.type == parser.RObjectType.STR: # Convert the internal strings value = np.array([self._convert_next(o) for o in obj.value]) elif obj.info.type == parser.RObjectType.VEC: # Convert the internal objects value = convert_vector(obj, self._convert_next, attrs=attrs) elif obj.info.type == parser.RObjectType.EXPR: rexpression_list = convert_vector( obj, self._convert_next, attrs=attrs, ) assert isinstance(rexpression_list, list) # Convert the internal objects returning a special object value = RExpression(rexpression_list) elif obj.info.type == parser.RObjectType.BCODE: value = RBytecode( code=self._convert_next(obj.value[0]), constants=[self._convert_next(c) for c in obj.value[1]], attributes=attrs, ) elif obj.info.type == parser.RObjectType.EXTPTR: value = RExternalPointer( protected=self._convert_next(obj.value[0]), tag=self._convert_next(obj.value[1]), ) elif obj.info.type == parser.RObjectType.S4: value = SimpleNamespace(**attrs) elif obj.info.type == parser.RObjectType.BASEENV: value = self.base_environment elif obj.info.type == parser.RObjectType.EMPTYENV: value = self.empty_environment elif obj.info.type == parser.RObjectType.MISSINGARG: value = NotImplemented elif obj.info.type == parser.RObjectType.GLOBALENV: value = self.global_environment elif obj.info.type == parser.RObjectType.REF: # Return the referenced value value = self.references.get(id(obj.referenced_object)) if value is None: reference_id = id(obj.referenced_object) assert obj.referenced_object is not None self.references[reference_id] = UnresolvedReference( self.references, reference_id, ) value = self._convert_next(obj.referenced_object) elif obj.info.type == parser.RObjectType.NILVALUE: value = None else: msg = f"Type {obj.info.type} not implemented" raise NotImplementedError(msg) if obj.info.object and attrs is not None: classname = attrs.get("class", ()) for i, c in enumerate(classname): constructor = self.constructor_dict.get(c, None) new_value = ( constructor(value, attrs) if constructor else NotImplemented ) if new_value is NotImplemented: missing_msg = ( f"Missing constructor for R class \"{c}\". " ) if len(classname) > (i + 1): solution_msg = ( f"The constructor for class " f"\"{classname[i+1]}\" will be " f"used instead." ) else: solution_msg = ( "The underlying R object is " "returned instead." ) warnings.warn( missing_msg + solution_msg, stacklevel=1, ) else: value = new_value break self.references[reference_id] = value return value def convert( data: parser.RData | parser.RObject, constructor_dict: ConstructorDict = DEFAULT_CLASS_MAP, *, default_encoding: str | None = None, force_default_encoding: bool = False, global_environment: MutableMapping[str, Any] | None = None, base_environment: MutableMapping[str, Any] | None = None, ) -> Any: # noqa: ANN401 """ Use the default converter (:func:`SimpleConverter`) to convert the data. Args: data: Parsed data. constructor_dict: Dictionary mapping names of R classes to constructor functions with the following prototype: .. code-block :: python def constructor(obj, attrs): ... This dictionary can be used to support custom R classes. By default, the dictionary used is :data:`~rdata.conversion._conversion.DEFAULT_CLASS_MAP` which has support for several common classes. default_encoding: Default encoding used for strings with unknown encoding. If `None`, the one stored in the file will be used, or ASCII as a fallback. force_default_encoding: Use the default encoding even if the strings specify other encoding. global_environment: Global environment to use. By default is an empty environment. base_environment: Base environment to use. By default is an empty environment. Examples: Parse one of the included examples, containing a vector >>> import rdata >>> >>> parsed = rdata.parser.parse_file( ... rdata.TESTDATA_PATH / "test_vector.rda") >>> converted = rdata.conversion.convert(parsed) >>> converted {'test_vector': array([1., 2., 3.])} Parse another example, containing a dataframe >>> import rdata >>> >>> parsed = rdata.parser.parse_file( ... rdata.TESTDATA_PATH / "test_dataframe.rda") >>> converted = rdata.conversion.convert(parsed) >>> converted {'test_dataframe': class value 1 a 1 2 b 2 3 b 3} """ return SimpleConverter( constructor_dict=constructor_dict, default_encoding=default_encoding, force_default_encoding=force_default_encoding, global_environment=global_environment, base_environment=base_environment, ).convert(data) rdata-0.11.2/rdata/parser/000077500000000000000000000000001457133752200152665ustar00rootroot00000000000000rdata-0.11.2/rdata/parser/__init__.py000066400000000000000000000004661457133752200174050ustar00rootroot00000000000000"""Utilities for parsing a rdata file.""" from ._parser import ( DEFAULT_ALTREP_MAP as DEFAULT_ALTREP_MAP, CharFlags as CharFlags, RData as RData, RObject as RObject, RObjectInfo as RObjectInfo, RObjectType as RObjectType, parse_data as parse_data, parse_file as parse_file, ) rdata-0.11.2/rdata/parser/_ascii.py000066400000000000000000000061271457133752200170750ustar00rootroot00000000000000from __future__ import annotations import io from typing import Any import numpy as np import numpy.typing as npt from ._parser import R_INT_NA, AltRepConstructorMap, Parser class ParserASCII(Parser): """Parser for data in ASCII format.""" def __init__( self, data: memoryview, *, expand_altrep: bool, altrep_constructor_dict: AltRepConstructorMap, ) -> None: super().__init__( expand_altrep=expand_altrep, altrep_constructor_dict=altrep_constructor_dict, ) self.file = io.TextIOWrapper(io.BytesIO(data), encoding="ascii") def _readline(self) -> str: r"""Read a line without trailing \n.""" return self.file.readline()[:-1] def _parse_array_values( self, dtype: npt.DTypeLike, length: int, ) -> npt.NDArray[Any]: array = np.empty(length, dtype=dtype) value: int | float | complex for i in range(length): line = self._readline() if np.issubdtype(dtype, np.integer): value = R_INT_NA if line == "NA" else int(line) elif np.issubdtype(dtype, np.floating): value = float(line) elif np.issubdtype(dtype, np.complexfloating): line2 = self._readline() value = complex(float(line), float(line2)) else: msg = f"Unknown dtype: {dtype}" raise ValueError(msg) array[i] = value return array def parse_string(self, length: int) -> bytes: # Non-ascii characters in strings are written using octal byte codes, # for example, a string 'aä' (2 chars) in UTF-8 is written as an ascii # string r'a\303\244' (9 chars). We want to transform this to a byte # string b'a\303\244' (3 bytes) corresponding to the byte # representation of the original UTF-8 string. # Let's use this string as an example to go through the code below # Read the ascii string s = self._readline() # Now s = r'a\303\244' (9 chars) # Convert characters to bytes (all characters are ascii) b = s.encode("ascii") # Now b = br'a\303\244' (9 bytes) # There is a special 'unicode_escape' encoding that does # basically two things here: # 1) interpret e.g. br'\303' (4 bytes) as a single byte b'\303' # 2) decode so-transformed byte string to a string with latin1 encoding s = b.decode("unicode_escape") # Now s = 'aä' (3 chars) # We don't really want the latter latin1 decoding step done by # the previous line of code, so we undo it by encoding in latin1 # back to bytes b = s.encode("latin1") # Now b = b'a\303\244' (3 bytes) # We return this byte representation here. Later in the code there # will be the decoding step from b'a\303\244' to 'aä', # that is, s = b.decode('utf8') assert len(b) == length return b def check_complete(self) -> None: assert self.file.read(1) == "" rdata-0.11.2/rdata/parser/_parser.py000066400000000000000000001133771457133752200173070ustar00rootroot00000000000000from __future__ import annotations import abc import bz2 import enum import gzip import lzma import os import pathlib import warnings from collections.abc import Callable, Iterator, Mapping, Sequence from dataclasses import dataclass from types import MappingProxyType from typing import ( TYPE_CHECKING, Any, Final, Protocol, Union, runtime_checkable, ) import numpy as np import numpy.typing as npt if TYPE_CHECKING: from ._ascii import ParserASCII from ._xdr import ParserXDR #: Value used to represent a missing integer in R. R_INT_NA: Final = -2**31 @runtime_checkable class BinaryFileLike(Protocol): """Protocol for binary files.""" def read(self) -> bytes: """Read the contents of the file.""" @runtime_checkable class BinaryBufferFileLike(Protocol): """Protocol for binary files.""" @property def buffer(self) -> BinaryFileLike: """Get the underlying buffer.""" AcceptableFile = Union[BinaryFileLike, BinaryBufferFileLike] try: from importlib.resources.abc import Traversable as Traversable except ImportError: @runtime_checkable class Traversable(Protocol): # type: ignore [no-redef] """Definition of Traversable protocol for Python < 3.11.""" def iterdir(self) -> Iterator[Traversable]: pass def read_bytes(self) -> bytes: pass def read_text(self, encoding: str | None = None) -> str: pass def is_dir(self) -> bool: pass def is_file(self) -> bool: pass def joinpath( self, *descendants: str | os.PathLike[str], ) -> Traversable: pass def __truediv__( self, child: str | os.PathLike[str], ) -> Traversable: pass def open( self, mode: str = "r", ) -> AcceptableFile: pass def name(self) -> str: pass class FileTypes(enum.Enum): """Type of file containing a R file.""" bzip2 = "bz2" gzip = "gzip" xz = "xz" rdata_binary_v2 = "rdata version 2 (binary)" rdata_binary_v3 = "rdata version 3 (binary)" rdata_ascii_v2 = "rdata version 2 (ascii)" rdata_ascii_v3 = "rdata version 3 (ascii)" magic_dict = { FileTypes.bzip2: b"\x42\x5a\x68", FileTypes.gzip: b"\x1f\x8b", FileTypes.xz: b"\xFD7zXZ\x00", FileTypes.rdata_binary_v2: b"RDX2\n", FileTypes.rdata_binary_v3: b"RDX3\n", FileTypes.rdata_ascii_v2: b"RDA2\n", FileTypes.rdata_ascii_v3: b"RDA3\n", } def file_type(data: memoryview) -> FileTypes | None: """Return the type of the file.""" for filetype, magic in magic_dict.items(): if data[:len(magic)] == magic: return filetype return None class RdataFormats(enum.Enum): """Format of a R file.""" XDR = "XDR" ASCII = "ASCII" ASCII_CRLF = "ASCII_CRLF" binary = "binary" format_dict: Final = MappingProxyType({ RdataFormats.XDR: b"X\n", RdataFormats.ASCII: b"A\n", RdataFormats.ASCII_CRLF: b"A\r\n", RdataFormats.binary: b"B\n", }) def rdata_format(data: memoryview) -> RdataFormats | None: """Return the format of the data.""" for format_type, magic in format_dict.items(): if data[:len(magic)] == magic: return format_type return None class RObjectType(enum.Enum): """Type of a R object.""" NIL = 0 # NULL SYM = 1 # symbols LIST = 2 # pairlists CLO = 3 # closures ENV = 4 # environments PROM = 5 # promises LANG = 6 # language objects SPECIAL = 7 # special functions BUILTIN = 8 # builtin functions CHAR = 9 # internal character strings LGL = 10 # logical vectors INT = 13 # integer vectors REAL = 14 # numeric vectors CPLX = 15 # complex vectors STR = 16 # character vectors DOT = 17 # dot-dot-dot object ANY = 18 # make “any” args work VEC = 19 # list (generic vector) EXPR = 20 # expression vector BCODE = 21 # byte code EXTPTR = 22 # external pointer WEAKREF = 23 # weak reference RAW = 24 # raw vector S4 = 25 # S4 classes not of simple type ALTREP = 238 # Alternative representations ATTRLIST = 239 # Bytecode attribute ATTRLANG = 240 # Bytecode attribute BASEENV = 241 # Base environment EMPTYENV = 242 # Empty environment BCREPREF = 243 # Bytecode repetition reference BCREPDEF = 244 # Bytecode repetition definition MISSINGARG = 251 # Missinf argument GLOBALENV = 253 # Global environment NILVALUE = 254 # NIL value REF = 255 # Reference BYTECODE_SPECIAL_SET: Final = frozenset(( RObjectType.BCODE, RObjectType.BCREPREF, RObjectType.BCREPDEF, RObjectType.LANG, RObjectType.LIST, RObjectType.ATTRLANG, RObjectType.ATTRLIST, )) class CharFlags(enum.IntFlag): """Flags for R objects of type char.""" HAS_HASH = 1 BYTES = 1 << 1 LATIN1 = 1 << 2 UTF8 = 1 << 3 CACHED = 1 << 5 ASCII = 1 << 6 @dataclass class RVersions: """R versions.""" format: int serialized: int minimum: int @dataclass class RExtraInfo: """ Extra information. Contains the default encoding (only in version 3). """ encoding: str | None = None @dataclass class RObjectInfo: """Internal attributes of a R object.""" type: RObjectType object: bool attributes: bool tag: bool gp: int reference: int def _str_internal( # noqa: PLR0912, C901 obj: RObject | Sequence[RObject], indent: int = 0, used_references: set[int] | None = None, ) -> str: if used_references is None: used_references = set() small_indent = indent + 2 big_indent = indent + 4 indent_spaces = " " * indent small_indent_spaces = " " * small_indent big_indent_spaces = " " * big_indent string = "" if isinstance(obj, Sequence): string += f"{indent_spaces}[\n" for elem in obj: string += _str_internal( elem, big_indent, used_references.copy(), ) string += f"{indent_spaces}]\n" return string string += f"{indent_spaces}{obj.info.type}\n" if obj.tag: tag_string = _str_internal( obj.tag, big_indent, used_references.copy(), ) string += f"{small_indent_spaces}tag:\n{tag_string}\n" if obj.info.reference: assert obj.referenced_object reference_string = ( f"{big_indent_spaces}..." if obj.info.reference in used_references else _str_internal( obj.referenced_object, indent + 4, used_references.copy()) ) string += ( f"{small_indent_spaces}reference: " f"{obj.info.reference}\n{reference_string}\n" ) string += f"{small_indent_spaces}value:\n" if isinstance(obj.value, RObject): string += _str_internal( obj.value, big_indent, used_references.copy(), ) elif isinstance(obj.value, (tuple, list)): for elem in obj.value: string += _str_internal( elem, big_indent, used_references.copy(), ) elif isinstance(obj.value, np.ndarray): max_displayed_elements: Final = 4 string += big_indent_spaces if len(obj.value) > max_displayed_elements: string += ( f"[{obj.value[0]}, {obj.value[1]} ... " f"{obj.value[-2]}, {obj.value[-1]}]\n" ) else: string += f"{obj.value}\n" else: string += f"{big_indent_spaces}{obj.value}\n" if obj.attributes: attr_string = _str_internal( obj.attributes, big_indent, used_references.copy(), ) string += f"{small_indent_spaces}attributes:\n{attr_string}\n" return string @dataclass class RObject: """Representation of a R object.""" info: RObjectInfo value: Any attributes: RObject | None tag: RObject | None = None referenced_object: RObject | None = None def __str__(self) -> str: return _str_internal(self) @dataclass class RData: """Data contained in a R file.""" versions: RVersions extra: RExtraInfo object: RObject def __str__(self) -> str: return ( "RData(\n" f" versions: {self.versions}\n" f" extra: {self.extra}\n" f" object: \n{_str_internal(self.object, indent=4)}\n" ")\n" ) @dataclass class EnvironmentValue: """Value of an environment.""" locked: bool enclosure: RObject frame: RObject hash_table: RObject AltRepConstructor = Callable[ [RObject], tuple[RObjectInfo, Any], ] AltRepConstructorMap = Mapping[bytes, AltRepConstructor] def format_float_with_scipen(number: float, scipen: int) -> bytes: """Format a floating point value as in R.""" fixed = np.format_float_positional(number, trim="-") scientific = np.format_float_scientific(number, trim="-") assert isinstance(fixed, str) assert isinstance(scientific, str) return ( scientific if len(fixed) - len(scientific) > scipen else fixed ).encode() def deferred_string_constructor( state: RObject, ) -> tuple[RObjectInfo, Any]: """Expand a deferred string ALTREP.""" new_info = RObjectInfo( type=RObjectType.STR, object=False, attributes=False, tag=False, gp=0, reference=0, ) object_to_format = state.value[0].value scipen = state.value[1].value value = [ RObject( info=RObjectInfo( type=RObjectType.CHAR, object=False, attributes=False, tag=False, gp=CharFlags.ASCII, reference=0, ), value=format_float_with_scipen(num, scipen), attributes=None, tag=None, referenced_object=None, ) for num in object_to_format ] return new_info, value def compact_seq_constructor( state: RObject, *, is_int: bool = False, ) -> tuple[RObjectInfo, Any]: """Expand a compact_seq ALTREP.""" new_info = RObjectInfo( type=RObjectType.INT if is_int else RObjectType.REAL, object=False, attributes=False, tag=False, gp=0, reference=0, ) n = int(state.value[0]) start = state.value[1] step = state.value[2] if is_int: start = int(start) step = int(step) # Calculate stop with integer arithmetic # and use built-in range() for numerical stability stop = start + (n - 1) * step value = np.array(range(start, stop + 1, step)) else: # Calculate stop with floating-point arithmetic stop = start + (n - 1) * step value = np.linspace(start, stop, n) return new_info, value def compact_intseq_constructor( state: RObject, ) -> tuple[RObjectInfo, Any]: """Expand a compact_intseq ALTREP.""" return compact_seq_constructor(state, is_int=True) def compact_realseq_constructor( state: RObject, ) -> tuple[RObjectInfo, Any]: """Expand a compact_realseq ALTREP.""" return compact_seq_constructor(state, is_int=False) def wrap_constructor( state: RObject, ) -> tuple[RObjectInfo, Any]: """Expand any wrap_* ALTREP.""" new_info = RObjectInfo( type=state.value[0].info.type, object=False, attributes=False, tag=False, gp=0, reference=0, ) value = state.value[0].value return new_info, value default_altrep_map_dict: Final[Mapping[bytes, AltRepConstructor]] = { b"deferred_string": deferred_string_constructor, b"compact_intseq": compact_intseq_constructor, b"compact_realseq": compact_realseq_constructor, b"wrap_real": wrap_constructor, b"wrap_string": wrap_constructor, b"wrap_logical": wrap_constructor, b"wrap_integer": wrap_constructor, b"wrap_complex": wrap_constructor, b"wrap_raw": wrap_constructor, } DEFAULT_ALTREP_MAP: Final = MappingProxyType(default_altrep_map_dict) class Parser(abc.ABC): """Parser interface for a R file.""" def __init__( self, *, expand_altrep: bool = True, altrep_constructor_dict: AltRepConstructorMap = DEFAULT_ALTREP_MAP, ) -> None: self.expand_altrep = expand_altrep self.altrep_constructor_dict = altrep_constructor_dict def _parse_array( self, dtype: npt.DTypeLike, ) -> npt.NDArray[Any]: """Parse an array composed of an integer (array size) and values.""" length = self.parse_int() return self._parse_array_values(dtype, length) @abc.abstractmethod def _parse_array_values( self, dtype: npt.DTypeLike, length: int, ) -> npt.NDArray[Any]: """Parse values of an array.""" def parse_bool(self) -> bool: """Parse a boolean.""" return bool(self.parse_int()) def parse_int(self) -> int: """Parse an integer.""" return int(self._parse_array_values(np.int32, 1)[0]) def parse_nullable_bool_array( self, *, fill_value: bool = True, ) -> npt.NDArray[np.bool_] | np.ma.MaskedArray[Any, Any]: """Parse a boolean array.""" return self.parse_nullable_int_array( fill_value=fill_value, ).astype(np.bool_) def parse_nullable_int_array( self, *, fill_value: int = R_INT_NA, ) -> npt.NDArray[np.int32] | np.ma.MaskedArray[Any, Any]: """Parse an integer array.""" data = self._parse_array(np.int32) mask = (data == R_INT_NA) data[mask] = fill_value if np.any(mask): return np.ma.array( # type: ignore [no-untyped-call,no-any-return] data=data, mask=mask, fill_value=fill_value, ) return data def parse_double_array(self) -> npt.NDArray[np.float64]: """Parse a double array.""" return self._parse_array(np.float64) def parse_complex_array(self) -> npt.NDArray[np.complex128]: """Parse a complex array.""" return self._parse_array(np.complex128) @abc.abstractmethod def parse_string(self, length: int) -> bytes: """Parse a string.""" def check_complete(self) -> None: """Check that parsing was completed.""" return def parse_all(self) -> RData: """Parse all the file.""" versions = self.parse_versions() extra_info = self.parse_extra_info(versions) obj = self.parse_R_object() return RData(versions, extra_info, obj) def parse_versions(self) -> RVersions: """Parse the versions header.""" format_version = self.parse_int() r_version = self.parse_int() minimum_r_version = self.parse_int() if format_version not in {2, 3}: msg = f"Format version {format_version} unsupported" raise NotImplementedError(msg) return RVersions(format_version, r_version, minimum_r_version) def parse_extra_info(self, versions: RVersions) -> RExtraInfo: """ Parse the extra info. Parses the encoding in version 3 format. """ encoding = None minimum_version_with_encoding = 3 if versions.format >= minimum_version_with_encoding: encoding_len = self.parse_int() encoding = self.parse_string(encoding_len).decode("ASCII") return RExtraInfo(encoding) def expand_altrep_to_object( self, info: RObject, state: RObject, ) -> tuple[RObjectInfo, Any]: """Expand alternative representation to normal object.""" assert info.info.type == RObjectType.LIST class_sym = info.value[0] while class_sym.info.type == RObjectType.REF: class_sym = class_sym.referenced_object assert class_sym.info.type == RObjectType.SYM assert class_sym.value.info.type == RObjectType.CHAR altrep_name = class_sym.value.value assert isinstance(altrep_name, bytes) constructor = self.altrep_constructor_dict[altrep_name] return constructor(state) def _parse_bytecode_constant( self, reference_list: list[RObject] | None, bytecode_rep_list: list[RObject | None] | None = None, ) -> RObject: obj_type = self.parse_int() return self.parse_R_object( reference_list, bytecode_rep_list, info_int=obj_type, ) def _parse_bytecode( self, reference_list: list[RObject] | None, bytecode_rep_list: list[RObject | None] | None = None, ) -> tuple[RObject, Sequence[RObject]]: """Parse R bytecode.""" if bytecode_rep_list is None: n_repeated = self.parse_int() code = self.parse_R_object(reference_list, bytecode_rep_list) if bytecode_rep_list is None: bytecode_rep_list = [None] * n_repeated n_constants = self.parse_int() constants = [ self._parse_bytecode_constant( reference_list, bytecode_rep_list, ) for _ in range(n_constants) ] return (code, constants) def parse_R_object( # noqa: N802, C901, PLR0912, PLR0915 self, reference_list: list[RObject] | None = None, bytecode_rep_list: list[RObject | None] | None = None, info_int: int | None = None, ) -> RObject: """Parse a R object.""" if reference_list is None: # Index is 1-based, so we insert a dummy object reference_list = [] original_info_int = info_int if ( info_int is not None and RObjectType(info_int) in BYTECODE_SPECIAL_SET ): info = parse_r_object_info(info_int) info.tag = info.type not in { RObjectType.BCREPREF, RObjectType.BCODE, } else: info_int = self.parse_int() info = parse_r_object_info(info_int) tag = None attributes = None referenced_object = None bytecode_rep_position = -1 tag_read = False attributes_read = False add_reference = False result = None value: Any if info.type == RObjectType.BCREPDEF: assert bytecode_rep_list bytecode_rep_position = self.parse_int() info.type = RObjectType(self.parse_int()) if info.type == RObjectType.NIL: value = None elif info.type == RObjectType.SYM: # Read Char value = self.parse_R_object(reference_list, bytecode_rep_list) # Symbols can be referenced add_reference = True elif info.type in { RObjectType.LIST, RObjectType.LANG, RObjectType.CLO, RObjectType.PROM, RObjectType.DOT, RObjectType.ATTRLANG, }: if info.type is RObjectType.ATTRLANG: info.type = RObjectType.LANG info.attributes = True tag = None if info.attributes: attributes = self.parse_R_object( reference_list, bytecode_rep_list, ) attributes_read = True if info.tag: tag = self.parse_R_object(reference_list, bytecode_rep_list) tag_read = True # Read CAR and CDR car = self.parse_R_object( reference_list, bytecode_rep_list, info_int=( None if original_info_int is None else self.parse_int() ), ) cdr = self.parse_R_object( reference_list, bytecode_rep_list, info_int=( None if original_info_int is None else self.parse_int() ), ) value = (car, cdr) elif info.type == RObjectType.ENV: info.object = True result = RObject( info=info, tag=tag, attributes=attributes, value=None, referenced_object=referenced_object, ) reference_list.append(result) locked = self.parse_bool() enclosure = self.parse_R_object(reference_list, bytecode_rep_list) frame = self.parse_R_object(reference_list, bytecode_rep_list) hash_table = self.parse_R_object(reference_list, bytecode_rep_list) attributes = self.parse_R_object(reference_list, bytecode_rep_list) value = EnvironmentValue( locked=locked, enclosure=enclosure, frame=frame, hash_table=hash_table, ) elif info.type in {RObjectType.SPECIAL, RObjectType.BUILTIN}: length = self.parse_int() if length > 0: value = self.parse_string(length=length) elif info.type == RObjectType.CHAR: length = self.parse_int() if length > 0: value = self.parse_string(length=length) elif length == 0: value = b"" elif length == -1: value = None else: msg = f"Length of CHAR cannot be {length}" raise NotImplementedError(msg) elif info.type == RObjectType.LGL: value = self.parse_nullable_bool_array() elif info.type == RObjectType.INT: value = self.parse_nullable_int_array() elif info.type == RObjectType.REAL: value = self.parse_double_array() elif info.type == RObjectType.CPLX: value = self.parse_complex_array() elif info.type in { RObjectType.STR, RObjectType.VEC, RObjectType.EXPR, }: length = self.parse_int() value = [None] * length for i in range(length): value[i] = self.parse_R_object( reference_list, bytecode_rep_list) elif info.type == RObjectType.BCODE: value = self._parse_bytecode(reference_list, bytecode_rep_list) tag_read = True elif info.type == RObjectType.EXTPTR: result = RObject( info=info, tag=tag, attributes=attributes, value=None, referenced_object=referenced_object, ) reference_list.append(result) protected = self.parse_R_object( reference_list, bytecode_rep_list, ) extptr_tag = self.parse_R_object( reference_list, bytecode_rep_list, ) value = (protected, extptr_tag) elif info.type == RObjectType.S4: value = None elif info.type == RObjectType.ALTREP: altrep_info = self.parse_R_object( reference_list, bytecode_rep_list, ) altrep_state = self.parse_R_object( reference_list, bytecode_rep_list, ) altrep_attr = self.parse_R_object( reference_list, bytecode_rep_list, ) if self.expand_altrep: info, value = self.expand_altrep_to_object( info=altrep_info, state=altrep_state, ) attributes = altrep_attr else: value = (altrep_info, altrep_state, altrep_attr) elif info.type == RObjectType.BASEENV: # noqa: SIM114 value = None elif info.type == RObjectType.EMPTYENV: value = None elif info.type == RObjectType.BCREPREF: assert bytecode_rep_list position = self.parse_int() result = bytecode_rep_list[position] assert result return result elif info.type == RObjectType.MISSINGARG: # noqa: SIM114 value = None elif info.type == RObjectType.GLOBALENV: # noqa: SIM114 value = None elif info.type == RObjectType.NILVALUE: value = None elif info.type == RObjectType.REF: value = None # Index is 1-based referenced_object = reference_list[info.reference - 1] else: msg = f"Type {info.type} not implemented" raise NotImplementedError(msg) if info.tag and not tag_read: warnings.warn( # noqa: B028 f"Tag not implemented for type {info.type} " "and ignored", ) if info.attributes and not attributes_read: attributes = self.parse_R_object(reference_list, bytecode_rep_list) if result is None: result = RObject( info=info, tag=tag, attributes=attributes, value=value, referenced_object=referenced_object, ) else: result.info = info result.attributes = attributes result.value = value result.referenced_object = referenced_object if add_reference: reference_list.append(result) if bytecode_rep_position >= 0: assert bytecode_rep_list bytecode_rep_list[bytecode_rep_position] = result return result def parse_file( file_or_path: AcceptableFile | os.PathLike[Any] | Traversable | str, *, expand_altrep: bool = True, altrep_constructor_dict: AltRepConstructorMap = DEFAULT_ALTREP_MAP, extension: str | None = None, ) -> RData: """ Parse a R file (.rda or .rdata). Args: file_or_path: File in the R serialization format. expand_altrep: Whether to translate ALTREPs to normal objects. altrep_constructor_dict: Dictionary mapping each ALTREP to its constructor. extension: Extension of the file. Returns: Data contained in the file (versions and object). See Also: :func:`parse_data`: Similar function that receives the data directly. Examples: Parse one of the included examples, containing a vector >>> import rdata >>> >>> parsed = rdata.parser.parse_file( ... rdata.TESTDATA_PATH / "test_vector.rda") >>> parsed RData(versions=RVersions(format=2, serialized=196610, minimum=131840), extra=RExtraInfo(encoding=None), object=RObject(info=RObjectInfo(type=, object=False, attributes=False, tag=True, gp=0, reference=0), value=(RObject(info=RObjectInfo(type=, object=False, attributes=False, tag=False, gp=0, reference=0), value=array([1., 2., 3.]), attributes=None, tag=None, referenced_object=None), RObject(info=RObjectInfo(type=, object=False, attributes=False, tag=False, gp=0, reference=0), value=None, attributes=None, tag=None, referenced_object=None)), attributes=None, tag=RObject(info=RObjectInfo(type=, object=False, attributes=False, tag=False, gp=0, reference=0), value=RObject(info=RObjectInfo(\ type=, object=False, attributes=False, tag=False, gp=64, reference=0), value=b'test_vector', attributes=None, tag=None, referenced_object=None), attributes=None, tag=None, referenced_object=None), referenced_object=None)) """ path = None if isinstance(file_or_path, Traversable): path = file_or_path elif isinstance(file_or_path, (os.PathLike, str)): path = pathlib.Path(file_or_path) else: # file is a pre-opened file binary_file = ( file_or_path.buffer if isinstance(file_or_path, BinaryBufferFileLike) else file_or_path ) data = binary_file.read() if path is not None: # file was a path-like if extension is None: extension = getattr(path, "suffix", None) data = path.read_bytes() return parse_data( data, expand_altrep=expand_altrep, altrep_constructor_dict=altrep_constructor_dict, extension=extension, ) def parse_data( data: bytes, *, expand_altrep: bool = True, altrep_constructor_dict: AltRepConstructorMap = DEFAULT_ALTREP_MAP, extension: str | None = None, ) -> RData: """ Parse the data of a R file, received as a sequence of bytes. Args: data: Data extracted of a R file. expand_altrep: Whether to translate ALTREPs to normal objects. altrep_constructor_dict: Dictionary mapping each ALTREP to its constructor. extension: Extension of the file. Returns: Data contained in the file (versions and object). See Also: :func:`parse_file`: Similar function that parses a file directly. Examples: Parse one of the included examples, containing a vector >>> import rdata >>> >>> with open(rdata.TESTDATA_PATH / "test_vector.rda", "rb") as f: ... parsed = rdata.parser.parse_data(f.read()) >>> >>> parsed RData(versions=RVersions(format=2, serialized=196610, minimum=131840), extra=RExtraInfo(encoding=None), object=RObject(info=RObjectInfo(type=, object=False, attributes=False, tag=True, gp=0, reference=0), value=(RObject(info=RObjectInfo(type=, object=False, attributes=False, tag=False, gp=0, reference=0), value=array([1., 2., 3.]), attributes=None, tag=None, referenced_object=None), RObject(info=RObjectInfo(type=, object=False, attributes=False, tag=False, gp=0, reference=0), value=None, attributes=None, tag=None, referenced_object=None)), attributes=None, tag=RObject(info=RObjectInfo(type=, object=False, attributes=False, tag=False, gp=0, reference=0), value=RObject(info=RObjectInfo(\ type=, object=False, attributes=False, tag=False, gp=64, reference=0), value=b'test_vector', attributes=None, tag=None, referenced_object=None), attributes=None, tag=None, referenced_object=None), referenced_object=None)) """ view = memoryview(data) filetype = file_type(view) parse_function = ( parse_rdata_binary if filetype in { FileTypes.rdata_binary_v2, FileTypes.rdata_binary_v3, FileTypes.rdata_ascii_v2, FileTypes.rdata_ascii_v3, None, } else parse_data ) if filetype is FileTypes.bzip2: new_data = bz2.decompress(data) elif filetype is FileTypes.gzip: new_data = gzip.decompress(data) elif filetype is FileTypes.xz: new_data = lzma.decompress(data) elif filetype in {FileTypes.rdata_binary_v2, FileTypes.rdata_binary_v3, FileTypes.rdata_ascii_v2, FileTypes.rdata_ascii_v3, }: if extension == ".rds": warnings.warn( # noqa: B028 f"Wrong extension {extension} for file in RDATA format", ) view = view[len(magic_dict[filetype]):] new_data = view else: new_data = view if extension != ".rds": warnings.warn("Unknown file type: assumed RDS") # noqa: B028 if extension not in {None, ".rds"}: warnings.warn(f"Wrong extension {extension} for file in RDS format") # noqa: B028 return parse_function( new_data, # type: ignore [arg-type] expand_altrep=expand_altrep, altrep_constructor_dict=altrep_constructor_dict, extension=extension, ) def parse_rdata_binary( data: memoryview, *, expand_altrep: bool = True, altrep_constructor_dict: AltRepConstructorMap = DEFAULT_ALTREP_MAP, extension: str | None = None, # noqa: ARG001 ) -> RData: """Select the appropiate parser and parse all the info.""" format_type = rdata_format(data) if format_type: data = data[len(format_dict[format_type]):] Parser: type[ParserXDR | ParserASCII] # noqa: N806 if format_type is RdataFormats.XDR: from ._xdr import ParserXDR as Parser elif format_type in (RdataFormats.ASCII, RdataFormats.ASCII_CRLF): from ._ascii import ParserASCII as Parser else: msg = "Unknown file format" raise NotImplementedError(msg) parser = Parser( data, expand_altrep=expand_altrep, altrep_constructor_dict=altrep_constructor_dict, ) r_data = parser.parse_all() parser.check_complete() return r_data def bits(data: int, start: int, stop: int) -> int: """Read bits [start, stop) of an integer.""" count = stop - start mask = ((1 << count) - 1) << start bitvalue = data & mask return bitvalue >> start def is_special_r_object_type(r_object_type: RObjectType) -> bool: """Check if a R type has a different serialization than the usual one.""" return ( r_object_type is RObjectType.NILVALUE or r_object_type is RObjectType.REF ) def parse_r_object_info(info_int: int) -> RObjectInfo: """Parse the internal information of an object.""" type_exp = RObjectType(bits(info_int, 0, 8)) reference = 0 if is_special_r_object_type(type_exp): object_flag = False attributes = False tag = False gp = 0 else: object_flag = bool(bits(info_int, 8, 9)) attributes = bool(bits(info_int, 9, 10)) tag = bool(bits(info_int, 10, 11)) gp = bits(info_int, 12, 28) if type_exp == RObjectType.REF: reference = bits(info_int, 8, 32) return RObjectInfo( type=type_exp, object=object_flag, attributes=attributes, tag=tag, gp=gp, reference=reference, ) rdata-0.11.2/rdata/parser/_xdr.py000066400000000000000000000022171457133752200165760ustar00rootroot00000000000000from __future__ import annotations import io from typing import Any import numpy as np import numpy.typing as npt from ._parser import AltRepConstructorMap, Parser class ParserXDR(Parser): """Parser for data in XDR format.""" def __init__( self, data: memoryview, *, expand_altrep: bool, altrep_constructor_dict: AltRepConstructorMap, ) -> None: super().__init__( expand_altrep=expand_altrep, altrep_constructor_dict=altrep_constructor_dict, ) self.file = io.BytesIO(data) def _parse_array_values( self, dtype: npt.DTypeLike, length: int, ) -> npt.NDArray[Any]: dtype = np.dtype(dtype) buffer = self.file.read(length * dtype.itemsize) # Read in big-endian order and convert to native byte order return np.frombuffer( buffer, dtype=dtype.newbyteorder(">"), ).astype(dtype, copy=False) def parse_string(self, length: int) -> bytes: return self.file.read(length) def check_complete(self) -> None: assert self.file.read(1) == b"" rdata-0.11.2/rdata/py.typed000066400000000000000000000001001457133752200154600ustar00rootroot00000000000000# Marker file for PEP 561. The rdata package uses inline types.rdata-0.11.2/rdata/testing.py000066400000000000000000000025161457133752200160250ustar00rootroot00000000000000"""Utilities for testing with R files.""" from __future__ import annotations import subprocess import tempfile from typing import Any, Protocol R_CODE_PREFIX = """::: """ class HasDoc(Protocol): """Python object having a docstring.""" __doc__: str | None def get_data_source( function_or_class: HasDoc, *, prefix: str = R_CODE_PREFIX, ) -> str: """Get the part of the docstring containing the data source.""" doc = function_or_class.__doc__ if doc is None: return "" source = "" for line in doc.splitlines(keepends=True): stripped_line = line.lstrip() if stripped_line.startswith(prefix): source += stripped_line.removeprefix(prefix) return source def execute_r_data_source( function_or_class: HasDoc, *, prefix: str = R_CODE_PREFIX, **kwargs: Any, # noqa: ANN401 ) -> None: """Execute R data source.""" source = get_data_source( function_or_class, prefix=prefix, ) if not source: return inits = "" for key, value in kwargs.items(): inits += f"{key} <- {value!r}\n" source = inits + source with tempfile.NamedTemporaryFile("w") as file: file.write(source) file.flush() subprocess.check_call( ["Rscript", file.name], # noqa: S603, S607 ) rdata-0.11.2/rdata/tests/000077500000000000000000000000001457133752200151345ustar00rootroot00000000000000rdata-0.11.2/rdata/tests/__init__.py000066400000000000000000000000431457133752200172420ustar00rootroot00000000000000"""Tests for the rdata package.""" rdata-0.11.2/rdata/tests/data/000077500000000000000000000000001457133752200160455ustar00rootroot00000000000000rdata-0.11.2/rdata/tests/data/test_altrep_compact_intseq.rda000066400000000000000000000001761457133752200241600ustar00rootroot00000000000000 r0b```f`a`e`f2XCCt-XF'*I-.O))J-O-HL.+)N-ʾbd|*eYSb`q~d` b'crdata-0.11.2/rdata/tests/data/test_altrep_compact_intseq_asymmetric.rda000066400000000000000000000002151457133752200264070ustar00rootroot00000000000000 r0b```f`afd`f2XCCt-XFN ZZ\SRZ[\WRZX\ZR T5*FeIJ,N!?N 5=rdata-0.11.2/rdata/tests/data/test_altrep_compact_realseq.rda000066400000000000000000000002001457133752200242750ustar00rootroot00000000000000 r0b```f`a`e`f2XCCt-XF'.I-.O))J-O-HL./JM)N-JbdJYSb`q> 83ȩrdata-0.11.2/rdata/tests/data/test_altrep_compact_realseq_asymmetric.rda000066400000000000000000000002201457133752200265340ustar00rootroot00000000000000 r0b```f`afd`f2XCCt-XFN VZ\SRZ[\_SZX\ZR TuFfIJ,NfvPc" krdata-0.11.2/rdata/tests/data/test_altrep_deferred_string.rda000066400000000000000000000002561457133752200243140ustar00rootroot00000000000000 r0b```f`a`e`f2XCCt-XF'.I-.O))J-OIMK-*JM/.)KJbdJYSb`q bN `; Y`ʷHq{)Geu8# P;5rdata-0.11.2/rdata/tests/data/test_altrep_wrap_logical.rda000066400000000000000000000001751457133752200236110ustar00rootroot00000000000000 r0b```f`a`e`f2XCCt-XF'(I-.O))J-//J,OLNʽbd}ʈ1ne}Ē.Һt#rdata-0.11.2/rdata/tests/data/test_dataframe_rownames.rda000066400000000000000000000003301457133752200234270ustar00rootroot00000000000000]O P$B@m!nD;~55.ysfepz{ƍ6,8T”0s*NO>$ld 8p*b2&W$ś*<)L EZ46`^ݯ;aBd3IHjcI Ff9rdata-0.11.2/rdata/tests/data/test_dataframe_v3.rda000066400000000000000000000002721457133752200221310ustar00rootroot00000000000000]P 0 m"'~~xMNΫ?jڭ=$}/}/ =o80`ENyWj,%@%k#`dRlN鹋?g!(&s5n>o% 55"4Q#YA+ Q>Ed+HߔYlwͦ/%AgTrdata-0.11.2/rdata/tests/data/test_dataframe_v3.rds000066400000000000000000000002441457133752200221520ustar00rootroot00000000000000]NI0 t6VBB x҅+/I>8qf<@ ^/3͎D$}ciź!Lo:cM'⑊xmFteXcBXPvU ޓ%^&,rdata-0.11.2/rdata/tests/data/test_empty_function.rda000066400000000000000000000005031457133752200226350ustar00rootroot00000000000000RKk@Fz h=xꥇKkM$@LB6KNY#xdٙ}3 0uZh0"pz12s= Z⩗2: ckJu dh ΏbA8})xМ{7qK| R'gKg+ ^2R^/ABQM$_CSrN|9Q"^H#ɧIXgNJͮoqȞ ҚpͧWRSj_!_iG*ls|j| ]H#FOKСrdata-0.11.2/rdata/tests/data/test_empty_function_uncompiled.rda000066400000000000000000000004171457133752200250600ustar00rootroot00000000000000 r0b```f`a@&khXH˕ħTƧ%d܂̜6f4lEEi@Sy< 2և|F4@i@L5'3/@4N]Bu- KsSb#P'Tbj4+/fb`J!zH|h2'VtpU9b)F0cx&X ?rdata-0.11.2/rdata/tests/data/test_empty_str.rda000066400000000000000000000001101457133752200216120ustar00rootroot00000000000000 r0b```b`fcf`b2Y# '+I-.O-(/.) ɂ?{$ =yQ>V1-وJɢdnR(]ٙoN.ϺnL m4>@Xz=f<C/aύT>$mmt.hKx M ؁2F]qe5N; y>pa1Ze CibS'. rV7d JĴYh .nJ VQvԲ}{ hilen*gzYeϡnZUϞfU6P ö|"#|Qh'$ eF^$_ PE Ϧ-;0ی]gÈq3oc&8ՔE.e%n\,Ks Q rdata-0.11.2/rdata/tests/data/test_half_named_matrix.rda000066400000000000000000000002301457133752200232310ustar00rootroot00000000000000 r0b```f`adb`f2XCCt-XFN -VZ\XRYePJ@i(-@59%3H13)*\ d Ci<P gda5Trdata-0.11.2/rdata/tests/data/test_list.rda000066400000000000000000000001471457133752200205510ustar00rootroot00000000000000 r0b```b`b&f H020piΒ G$ | `@a$#ށE+.L Y(hrdata-0.11.2/rdata/tests/data/test_list_attrs.rda000066400000000000000000000001571457133752200217670ustar00rootroot00000000000000 r0b```b`faa`b2Y# '/I-.%%E@LhJs+r@0H,,14&2l/HA@j rdata-0.11.2/rdata/tests/data/test_logical.rda000066400000000000000000000001111457133752200211770ustar00rootroot00000000000000 r0b```b`b&f H020pi , H?Ordata-0.11.2/rdata/tests/data/test_matrix.rda000066400000000000000000000001461457133752200211010ustar00rootroot00000000000000 r0b```b`b&f H020piĒ *>0p PZJs@i )@l+.Grdata-0.11.2/rdata/tests/data/test_minimal_function.rda000066400000000000000000000004231457133752200231260ustar00rootroot00000000000000 r0b```f`a@&khXHfe&ħ%d101)d+.J.JM qe3fNI2Y ̼֜b CIPlt`l MP.48@%榢 bLPAn *1=5?YJBC3 <@,cAr/i t\3`)ZQPZ\ bϼ f  (y2rdata-0.11.2/rdata/tests/data/test_minimal_function_uncompiled.rda000066400000000000000000000003311457133752200253430ustar00rootroot00000000000000 r0b```f`a@&khXH+fe&ħ%d܂̜6f4=lEEi@sy< *6|F4@i@L5'3/@4!R]_ sSbCP$Tb j4+/lrdata-0.11.2/rdata/tests/data/test_na_string.rda000066400000000000000000000001161457133752200215560ustar00rootroot00000000000000 r0b```b`fcf`b2Y# '+I-.K/.)Ke8RnErdata-0.11.2/rdata/tests/data/test_named_matrix.rda000066400000000000000000000002411457133752200222410ustar00rootroot00000000000000 r0b```f`adb`f2XCCt-XFN -XZ\XRY`PJ@i(-@59%3H13)*\ d Ci<P gUÌ$jgdaOϳrdata-0.11.2/rdata/tests/data/test_nullable_int.rda000066400000000000000000000001361457133752200222440ustar00rootroot00000000000000 r0b```f`afd`f2XCCt-XFN -XZ\WWd`0Urdata-0.11.2/rdata/tests/data/test_nullable_logical.rda000066400000000000000000000001351457133752200230630ustar00rootroot00000000000000 r0b```f`afd`f2XCCt-XFN -ZZ\W)0P&Yrdata-0.11.2/rdata/tests/data/test_s4.rda000066400000000000000000000002131457133752200201160ustar00rootroot00000000000000 r0b```b`faa`b2Y# 'f/I-./6a`dDbKMHblΉE9h*A @B6 P59' ŴԢ 80@i(UsIqkq V;w 0 ZXs0 2lfƮrdata-0.11.2/rdata/tests/data/test_vector.rda000066400000000000000000000001161457133752200210740ustar00rootroot00000000000000 r0b```b`b&f H020pi" ?0`cQTRrdata-0.11.2/rdata/tests/test_rdata.py000066400000000000000000000540671457133752200176540ustar00rootroot00000000000000"""Tests of parsing and conversion.""" import itertools import unittest from collections import ChainMap from fractions import Fraction from types import SimpleNamespace from typing import Any import numpy as np import pandas as pd import pytest import xarray import rdata TESTDATA_PATH = rdata.TESTDATA_PATH class SimpleTests(unittest.TestCase): """Collection of simple test cases.""" def test_opened_file(self) -> None: """Test that an opened file can be passed to parse_file.""" with (TESTDATA_PATH / "test_vector.rda").open("rb") as f: parsed = rdata.parser.parse_file(f) converted = rdata.conversion.convert(parsed) assert isinstance(converted, dict) def test_opened_string(self) -> None: """Test that a string can be passed to parse_file.""" parsed = rdata.parser.parse_file( str(TESTDATA_PATH / "test_vector.rda"), ) converted = rdata.conversion.convert(parsed) assert isinstance(converted, dict) def test_logical(self) -> None: """Test parsing of logical vectors.""" data = rdata.read_rda(TESTDATA_PATH / "test_logical.rda") np.testing.assert_equal(data, { "test_logical": np.array([True, True, False, True, False]), }) def test_nullable_logical(self) -> None: """Test parsing of logical vectors containing NA.""" data = rdata.read_rda(TESTDATA_PATH / "test_nullable_logical.rda") array = data["test_nullable_logical"] np.testing.assert_array_equal( array.data, np.array([True, False, True]), ) np.testing.assert_array_equal( array.mask, np.array([False, False, True]), ) def test_nullable_int(self) -> None: """Test parsing of integer vectors containing NA.""" data = rdata.read_rda(TESTDATA_PATH / "test_nullable_int.rda") array = data["test_nullable_int"] np.testing.assert_array_equal( array.data, np.array([313, -12, -2**31]), ) np.testing.assert_array_equal( array.mask, np.array([False, False, True]), ) def test_vector(self) -> None: """Test parsing of numerical vectors.""" data = rdata.read_rda(TESTDATA_PATH / "test_vector.rda") np.testing.assert_equal(data, { "test_vector": np.array([1.0, 2.0, 3.0]), }) def test_empty_string(self) -> None: """Test that the empty string is parsed correctly.""" data = rdata.read_rda(TESTDATA_PATH / "test_empty_str.rda") np.testing.assert_equal(data, { "test_empty_str": [""], }) def test_na_string(self) -> None: """Test that the NA string is parsed correctly.""" data = rdata.read_rda(TESTDATA_PATH / "test_na_string.rda") np.testing.assert_equal(data, { "test_na_string": [None], }) def test_complex(self) -> None: """Test that complex numbers can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_complex.rda") np.testing.assert_equal(data, { "test_complex": np.array([1 + 2j, 2, 0, 1 + 3j, -1j]), }) def test_matrix(self) -> None: """Test that a matrix can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_matrix.rda") np.testing.assert_equal(data, { "test_matrix": np.array([ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], ]), }) def test_named_matrix(self) -> None: """Test that a named matrix can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_named_matrix.rda") reference = xarray.DataArray( [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], ], dims=["dim_0", "dim_1"], coords={ "dim_0": ["dim0_0", "dim0_1"], "dim_1": ["dim1_0", "dim1_1", "dim1_2"], }, ) xarray.testing.assert_identical( data["test_named_matrix"], reference, ) def test_half_named_matrix(self) -> None: """Test that a named matrix with no name for a dim can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_half_named_matrix.rda") reference = xarray.DataArray( [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], ], dims=["dim_0", "dim_1"], coords={ "dim_0": ["dim0_0", "dim0_1"], }, ) xarray.testing.assert_identical( data["test_half_named_matrix"], reference, ) def test_full_named_matrix(self) -> None: """Test that a named matrix with dim names can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_full_named_matrix.rda") reference = xarray.DataArray( [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], ], dims=["my_dim_0", "my_dim_1"], coords={ "my_dim_0": ["dim0_0", "dim0_1"], "my_dim_1": ["dim1_0", "dim1_1", "dim1_2"], }, ) xarray.testing.assert_identical( data["test_full_named_matrix"], reference, ) def test_full_named_matrix_rds(self) -> None: """Test that a named matrix with dim names can be parsed.""" data = rdata.read_rds(TESTDATA_PATH / "test_full_named_matrix.rds") reference = xarray.DataArray( [ [1.0, 2.0, 3.0], [4.0, 5.0, 6.0], ], dims=["my_dim_0", "my_dim_1"], coords={ "my_dim_0": ["dim0_0", "dim0_1"], "my_dim_1": ["dim1_0", "dim1_1", "dim1_2"], }, ) xarray.testing.assert_identical( data, reference, ) def test_list(self) -> None: """Test that list can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_list.rda") np.testing.assert_equal(data, { "test_list": [ np.array([1.0]), ["a", "b", "c"], np.array([2.0, 3.0]), ["hi"], ], }) @pytest.mark.filterwarnings("ignore:Missing constructor") def test_file(self) -> None: """Test that external pointers can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_file.rda") np.testing.assert_equal(data, { "test_file": [5], }) def test_expression(self) -> None: """Test that expressions can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_expression.rda") np.testing.assert_equal(data, { "test_expression": rdata.conversion.RExpression([ rdata.conversion.RLanguage( ["^", "base", "exponent"], attributes={}, ), ]), }) def test_builtin(self) -> None: """Test that builtin functions can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_builtin.rda") np.testing.assert_equal(data, { "test_builtin": rdata.conversion.RBuiltin(name="abs"), }) def test_minimal_function_uncompiled(self) -> None: """Test that a minimal function can be parsed.""" data = rdata.read_rda( TESTDATA_PATH / "test_minimal_function_uncompiled.rda", ) converted_fun = data["test_minimal_function_uncompiled"] assert isinstance( converted_fun, rdata.conversion.RFunction, ) np.testing.assert_equal(converted_fun.environment, ChainMap({})) np.testing.assert_equal(converted_fun.formals, None) np.testing.assert_equal(converted_fun.body, None) np.testing.assert_equal( converted_fun.source, "test_minimal_function_uncompiled <- function() NULL\n", ) @pytest.mark.filterwarnings("ignore:Missing constructor") def test_minimal_function(self) -> None: """Test that a minimal function (compiled) can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_minimal_function.rda") converted_fun = data["test_minimal_function"] assert isinstance( converted_fun, rdata.conversion.RFunction, ) np.testing.assert_equal(converted_fun.environment, ChainMap({})) np.testing.assert_equal(converted_fun.formals, None) converted_body = converted_fun.body assert isinstance( converted_body, rdata.conversion.RBytecode, ) np.testing.assert_equal(converted_body.code, np.array([12, 17, 1])) np.testing.assert_equal(converted_body.attributes, {}) np.testing.assert_equal( converted_fun.source, "test_minimal_function <- function() NULL\n", ) def test_empty_function_uncompiled(self) -> None: """Test that a simple function can be parsed.""" data = rdata.read_rda( TESTDATA_PATH / "test_empty_function_uncompiled.rda", ) converted_fun = data["test_empty_function_uncompiled"] assert isinstance( converted_fun, rdata.conversion.RFunction, ) np.testing.assert_equal(converted_fun.environment, ChainMap({})) np.testing.assert_equal(converted_fun.formals, None) assert isinstance(converted_fun.body, rdata.conversion.RLanguage) np.testing.assert_equal( converted_fun.source, "test_empty_function_uncompiled <- function() {}\n", ) @pytest.mark.filterwarnings("ignore:Missing constructor") def test_empty_function(self) -> None: """Test that a simple function (compiled) can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_empty_function.rda") converted_fun = data["test_empty_function"] assert isinstance( converted_fun, rdata.conversion.RFunction, ) np.testing.assert_equal(converted_fun.environment, ChainMap({})) np.testing.assert_equal(converted_fun.formals, None) converted_body = converted_fun.body assert isinstance( converted_body, rdata.conversion.RBytecode, ) np.testing.assert_equal(converted_body.code, np.array([12, 17, 1])) np.testing.assert_equal(converted_body.attributes, {}) np.testing.assert_equal( converted_fun.source, "test_empty_function <- function() {}\n", ) @pytest.mark.filterwarnings("ignore:Missing constructor") def test_function(self) -> None: """Test that functions can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_function.rda") converted_fun = data["test_function"] assert isinstance( converted_fun, rdata.conversion.RFunction, ) np.testing.assert_equal(converted_fun.environment, ChainMap({})) np.testing.assert_equal(converted_fun.formals, None) converted_body = converted_fun.body assert isinstance( converted_body, rdata.conversion.RBytecode, ) np.testing.assert_equal( converted_body.code, np.array([12, 23, 1, 34, 4, 38, 2, 1]), ) np.testing.assert_equal(converted_body.attributes, {}) np.testing.assert_equal( converted_fun.source, "test_function <- function() {print(\"Hello\")}\n", ) @pytest.mark.filterwarnings("ignore:Missing constructor") def test_function_arg(self) -> None: """Test that functions can be parsed.""" data = rdata.read_rda(TESTDATA_PATH / "test_function_arg.rda") converted_fun = data["test_function_arg"] assert isinstance( converted_fun, rdata.conversion.RFunction, ) np.testing.assert_equal(converted_fun.environment, ChainMap({})) np.testing.assert_equal(converted_fun.formals, {"a": NotImplemented}) converted_body = converted_fun.body assert isinstance( converted_body, rdata.conversion.RBytecode, ) np.testing.assert_equal( converted_body.code, np.array([12, 23, 1, 29, 4, 38, 2, 1]), ) np.testing.assert_equal(converted_body.attributes, {}) np.testing.assert_equal( converted_fun.source, "test_function_arg <- function(a) {print(a)}\n", ) def test_encodings(self) -> None: """Test of differents encodings.""" with self.assertWarns( UserWarning, msg="Unknown encoding. Assumed ASCII.", ): data = rdata.read_rda( TESTDATA_PATH / "test_encodings.rda", ) np.testing.assert_equal(data, { "test_encoding_utf8": ["eĥoŝanĝo ĉiuĵaŭde"], "test_encoding_latin1": ["cañón"], "test_encoding_bytes": [b"reba\xf1o"], "test_encoding_latin1_implicit": [b"\xcd\xf1igo"], }) def test_encodings_v3(self) -> None: """Test encodings in version 3 format.""" data = rdata.read_rda(TESTDATA_PATH / "test_encodings_v3.rda") np.testing.assert_equal(data, { "test_encoding_utf8": ["eĥoŝanĝo ĉiuĵaŭde"], "test_encoding_latin1": ["cañón"], "test_encoding_bytes": [b"reba\xf1o"], "test_encoding_latin1_implicit": ["Íñigo"], }) def test_dataframe(self) -> None: """Test dataframe conversion.""" for f in ("test_dataframe.rda", "test_dataframe_v3.rda"): with self.subTest(file=f): data = rdata.read_rda(TESTDATA_PATH / f) pd.testing.assert_frame_equal( data["test_dataframe"], pd.DataFrame( { "class": pd.Categorical( ["a", "b", "b"], ), "value": pd.Series( [1, 2, 3], dtype=pd.Int32Dtype(), ).array, }, index=pd.RangeIndex(start=1, stop=4), ), ) def test_dataframe_rds(self) -> None: """Test dataframe conversion.""" for f in ("test_dataframe.rds", "test_dataframe_v3.rds"): with self.subTest(file=f): data = rdata.read_rds(TESTDATA_PATH / f) pd.testing.assert_frame_equal( data, pd.DataFrame( { "class": pd.Categorical( ["a", "b", "b"], ), "value": pd.Series( [1, 2, 3], dtype=pd.Int32Dtype(), ).array, }, index=pd.RangeIndex(start=1, stop=4), ), ) def test_dataframe_rownames(self) -> None: """Test dataframe conversion.""" data = rdata.read_rda(TESTDATA_PATH / "test_dataframe_rownames.rda") pd.testing.assert_frame_equal( data["test_dataframe_rownames"], pd.DataFrame( { "class": pd.Categorical( ["a", "b", "b"], ), "value": pd.Series( [1, 2, 3], dtype=pd.Int32Dtype(), ).array, }, index=("Madrid", "Frankfurt", "Herzberg am Harz"), ), ) def test_ts(self) -> None: """Test time series conversion.""" data = rdata.read_rda(TESTDATA_PATH / "test_ts.rda") pd.testing.assert_series_equal( data["test_ts"], pd.Series({ 2000 + Fraction(2, 12): 1.0, 2000 + Fraction(3, 12): 2.0, 2000 + Fraction(4, 12): 3.0, }), ) def test_s4(self) -> None: """Test parsing of S4 classes.""" with pytest.warns(UserWarning, match="Missing constructor"): data = rdata.read_rda(TESTDATA_PATH / "test_s4.rda") np.testing.assert_equal(data, { "test_s4": SimpleNamespace( age=np.array(28), name=["Carlos"], **{"class": ["Person"]}, ), }) def test_environment(self) -> None: """Test parsing of environments.""" parsed = rdata.parser.parse_file( TESTDATA_PATH / "test_environment.rda", ) converted = rdata.conversion.convert(parsed) dict_env = {"string": ["test"]} empty_global_env: dict[str, Any] = {} np.testing.assert_equal(converted, { "test_environment": ChainMap(dict_env, ChainMap(empty_global_env)), }) global_env = {"global": "test"} converted_global = rdata.conversion.convert( parsed, global_environment=global_env, ) np.testing.assert_equal(converted_global, { "test_environment": ChainMap(dict_env, ChainMap(global_env)), }) def test_emptyenv(self) -> None: """Test parsing the empty environment.""" data = rdata.read_rda(TESTDATA_PATH / "test_emptyenv.rda") assert data == { "test_emptyenv": ChainMap({}), } def test_list_attrs(self) -> None: """Test that lists accept attributes.""" data = rdata.read_rda(TESTDATA_PATH / "test_list_attrs.rda") np.testing.assert_equal(data, { "test_list_attrs": [["list"], [5]], }) def test_altrep_compact_intseq(self) -> None: """Test alternative representation of sequences of ints.""" data = rdata.read_rda(TESTDATA_PATH / "test_altrep_compact_intseq.rda") np.testing.assert_equal(data, { "test_altrep_compact_intseq": np.arange(1000), }) def test_altrep_compact_intseq_asymmetric(self) -> None: """ Test alternative representation of sequences of ints. This test an origin different from 0, to reproduce issue #29. """ data = rdata.read_rda( TESTDATA_PATH / "test_altrep_compact_intseq_asymmetric.rda", ) np.testing.assert_equal(data, { "test_altrep_compact_intseq_asymmetric": np.arange(-5, 6), }) def test_altrep_compact_realseq(self) -> None: """Test alternative representation of sequences of ints.""" data = rdata.read_rda( TESTDATA_PATH / "test_altrep_compact_realseq.rda", ) np.testing.assert_equal(data, { "test_altrep_compact_realseq": np.arange(1000.0), }) def test_altrep_compact_realseq_asymmetric(self) -> None: """ Test alternative representation of sequences of ints. This test an origin different from 0, to reproduce issue #29. """ data = rdata.read_rda( TESTDATA_PATH / "test_altrep_compact_realseq_asymmetric.rda", ) np.testing.assert_equal(data, { "test_altrep_compact_realseq_asymmetric": np.arange(-5.0, 6.0), }) def test_altrep_deferred_string(self) -> None: """Test alternative representation of deferred strings.""" data = rdata.read_rda( TESTDATA_PATH / "test_altrep_deferred_string.rda", ) np.testing.assert_equal(data, { "test_altrep_deferred_string": [ "1", "2.3", "10000", "1e+05", "-10000", "-1e+05", "0.001", "1e-04", "1e-05", ], }) def test_altrep_wrap_real(self) -> None: """Test alternative representation of wrap_real.""" data = rdata.read_rda( TESTDATA_PATH / "test_altrep_wrap_real.rda", ) np.testing.assert_equal(data, { "test_altrep_wrap_real": [3], }) def test_altrep_wrap_string(self) -> None: """Test alternative representation of wrap_string.""" data = rdata.read_rda(TESTDATA_PATH / "test_altrep_wrap_string.rda") np.testing.assert_equal(data, { "test_altrep_wrap_string": ["Hello"], }) def test_altrep_wrap_logical(self) -> None: """Test alternative representation of wrap_logical.""" data = rdata.read_rda(TESTDATA_PATH / "test_altrep_wrap_logical.rda") np.testing.assert_equal(data, { "test_altrep_wrap_logical": [True], }) def test_ascii(self) -> None: """Test ascii files.""" ref_ma = np.ma.array( # type: ignore[no-untyped-call] data=[True], mask=[True], fill_value=True, ) ref = [[1.1], [2], [3. + 4.j], ref_ma, ["aä"]] for tag, v, ext in itertools.product( ("", "win_"), (2, 3), ("rda", "rds"), ): f = f"test_ascii_{tag}v{v}.{ext}" with self.subTest(file=f): parsed = rdata.parser.parse_file( TESTDATA_PATH / f, ) converted = rdata.conversion.convert(parsed) if ext == "rda": np.testing.assert_equal(converted, {"data": ref}) ma = converted["data"][3] else: np.testing.assert_equal(converted, ref) ma = converted[3] # Test masked array separately np.testing.assert_equal(ma.data, ref_ma.data) np.testing.assert_equal(ma.mask, ref_ma.mask) np.testing.assert_equal(ma.mask, ref_ma.mask) np.testing.assert_equal(ma.get_fill_value(), ref_ma.get_fill_value()) if __name__ == "__main__": unittest.main() rdata-0.11.2/readthedocs.yml000066400000000000000000000011701457133752200157060ustar00rootroot00000000000000# .readthedocs.yml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 build: os: ubuntu-22.04 tools: python: "3.9" # Build documentation in the docs/ directory with Sphinx sphinx: builder: html configuration: docs/conf.py # Build documentation with MkDocs #mkdocs: # configuration: mkdocs.yml # Optionally build your docs in additional formats such as PDF # Optionally set the version of Python and requirements required to build your docs python: install: - method: pip path: . extra_requirements: - docs