First version

main
Estelle Poulin 1 year ago
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…
Cancel
Save