Skip to main content

Remote HTTP Mock

Project description

jj

Codecov PyPI PyPI - Downloads Python Version

Installation

pip3 install jj

Usage

import jj

@jj.match("*")
async def handler(request: jj.Request) -> jj.Response:
    return jj.Response(body="200 OK")

jj.serve()

Documentation


Matchers

Method

match_method(method)
from jj.http.methods import GET

@jj.match_method(GET)
async def handler(request):
    return jj.Response(body="Method: " + request.method)
match_methods(methods)
from jj.http.methods import PUT, PATCH

@jj.match_methods(PUT, PATCH)
async def handler(request):
    return jj.Response(body="Method: " + request.method)

Path

match_path(path)
@jj.match_path("/users")
async def handler(request):
    return jj.Response(body="Path: " + request.path)

Segments

@jj.match_path("/users/{users_id}")
async def handler(request):
    return jj.Response(body=f"Segments: {request.segments}")

More information available here https://docs.aiohttp.org/en/stable/web_quickstart.html#variable-resources

Params

match_param(name, val)
@jj.match_param("locale", "en_US")
async def handler(request):
    locales = request.params.getall('locale')
    return jj.Response(body="Locales: " + ",".join(locales))
match_params(params)
@jj.match_params({"locale": "en_US", "timezone": "UTC"})
async def handler(request):
    # Literal String Interpolation (PEP 498)
    return jj.Response(body=f"Params: {request.params}")

Headers

match_header(name, val)
@jj.match_header("X-Forwarded-Proto", "https")
async def handler(request):
    proto = request.headers.getone("X-Forwarded-Proto")
    return jj.Response(body="Proto: " + proto)
match_headers(headers)
@jj.match_headers({
    "x-user-id": "1432",
    "x-client-id": "iphone",
})
async def handler(request):
    return jj.Response(body=f"Headers: {request.headers}")

Combining Matchers

match_any(matchers)
from jj.http import PATCH, PUT

@jj.match_any([
    jj.match_method(PUT),
    jj.match_method(PATCH),
])
async def handler(request):
    return jj.Response(body="200 OK")
match_all(matchers)
@jj.match_all([
    jj.match_method("*"),
    jj.match_path("/"),
    jj.match_params({"locale": "en_US"}),
    jj.match_headers({"x-request-id": "0fefbf48"}),
])
async def handler(request):
    return jj.Response(body="200 OK")
match(method, path, params, headers)
@jj.match("*", "/", {"locale": "en_US"}, {"x-request-id": "0fefbf48"})
async def handler(request):
    return jj.Response(body="200 OK")

Responses

Response

JSON Response
@jj.match("*")
async def handler(request):
    return jj.Response(json={"message": "200 OK"})
HTML Response
@jj.match("*")
async def handler(request):
    return jj.Response(body="<p>text<p>", headers={"Content-Type": "text/html"})
Binary Response
@jj.match("*")
async def handler(request):
    return jj.Response(body=b"<binary>")
Not Found Response
@jj.match("*")
async def handler(request):
    return jj.Response(status=404, reason="Not Found")
Predefined Body
from jj.http import GET

@jj.match(GET, "/users")
async def handler(request):
    return jj.Response(body=open("responses/users.json", "rb"))
from jj.http import POST, CREATED

@jj.match(POST, "/users")
async def handler(request):
    return jj.Response(body=open("responses/created.json", "rb"), status=CREATED)

StaticResponse

Inline Content
from jj.http import GET

@jj.match(GET, "/image")
async def handler(request):
    return jj.StaticResponse("public/image.jpg")
Downloadable File
from jj.http import GET

@jj.match(GET, "/report")
async def handler(request):
    return jj.StaticResponse("public/report.csv", attachment=True)
from jj.http import GET

@jj.match(GET, "/")
async def handler(request):
    return jj.StaticResponse("public/report.csv", attachment="report.csv")

For more information visit https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition

RelayResponse β

@jj.match("*")
async def handler(request):
    return jj.RelayResponse(target="https://httpbin.org/")

Apps

Single App

import jj
from jj.http.methods import GET, ANY
from jj.http.codes import OK, NOT_FOUND

class App(jj.App):
    @jj.match(GET, "/")
    async def root_handler(self, request: jj.Request) -> jj.Response:
        return jj.Response(status=OK, json={"message": "200 OK"})

    @jj.match(ANY)
    async def default_handler(self, request: jj.Request) -> jj.Response:
        return jj.Response(status=NOT_FOUND, json={"message": "Not Found"})

jj.serve(App(), port=5000)

Multiple Apps

import jj

class App(jj.App):
    @jj.match("*")
    async def handler(self, request: jj.Request) -> jj.Response:
        return jj.Response(body="App")

class AnotherApp(jj.App):
    @jj.match("*")
    async def handler(self, request: jj.Request) -> jj.Response:
        return jj.Response(body="AnotherApp")

jj.start(App(), port=5001)
jj.start(AnotherApp(), port=5002)

jj.wait_for([KeyboardInterrupt])

App Inheritance

import jj

class UsersApp(jj.App):
    @jj.match("*", path="/users")
    async def handler(self, request: jj.Request) -> jj.Response:
        return jj.Response(body="Users")

class GroupsApp(jj.App):
    @jj.match("*", path="/groups")
    async def handler(self, request: jj.Request) -> jj.Response:
        return jj.Response(body="Groups")

class App(UsersApp, GroupsApp):
    pass

jj.serve(App())

Middlewares

Handler Middleware

import jj
from jj.http.codes import OK, FORBIDDEN

class Middleware(jj.Middleware):
    async def do(self, request, handler, app):
        if request.headers.get("x-secret-key") != "<SECRET_KEY>":
            return jj.Response(status=FORBIDDEN, body="Forbidden")
        return await handler(request)

class App(jj.App):
    @Middleware()
    @jj.match("*")
    async def handler(self, request: jj.Request) -> jj.Response:
        return jj.Response(status=OK, body="Ok")

jj.serve(App())

App Middleware

import jj
from jj.http.codes import OK, FORBIDDEN

class ReusableMiddleware(jj.Middleware):
    def __init__(self, secret_key):
        super().__init__()
        self._secret_key = secret_key

    async def do(self, request, handler, app):
        if request.headers.get("x-secret-key") != self._secret_key:
            return jj.Response(status=FORBIDDEN, body="Forbidden")
        return await handler(request)

private = ReusableMiddleware("<SECRET_KEY>")

@private
class App(jj.App):
    @jj.match("*")
    async def handler(self, request: jj.Request) -> jj.Response:
        return jj.Response(status=OK, body="Ok")

jj.serve(App())

Remote Mock

Server Side

Start Remote Mock
import jj
from jj.mock import Mock

jj.serve(Mock(), port=8080)

or via docker

docker run -p 8080:80 nikitanovosibirsk/jj

Client Side

import asyncio

import jj
from jj.mock import mocked


async def main():
    matcher = jj.match("GET", "/users")
    response = jj.Response(status=200, json=[])

    async with mocked(matcher, response) as mock:
        # Request GET /users
        # Returns status=200 body=[]
    assert len(mock.history) == 1

asyncio.run(main())

Use jj-district42 for testing requests

Low Level API
Register Remote Handler
import asyncio

import jj
from jj.mock import RemoteMock


async def main():
    remote_mock = RemoteMock("http://localhost:8080")

    matcher = jj.match("GET", "/users")
    response = jj.Response(status=200, json=[])
    remote_handler = remote_mock.create_handler(matcher, response)
    await remote_handler.register()

    # Request GET /users
    # Returns status=200 body=[]

asyncio.run(main())
Deregister Remote Handler
import asyncio

import jj
from jj.mock import RemoteMock


async def main():
    remote_mock = RemoteMock("http://localhost:8080")

    matcher = jj.match("GET", "/users")
    response = jj.Response(status=200, json=[])
    remote_handler = remote_mock.create_handler(matcher, response)
    await remote_handler.register()

    # Request GET /users
    # Returns status=200 body=[]

    await remote_handler.deregister()

asyncio.run(main())
Retrieve Remote Handler History
import asyncio

import jj
from jj.mock import RemoteMock


async def main():
  remote_mock = RemoteMock("http://localhost:8080")

  matcher = jj.match("GET", "/users")
  response = jj.Response(status=200, json=[])
  remote_handler = remote_mock.create_handler(matcher, response)
  await remote_handler.register()

  # Request GET /users
  # Returns status=200 body=[]

  history = await remote_handler.fetch_history()
  print(history)

  await remote_handler.deregister()

asyncio.run(main())

History:

[
    {
        'request': HistoryRequest(
            method='GET',
            path='/users',
            params=<MultiDictProxy()>,
            headers=<CIMultiDictProxy('Host': 'localhost:8080',
                                      'Accept': '*/*',
                                      'Accept-Encoding': 'gzip, deflate',
                                      'User-Agent': 'Python/3.8 aiohttp/3.7.3')>,
            body=b'',
        ),
        'response': HistoryResponse(
            status=200,
            reason='OK',
            headers=<CIMultiDictProxy('Content-Type': 'application/json',
                                      'Server': 'jj via aiohttp/3.7.3',
                                      'Content-Length': '2',
                                      'Date': 'Sun, 09 May 2021 08:08:19 GMT')>,
            body=b'[]',
        ),
        'tags': ['f75c2ab7-f68d-4b4a-85e0-1f38bb0abe9a']
    }
]

Expiration Policy

import jj
from jj.mock import mocked
from jj.expiration_policy import ExpireAfterRequests
from httpx import AsyncClient

matcher = jj.match("GET", "/")
response = jj.Response(status=200)
policy = ExpireAfterRequests(1)

async with mocked(matcher, response, expiration_policy=policy):
    async with AsyncClient() as client:
        response = await client.get("/")

Custom Logger

import logging

import jj
from jj.logs import SimpleFormatter
from jj.mock import Mock, SystemLogFilter


class Formatter(SimpleFormatter):
    def format_request(self, request: jj.Request, record: logging.LogRecord) -> str:
        return f"-> {request.method} {request.url.path_qs} {request.headers}"

    def format_response(self, response: jj.Response, request: jj.Request, record: logging.LogRecord) -> str:
        return f"<- {response.status} {response.reason} {response.body}"


handler = logging.StreamHandler()
handler.setFormatter(Formatter())

logger = logging.getLogger("custom_logger")
logger.setLevel(logging.INFO)
logger.addHandler(handler)
logger.addFilter(SystemLogFilter())

jj.serve(Mock(), logger=logger)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jj-2.4.2.tar.gz (57.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jj-2.4.2-py3-none-any.whl (103.2 kB view details)

Uploaded Python 3

File details

Details for the file jj-2.4.2.tar.gz.

File metadata

  • Download URL: jj-2.4.2.tar.gz
  • Upload date:
  • Size: 57.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.10.4

File hashes

Hashes for jj-2.4.2.tar.gz
Algorithm Hash digest
SHA256 a0bd77f78e312ab46a41332f5b52718724c0267aaea67183d2059b2c7c2b8f2e
MD5 c64871362afe67eecd3ca94d81582cce
BLAKE2b-256 2bb64d34c2d4f391e2eb7c8ecb80a66dc067aa266f55d665636b16220554f52b

See more details on using hashes here.

File details

Details for the file jj-2.4.2-py3-none-any.whl.

File metadata

  • Download URL: jj-2.4.2-py3-none-any.whl
  • Upload date:
  • Size: 103.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.0 CPython/3.10.4

File hashes

Hashes for jj-2.4.2-py3-none-any.whl
Algorithm Hash digest
SHA256 7b7100bc0b4689c6f3258871939e8522925f0bbb9aeb3f4e313439ad27428259
MD5 b47fe7b30818d295ce82888e303bae50
BLAKE2b-256 d971095ae3cad3a01e8e68fe1cd38c22027d839c006bf0f965b57dbcd281a1e6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page