Skip to content

Commit

Permalink
docs: Document and clear up web {client,server} and other code
Browse files Browse the repository at this point in the history
  • Loading branch information
alexpovel committed Feb 5, 2023
1 parent 5229102 commit 7690b08
Show file tree
Hide file tree
Showing 9 changed files with 102 additions and 13 deletions.
6 changes: 4 additions & 2 deletions ancv/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ def api(
# Not specifying a token works just as well, but has a much lower request
# ceiling:
token=os.environ.get("GH_TOKEN"),
homepage=os.environ.get("HOMEPAGE", METADATA.home_page or "NO HOMEPAGE SET"),
terminal_landing_page=os.environ.get(
"HOMEPAGE", METADATA.home_page or "NO HOMEPAGE SET"
),
# When visiting this endpoint in a browser, we want to redirect to the homepage.
# That page cannot be this same path under the same hostname again, else we get
# a loop.
landing_page=os.environ.get(
browser_landing_page=os.environ.get(
"LANDING_PAGE",
METADATA.project_url[0] if METADATA.project_url else "https://github.com/",
),
Expand Down
4 changes: 4 additions & 0 deletions ancv/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
class ResumeLookupError(LookupError):
"""Raised when a user's resume cannot be found, is malformed, ..."""

pass


class ResumeConfigError(ValueError):
"""Raised when a resume config is invalid, e.g. missing required fields."""

pass
2 changes: 1 addition & 1 deletion ancv/reflection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class Metadata(BaseModel):
"""Python package metadata.
"""Modeling Python package metadata.
Modelled after the Python core metadata specification:
https://packaging.python.org/en/latest/specifications/core-metadata/ .
Expand Down
7 changes: 7 additions & 0 deletions ancv/visualization/translations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@


class Translation(BaseModel):
"""Modelling a translation for a resume section or field.
These are simple, hard-coded translations. Special grammatical cases, singular vs.
plural, etc. are not handled and need to be handled identically across all languages
(which might end up not working...).
"""

grade: str
awarded_by: str
issued_by: str
Expand Down
30 changes: 30 additions & 0 deletions ancv/web/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,36 @@ async def get_resume(
filename: str = "resume.json",
size_limit: int = 1 * SIPrefix.MEGA,
) -> ResumeSchema:
"""Fetch a user's resume from their GitHub gists.
Searches through all of the user's gists for a file with a given name. Checks for
various bad states:
- User...
- doesn't exist.
- has no gists.
- has no gists with the given filename.
- File...
- is too large.
- is not valid JSON.
- is not valid against the resume schema.
There are others that are probably not covered (hard to test).
Sections of the code are timed for performance analysis.
Args:
user: The GitHub username to fetch the resume from.
session: The `aiohttp.ClientSession` to use for the request.
github: The API object to use for the request.
stopwatch: The `Stopwatch` to use for timing.
filename: The name of the file to look for in the user's gists.
size_limit: The maximum size of the file to look for in the user's gists.
Returns:
The parsed resume.
"""

log = LOGGER.bind(user=user, session=session)

stopwatch("Fetching Gists")
Expand Down
59 changes: 52 additions & 7 deletions ancv/web/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@


def is_terminal_client(user_agent: str) -> bool:
"""Determines if a user agent string indicates a terminal client."""

terminal_clients = [
"curl",
"wget",
Expand All @@ -41,29 +43,51 @@ def is_terminal_client(user_agent: str) -> bool:

@dataclass
class ServerContext:
"""Context for the server."""

host: Optional[str]
port: Optional[int]
path: Optional[str]


class Runnable(ABC):
"""A server object that can be `run`, enabling different server implementations."""

@abstractmethod
def run(self, context: ServerContext) -> None:
...


class APIHandler(Runnable):
"""A runnable server for handling dynamic API requests.
This is the core application server powering the API. It is responsible for handling
requests for the resume of a given user, and returning the appropriate response. It
queries the live GitHub API.
"""

def __init__(
self,
requester: str,
token: Optional[str],
homepage: str,
landing_page: str,
terminal_landing_page: str,
browser_landing_page: str,
) -> None:
"""Initializes the handler.
Args:
requester: The user agent to use for the GitHub API requests.
token: The token to use for the GitHub API requests.
terminal_landing_page: URL to "redirect" to for requests to the root from a
*terminal* client.
browser_landing_page: URL to redirect to for requests to the root from a
*browser* client.
"""

self.requester = requester
self.token = token
self.homepage = homepage
self.landing_page = landing_page
self.terminal_landing_page = terminal_landing_page
self.browser_landing_page = browser_landing_page

LOGGER.debug("Instantiating web application.")
self.app = web.Application()
Expand Down Expand Up @@ -129,17 +153,25 @@ async def app_context(self, app: web.Application) -> AsyncGenerator[None, None]:
log.info("App context teardown done.")

async def root(self, request: web.Request) -> web.Response:
"""The root endpoint, redirecting to the landing page."""

user_agent = request.headers.get("User-Agent", "")

if is_terminal_client(user_agent):
return web.Response(text=f"Visit {self.homepage} to get started.\n")
return web.Response(
text=f"Visit {self.terminal_landing_page} to get started.\n"
)

raise web.HTTPFound(self.landing_page) # Redirect
raise web.HTTPFound(self.browser_landing_page) # Redirect

async def showcase(self, request: web.Request) -> web.Response:
"""The showcase endpoint, returning a static resume."""

return web.Response(text=_SHOWCASE_RESUME)

async def username(self, request: web.Request) -> web.Response:
"""The username endpoint, returning a dynamic resume from a user's gists."""

stopwatch = Stopwatch()
stopwatch(segment="Initialize Request")

Expand Down Expand Up @@ -187,7 +219,15 @@ async def username(self, request: web.Request) -> web.Response:


class FileHandler(Runnable):
"""A handler serving a rendered, static template loaded from a file at startup."""

def __init__(self, file: Path) -> None:
"""Initializes the handler.
Args:
file: The (JSON Resume) file to load the template from.
"""

self.template = Template.from_file(file)
self.rendered = self.template.render()

Expand All @@ -202,12 +242,17 @@ def run(self, context: ServerContext) -> None:
web.run_app(self.app, host=context.host, port=context.port, path=context.path)

async def root(self, request: web.Request) -> web.Response:
"""The root and *only* endpoint, returning the rendered template."""

LOGGER.debug("Serving rendered template.", request=request)
return web.Response(text=self.rendered)


def server_timing_header(timings: dict[str, timedelta]) -> str:
"""https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing"""
"""From a mapping of names to `timedelta`s, return a `Server-Timing` header value.
See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing
"""

# For controlling `timedelta` conversion precision, see:
# https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds
Expand Down
1 change: 1 addition & 0 deletions tests/test_timing.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def sleep(seconds: float) -> None:
while time.time() <= (now + seconds):
time.sleep(0.001)


@pytest.mark.flaky(reruns=3)
def test_stopwatch_basics() -> None:
stopwatch = Stopwatch()
Expand Down
4 changes: 2 additions & 2 deletions tests/web/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ def api_client_app() -> Application:
return APIHandler(
requester=f"{METADATA.name}-PYTEST-REQUESTER",
token=GH_TOKEN,
homepage=f"{METADATA.name}-PYTEST-HOMEPAGE",
landing_page=f"{METADATA.name}-PYTEST-LANDING_PAGE",
terminal_landing_page=f"{METADATA.name}-PYTEST-HOMEPAGE",
browser_landing_page=f"{METADATA.name}-PYTEST-LANDING_PAGE",
).app


Expand Down
2 changes: 1 addition & 1 deletion tests/web/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,4 @@ def test_server_timing_header(


def test_exact_showcase_output(showcase_output: str) -> None:
assert (_SHOWCASE_RESUME == showcase_output)
assert _SHOWCASE_RESUME == showcase_output

0 comments on commit 7690b08

Please sign in to comment.