Skip to content

Commit

Permalink
Merge pull request #117 from darrenburns/mutable-api
Browse files Browse the repository at this point in the history
Mutable api
  • Loading branch information
darrenburns authored Oct 18, 2024
2 parents 09a8095 + f0aa717 commit 7dfbd5d
Show file tree
Hide file tree
Showing 10 changed files with 206 additions and 141 deletions.
Binary file modified .coverage
Binary file not shown.
23 changes: 17 additions & 6 deletions docs/guide/scripting.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ In the context of Posting, a "script" is a regular Python function.
By default, if you specify a path to a Python file, Posting will look for and execute the following functions at the appropriate times:

- `setup(posting: Posting) -> None`
- `on_request(request: httpx.Request, posting: Posting) -> None`
- `on_request(request: RequestModel, posting: Posting) -> None`
- `on_response(response: httpx.Response, posting: Posting) -> None`

However, you can have Posting call any function you wish using the syntax `path/to/script.py:function_to_run`.
Expand All @@ -35,7 +35,7 @@ Note that relative paths are relative to the collection directory.
This ensures that if you place scripts inside your collection directory,
they're included when you share a collection with others.

Note that you do not need to specify all of the arguments when writing these functions. Posting will only pass the number of arguments that you've specified when it calls your function. For example, you could define a your `on_request` function as `def on_request(request: httpx.Request) -> None` and Posting would call it with `on_request(request: httpx.Request)` without passing the `posting` argument.
Note that you do not need to specify all of the arguments when writing these functions. Posting will only pass the number of arguments that you've specified when it calls your function. For example, you could define a your `on_request` function as `def on_request(request: RequestModel) -> None` and Posting would call it with `on_request(request: RequestModel)` without passing the `posting` argument.

## Editing scripts

Expand Down Expand Up @@ -80,12 +80,20 @@ def setup(posting: Posting) -> None:

The **pre-request script** is run after the request has been constructed and variables have been substituted, right before the request is sent.

You can directly modify the `Request` object in this function, for example to set headers, query parameters, etc.
You can directly modify the `RequestModel` object in this function, for example to set headers, query parameters, etc.
The code snippet below shows some of the API.

```python
def on_request(request: httpx.Request, posting: Posting) -> None:
# Set a custom header on the request.
request.headers["X-Custom-Header"] = "foo"
from posting import Auth, Header, RequestModel, Posting


def on_request(request: RequestModel, posting: Posting) -> None:
# Add a custom header to the request.
request.headers.append(Header(name="X-Custom-Header", value="foo"))

# Set auth on the request.
request.auth = Auth.basic_auth("username", "password")
# request.auth = Auth.digest_auth("username", "password")

# This will be captured and written to the log.
print("Request is being sent!")
Expand All @@ -101,6 +109,9 @@ You can use this to extract data from the response, for example a JWT token,
and set it as a variable to be used in later requests.

```python
from posting import Posting


def on_response(response: httpx.Response, posting: Posting) -> None:
# Print the status code of the response to the log.
print(response.status_code)
Expand Down
26 changes: 26 additions & 0 deletions src/posting/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from .collection import (
Auth,
Cookie,
Header,
QueryParam,
RequestBody,
RequestModel,
FormItem,
Options,
Scripts,
)
from .scripts import Posting


__all__ = [
"Auth",
"Cookie",
"Header",
"QueryParam",
"RequestBody",
"RequestModel",
"FormItem",
"Options",
"Scripts",
"Posting",
]
32 changes: 16 additions & 16 deletions src/posting/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,29 +362,15 @@ async def send_request(self) -> None:
timeout=request_model.options.timeout,
auth=request_model.auth.to_httpx_auth() if request_model.auth else None,
) as client:
request = self.build_httpx_request(request_model, client)
request.headers["User-Agent"] = (
f"Posting/{VERSION} (Terminal-based API client)"
)
print("-- sending request --")
print(request)
print(request.headers)
print("follow redirects =", request_options.follow_redirects)
print("verify =", request_options.verify_ssl)
print("attach cookies =", request_options.attach_cookies)
print("proxy =", request_model.options.proxy_url)
print("timeout =", request_model.options.timeout)
print("auth =", request_model.auth)

script_context.request = request
script_context.request = request_model

# If there's an associated pre-request script, run it.
if on_request := request_model.scripts.on_request:
try:
self.get_and_run_script(
on_request,
"on_request",
request,
request_model,
script_context,
)
except Exception:
Expand All @@ -394,6 +380,20 @@ async def send_request(self) -> None:
self.response_script_output.set_request_status("success")
else:
self.response_script_output.set_request_status("no-script")
request = self.build_httpx_request(request_model, client)

request.headers["User-Agent"] = (
f"Posting/{VERSION} (Terminal-based API client)"
)
print("-- sending request --")
print(request)
print(request.headers)
print("follow redirects =", request_options.follow_redirects)
print("verify =", request_options.verify_ssl)
print("attach cookies =", request_options.attach_cookies)
print("proxy =", request_model.options.proxy_url)
print("timeout =", request_model.options.timeout)
print("auth =", request_model.auth)

response = await client.send(
request=request,
Expand Down
20 changes: 16 additions & 4 deletions src/posting/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ def to_httpx_auth(self) -> httpx.Auth | None:
return httpx.DigestAuth(self.digest.username, self.digest.password)
return None

@classmethod
def basic_auth(cls, username: str, password: str) -> Auth:
return cls(type="basic", basic=BasicAuth(username=username, password=password))

@classmethod
def digest_auth(cls, username: str, password: str) -> Auth:
return cls(
type="digest", digest=DigestAuth(username=username, password=password)
)


class BasicAuth(BaseModel):
username: str = Field(default="")
Expand Down Expand Up @@ -101,7 +111,7 @@ class RequestBody(BaseModel):
form_data: list[FormItem] | None = Field(default=None)
"""The form data of the request."""

content_type: str | None = Field(default=None)
content_type: str | None = Field(default=None, init=False)
"""We may set an additional header if the content type is known."""

def to_httpx_args(self) -> dict[str, Any]:
Expand All @@ -122,6 +132,11 @@ def request_sort_key(request: RequestModel) -> tuple[int, str]:


class Scripts(BaseModel):
"""The scripts associated with the request.
Paths are relative to the collection directory root.
"""

setup: str | None = Field(default=None)
"""A relative path to a script that will be run before the template is applied."""

Expand Down Expand Up @@ -154,9 +169,6 @@ class RequestModel(BaseModel):
body: RequestBody | None = Field(default=None)
"""The body of the request."""

content: str | bytes | None = Field(default=None)
"""The content of the request."""

auth: Auth | None = Field(default=None)
"""The authentication information for the request."""

Expand Down
6 changes: 3 additions & 3 deletions src/posting/scripts.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from __future__ import annotations

import asyncio
import sys
from pathlib import Path
from types import ModuleType
from typing import TYPE_CHECKING, Callable, Any
import threading

from httpx import Request, Response
from httpx import Response
from textual.notifications import SeverityLevel

from posting.collection import RequestModel
from posting.variables import get_variables, update_variables

if TYPE_CHECKING:
Expand All @@ -27,7 +27,7 @@ def __init__(self, app: PostingApp):
self._app: PostingApp = app
"""The Textual App instance for Posting."""

self.request: Request | None = None
self.request: RequestModel | None = None
"""The request that is currently being processed."""

self.response: Response | None = None
Expand Down
8 changes: 8 additions & 0 deletions src/posting/widgets/response/script_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@
from textual.reactive import Reactive, reactive
from textual.widgets import Label, RichLog

from posting.help_screen import HelpData

ScriptStatus = Literal["success", "error", "no-script"]


class ScriptOutput(VerticalScroll):
help = HelpData(
title="Script Output",
description="""\
This log displays the output of scripts that executed during the last request.
""",
)
DEFAULT_CSS = """\
ScriptOutput {
padding: 0 2;
Expand Down
Loading

0 comments on commit 7dfbd5d

Please sign in to comment.