Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutable api #117

Merged
merged 5 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading