First version
							parent
							
								
									3db4a35cc5
								
							
						
					
					
						commit
						a4b4a4c72b
					
				@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					name: Publish Python distributions to PyPI and TestPyPI
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  push:
 | 
				
			||||||
 | 
					    branches: [main]
 | 
				
			||||||
 | 
					  pull_request:
 | 
				
			||||||
 | 
					    branches: [main]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  validate:
 | 
				
			||||||
 | 
					    name: Run static analysis on the code
 | 
				
			||||||
 | 
					    runs-on: ubuntu-22.04
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: Lint with flake8
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          pip install flake8
 | 
				
			||||||
 | 
					          # stop the build if there are Python syntax errors or undefined names
 | 
				
			||||||
 | 
					          flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
 | 
				
			||||||
 | 
					          # exit-zero treats all errors as warnings.
 | 
				
			||||||
 | 
					          flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  build-and-publish:
 | 
				
			||||||
 | 
					    name: Build and publish Python distribution
 | 
				
			||||||
 | 
					    runs-on: ubuntu-22.04
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - uses: actions/checkout@main
 | 
				
			||||||
 | 
					      - name: Initialize Python 3.11
 | 
				
			||||||
 | 
					        uses: actions/setup-python@v1
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          python-version: 3.11
 | 
				
			||||||
 | 
					      - name: Install dependencies
 | 
				
			||||||
 | 
					        run: |
 | 
				
			||||||
 | 
					          python -m pip install --upgrade pip build
 | 
				
			||||||
 | 
					      - name: Build binary wheel and a source tarball
 | 
				
			||||||
 | 
					        run: python -m build 
 | 
				
			||||||
 | 
					      - name: Publish distribution to PyPI
 | 
				
			||||||
 | 
					        uses: pypa/gh-action-pypi-publish@v1.8.5 
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          password: ${{ secrets.PIPY_PASSWORD }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					# pyproject.toml
 | 
				
			||||||
 | 
					# https://packaging.python.org/en/latest/specifications/declaring-project-metadata
 | 
				
			||||||
 | 
					# https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[build-system]
 | 
				
			||||||
 | 
					requires = ["setuptools", "setuptools-scm", "build"]
 | 
				
			||||||
 | 
					build-backend = "setuptools.build_meta"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[project]
 | 
				
			||||||
 | 
					name = "rtex"
 | 
				
			||||||
 | 
					description = "*Unofficial* client for the Rtex API server"
 | 
				
			||||||
 | 
					authors = [
 | 
				
			||||||
 | 
						{name = "Estelle Poulin", email = "dev@inspiredby.es"},
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					readme = "README.md"
 | 
				
			||||||
 | 
					requires-python = ">=3.11"
 | 
				
			||||||
 | 
					keywords = ["rtex", "latex", "math"]
 | 
				
			||||||
 | 
					license = {text = "MIT"}
 | 
				
			||||||
 | 
					classifiers = [
 | 
				
			||||||
 | 
					    "Programming Language :: Python :: 3",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					dynamic = ["version", "dependencies"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[project.urls]
 | 
				
			||||||
 | 
					homepage = "https://github.com/estheruary/python-rtex"
 | 
				
			||||||
 | 
					repository = "https://github.com/estheruary/python-rtex"
 | 
				
			||||||
 | 
					changelog = "https://github.com/estheruary/python-rtex/-/blob/main/CHANGELOG.md"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.setuptools]
 | 
				
			||||||
 | 
					packages = ["rtex"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.setuptools.dynamic]
 | 
				
			||||||
 | 
					version = {attr = "rtex.__version__"}
 | 
				
			||||||
 | 
					dependencies = {file = ["requirements.txt"]}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.black]
 | 
				
			||||||
 | 
					line-length = 100
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.isort]
 | 
				
			||||||
 | 
					profile = "black"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[tool.vulture]
 | 
				
			||||||
 | 
					ignore_names = ["self", "cls"]
 | 
				
			||||||
@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					aiohttp==3.8.4
 | 
				
			||||||
 | 
					pydantic==1.10.2
 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					__version__ = "0.0.1"
 | 
				
			||||||
@ -0,0 +1,161 @@
 | 
				
			|||||||
 | 
					import io
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import string
 | 
				
			||||||
 | 
					import textwrap
 | 
				
			||||||
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import aiohttp
 | 
				
			||||||
 | 
					from pydantic import validate_arguments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from rtex.constants import DEFAULT_API_HOST, FORMAT_MIME
 | 
				
			||||||
 | 
					from rtex.exceptions import YouNeedToUseAContextManager
 | 
				
			||||||
 | 
					from rtex.models import (
 | 
				
			||||||
 | 
					    CreateLaTeXDocumentRequest,
 | 
				
			||||||
 | 
					    CreateLaTeXDocumentResponse,
 | 
				
			||||||
 | 
					    RenderDensity,
 | 
				
			||||||
 | 
					    RenderFormat,
 | 
				
			||||||
 | 
					    RenderQuality,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AsyncRtexClient:
 | 
				
			||||||
 | 
					    def __init__(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        api_host=os.environ.get("RTEX_API_HOST", DEFAULT_API_HOST),
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        self.api_host = api_host
 | 
				
			||||||
 | 
					        self.latex_template = string.Template(
 | 
				
			||||||
 | 
					            textwrap.dedent(
 | 
				
			||||||
 | 
					                r"""
 | 
				
			||||||
 | 
					                \documentclass{$docclass}
 | 
				
			||||||
 | 
					                \begin{document}
 | 
				
			||||||
 | 
					                $doc
 | 
				
			||||||
 | 
					                \end{document}
 | 
				
			||||||
 | 
					                """
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def __aenter__(self):
 | 
				
			||||||
 | 
					        self.session = await aiohttp.ClientSession(
 | 
				
			||||||
 | 
					            base_url=self.api_host,
 | 
				
			||||||
 | 
					            headers={
 | 
				
			||||||
 | 
					                "Content-Type": "application/json",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ).__aenter__()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async def __aexit__(self, exc_type, exc_val, exc_tb):
 | 
				
			||||||
 | 
					        return await self.session.__aexit__(exc_type, exc_val, exc_tb)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _oops_no_session(self):
 | 
				
			||||||
 | 
					        if not self.session:
 | 
				
			||||||
 | 
					            raise YouNeedToUseAContextManager(
 | 
				
			||||||
 | 
					                textwrap.dedent(
 | 
				
			||||||
 | 
					                    f"""\
 | 
				
			||||||
 | 
					                {self.__class__.__name__} keeps a aiohttp.ClientSession under
 | 
				
			||||||
 | 
					                the hood and needs to be closed when you're done with it. But
 | 
				
			||||||
 | 
					                since there isn't an async version of __del__ we have to use
 | 
				
			||||||
 | 
					                __aenter__/__aexit__ instead. Apologies.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Instead of
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    myclient = {self.__class__.__name__}()
 | 
				
			||||||
 | 
					                    myclient.text_to_image(...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Do this
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    async with {self.__class__.__name__} as myclient:
 | 
				
			||||||
 | 
					                        myclient.text_to_image(...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                Note that it's `async with` and not `with`.
 | 
				
			||||||
 | 
					                """
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validate_arguments
 | 
				
			||||||
 | 
					    async def create_render(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        code: str,
 | 
				
			||||||
 | 
					        format: RenderFormat = "png",
 | 
				
			||||||
 | 
					        documentclass: str = "minimal",
 | 
				
			||||||
 | 
					        quality: Optional[RenderQuality] = None,
 | 
				
			||||||
 | 
					        density: Optional[RenderDensity] = None,
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        self._oops_no_session()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        final_doc = self.latex_template.substitute(
 | 
				
			||||||
 | 
					            docclass=documentclass,
 | 
				
			||||||
 | 
					            doc=code,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        request_body = CreateLaTeXDocumentRequest(
 | 
				
			||||||
 | 
					            code=final_doc,
 | 
				
			||||||
 | 
					            format=format,
 | 
				
			||||||
 | 
					            quality=quality,
 | 
				
			||||||
 | 
					            density=density,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res = await self.session.post(
 | 
				
			||||||
 | 
					            "/api/v2",
 | 
				
			||||||
 | 
					            headers={"Accept": "application/json"},
 | 
				
			||||||
 | 
					            data=request_body.json(
 | 
				
			||||||
 | 
					                exclude_defaults=True,
 | 
				
			||||||
 | 
					                exclude_none=True,
 | 
				
			||||||
 | 
					                exclude_unset=True,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return CreateLaTeXDocumentResponse.parse_obj(await res.json()).__root__
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validate_arguments(config={"arbitrary_types_allowed": True})
 | 
				
			||||||
 | 
					    async def save_render(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        filename: str,
 | 
				
			||||||
 | 
					        output_fd: io.IOBase,
 | 
				
			||||||
 | 
					        format: RenderFormat = "png",
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        self._oops_no_session()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res = await self.session.get(
 | 
				
			||||||
 | 
					            f"/api/v2/{filename}",
 | 
				
			||||||
 | 
					            headers={
 | 
				
			||||||
 | 
					                "Accept": FORMAT_MIME[format],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        async for chunk, _ in res.content.iter_chunks():
 | 
				
			||||||
 | 
					            output_fd.write(chunk)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validate_arguments
 | 
				
			||||||
 | 
					    async def get_render(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        filename: str,
 | 
				
			||||||
 | 
					        format: RenderFormat = "png",
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        buf = io.BytesIO()
 | 
				
			||||||
 | 
					        await self.save_render(filename, buf, format)
 | 
				
			||||||
 | 
					        buf.seek(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return buf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @validate_arguments
 | 
				
			||||||
 | 
					    async def render_math(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        code: str,
 | 
				
			||||||
 | 
					        format: RenderFormat = "png",
 | 
				
			||||||
 | 
					    ):
 | 
				
			||||||
 | 
					        final_doc = rf"\({code}\)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res = await self.create_render(
 | 
				
			||||||
 | 
					            code=final_doc,
 | 
				
			||||||
 | 
					            format=format,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if res.status == "error":
 | 
				
			||||||
 | 
					            raise RuntimeError("Failed to render code")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return await self.get_render(
 | 
				
			||||||
 | 
					            filename=res.filename,
 | 
				
			||||||
 | 
					            format=format,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					from typing import Dict, Final
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DEFAULT_API_HOST: Final[str] = "https://rtex.probablyaweb.site"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FORMAT_MIME: Final[Dict[str, str]] = {
 | 
				
			||||||
 | 
					    "png": "image/png",
 | 
				
			||||||
 | 
					    "jpg": "image/jpeg",
 | 
				
			||||||
 | 
					    "pdf": "application/pdf",
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					class RtexError(Exception):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class YouNeedToUseAContextManager(RtexError):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					from typing import Annotated, Literal, Optional, Union
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from pydantic import BaseModel, Field, conint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					RenderFormat = Literal["png", "jpg", "pdf"]
 | 
				
			||||||
 | 
					RenderQuality = Annotated[int, conint(ge=0, le=100)]
 | 
				
			||||||
 | 
					RenderDensity = int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateLaTeXDocumentRequest(BaseModel):
 | 
				
			||||||
 | 
					    code: str
 | 
				
			||||||
 | 
					    format: RenderFormat
 | 
				
			||||||
 | 
					    quality: Optional[RenderQuality]
 | 
				
			||||||
 | 
					    density: Optional[RenderDensity] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateLaTeXDocumentSuccessResponse(BaseModel):
 | 
				
			||||||
 | 
					    status: Literal["success"]
 | 
				
			||||||
 | 
					    log: str
 | 
				
			||||||
 | 
					    filename: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateLaTeXDocumentErrorResponse(BaseModel):
 | 
				
			||||||
 | 
					    status: Literal["error"]
 | 
				
			||||||
 | 
					    log: str
 | 
				
			||||||
 | 
					    description: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class CreateLaTeXDocumentResponse(BaseModel):
 | 
				
			||||||
 | 
					    __root__: Union[
 | 
				
			||||||
 | 
					        CreateLaTeXDocumentSuccessResponse,
 | 
				
			||||||
 | 
					        CreateLaTeXDocumentErrorResponse,
 | 
				
			||||||
 | 
					    ] = Field(discriminator="status")
 | 
				
			||||||
					Loading…
					
					
				
		Reference in New Issue