Skip to content
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ Thousands of code examples for using the EasyPost API across 7+ programming lang

## Folder Structure

- `community` code snippets contributed from the community. These may include custom workflows, how to integrate EasyPost with other software, etc. These are **unofficial** and **not supported or maintained by EasyPost**.
- `official` official code snippets that populate on the EasyPost website
- `docs` code snippets that populate on our API docs page. Each language will have its own subdirectory
- `fixtures` test data used as fixtures in our client library test suites
- `guides` code snippets that populate on our guides page. Each language will have its own subdirectory
- `responses` responses for our example snippets found in the `docs` directory that will give you a good idea of what to expect back from the EasyPost API
- `community` code snippets contributed from the community. These may include custom workflows, how to integrate EasyPost with other software, etc. These are **unofficial** and **not supported or maintained by EasyPost**.
- `style_guides` contain the style guides and configs for tooling we use in each public language we support
- `tools` contain utilities to help facilitate the code in this repo such as the response snippet generation suite

> NOTE: filenames may not match language convention - this is intentional. Our documentation website uses these example snippets and expects folder/filenames to follow a certain templated naming convention.

Expand Down
2 changes: 2 additions & 0 deletions tools/docs/responses/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cassettes
responses
62 changes: 62 additions & 0 deletions tools/docs/responses/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
PYTHON_BINARY := /opt/python3.10/bin/python3.10
VIRTUAL_ENV := venv
VIRTUAL_BIN := $(VIRTUAL_ENV)/bin
PROJECT_NAME := easypost_responses
TEST_DIR := tests
TARGERT := tests

## help - Display help about make targets for this Makefile
help:
@cat Makefile | grep '^## ' --color=never | cut -c4- | sed -e "`printf 's/ - /\t- /;'`" | column -s "`printf '\t'`" -t

## build - Builds the project in preparation for release
build:
$(VIRTUAL_BIN)/python setup.py sdist bdist_wheel

## clean - Remove the virtual environment and clear out .pyc files
clean:
rm -rf $(VIRTUAL_ENV) dist/ build/ *.egg-info/ .pytest_cache .mypy_cache
find . -name '*.pyc' -delete

## black - Runs the Black Python formatter against the project
black:
$(VIRTUAL_BIN)/black $(PROJECT_NAME)/ $(TEST_DIR)/

## black-check - Checks if the project is formatted correctly against the Black rules
black-check:
$(VIRTUAL_BIN)/black $(PROJECT_NAME)/ $(TEST_DIR)/ --check

## format - Runs all formatting tools against the project
format: black isort

## format-check - Checks if the project is formatted correctly against all formatting rules
format-check: black-check isort-check lint mypy

## install - Install the project locally
install:
$(PYTHON_BINARY) -m venv $(VIRTUAL_ENV)
$(VIRTUAL_BIN)/pip install -e ."[dev]"
git submodule init
git submodule update

## isort - Sorts imports throughout the project
isort:
$(VIRTUAL_BIN)/isort $(PROJECT_NAME)/ $(TEST_DIR)/

## isort-check - Checks that imports throughout the project are sorted correctly
isort-check:
$(VIRTUAL_BIN)/isort $(PROJECT_NAME)/ $(TEST_DIR)/ --check-only

## lint - Lint the project
lint:
$(VIRTUAL_BIN)/flake8 $(PROJECT_NAME)/ $(TEST_DIR)/

## test - Test the project
test:
$(VIRTUAL_BIN)/pytest $(TARGET)

## update-examples-submodule Updates the examples submodule
update-examples-submodule:
git submodule update --remote ../../docs/examples

.PHONY: help build clean black black-check format format-check install isort isort-check lint test
19 changes: 19 additions & 0 deletions tools/docs/responses/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Response Generation Tooling

This tool generates the response snippets that accompany the example snippets on the EasyPost Docs page.

The tool is setup as a "test suite" that will record the requests and responses via `pyvcr` and load the yaml to extract the response content to save as a standalone file. Whenever new responses are needed, simply re-run the tool and each response will be regenerated.

> NOTE: The test names are VERY important and must match up with the expected plural object and action name so that when generated, we can drop them into the examples without manual intervention.

## Usage

> NOTE: Currently, you'll need to run the command twice - it will fail the first time through. The first time will record the cassette, the second time it will save the standalone files. This is necessary because `vcrpy` doesn't save the file until the test function closes, though we call the standalone function inside the test function to capture the test name.

```bash
# Run the following to save recordings to standalone files
make test

# Run the following to overwrite previous interactions
OVERWRITE=true make test
```
Empty file.
164 changes: 164 additions & 0 deletions tools/docs/responses/builder/snippets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import json
import os
from typing import (
Any,
Dict,
Optional,
Tuple,
)

import yaml


ALL_RESOURCES = {
"addresses",
"api-keys",
"batches",
"billing",
"brand",
"carrier-accounts",
"carrier-types",
"child-users",
"customs-infos",
"customs-items",
"endshipper",
"events",
"insurance",
"options",
"orders",
"parcels",
"pickups",
"rates",
"referral-customers",
"refunds",
"reports",
"returns",
"scan-form",
"shipments",
"shipping-insurance",
"smartrate",
"tax-identifiers",
"trackers",
"users",
"webhooks",
}


def build_response_snippet(
interaction_index: Optional[int] = 0, objects_to_persist: Optional[int] = None
):
"""Builds the response snippet from a recorded VCR interaction."""
create_dir("responses")

test_name = os.environ.get("PYTEST_CURRENT_TEST").split(":")[-1].split(" ")[0]
cassette_filename = f"{test_name}.yaml"

cassette_content = extract_response_from_cassette(
cassette_filename, interaction_index
)

response_snippet_folder, bare_snippet_name = save_response_snippet(
cassette_filename, cassette_content, objects_to_persist
)

# Assert the standalone snippet actually got saved, fail if not
assert os.path.exists(
os.path.join("responses", response_snippet_folder, bare_snippet_name)
), f"{bare_snippet_name} standalone snippet file missing!"


def create_dir(dir_name: str):
"""Creates a directory if it does not exist yet."""
if not os.path.exists(dir_name):
os.mkdir(dir_name)


def extract_response_from_cassette(
cassette_filename: str, interaction_index: Optional[int] = 0
) -> Any:
"""Opens a single cassette file and extracts the response content."""
with open(os.path.join("tests", "cassettes", cassette_filename), "r") as cassette:
try:
cassette_data = yaml.safe_load(cassette)
for key, _ in cassette_data.items():
if key == "interactions":
response_content = cassette_data[key][interaction_index][
"response"
]["body"]["string"]
response = response_content if response_content else "{}"
except yaml.YAMLError:
raise

return response


def _setup_saving_response_snippet(response_snippet_filename: str):
"""Reusable helper to setup the logic to save a standalone response snippet."""

bare_snippet_name = response_snippet_filename.replace("test_", "").replace(
".yaml", ".json"
)
split_resource_name = bare_snippet_name.split("_")
first_resource_name = split_resource_name[0]
second_resource_name = split_resource_name[1]
first_and_second_resource_name = f"{first_resource_name}-{second_resource_name}"
resource_name = (
first_resource_name
if first_and_second_resource_name not in ALL_RESOURCES
else first_and_second_resource_name
)

# Setup the names like the website wants it
response_snippet_folder = resource_name.replace("_", "-")
bare_snippet_name = bare_snippet_name.replace("_", "-")

create_dir(os.path.join("responses", response_snippet_folder))

return response_snippet_folder, bare_snippet_name


def save_response_snippet(
response_snippet_filename: str,
response_snippet_content: Any,
objects_to_persist: Optional[int] = None,
) -> Tuple[str, str]:
"""Saves the response content of a cassette to a standalone snippet file."""
response_snippet_folder, bare_snippet_name = _setup_saving_response_snippet(
response_snippet_filename
)

with open(
os.path.join("responses", response_snippet_folder, bare_snippet_name), "w"
) as response_snippet_file:
if objects_to_persist:
json.dump(
json.loads(response_snippet_content)[:objects_to_persist],
response_snippet_file,
indent=2,
)
else:
json.dump(
json.loads(response_snippet_content), response_snippet_file, indent=2
)

response_snippet_file.write("\n")

return response_snippet_folder, bare_snippet_name


def save_raw_json(response_snippet_filename: str, response_dict: Dict[str, Any]):
"""Saves a raw response dictionary to a standalone snippet file (used for hard-coded responses
that cannot easily be plugged into a test suite (eg: Billing functions).
"""
response_snippet_folder, bare_snippet_name = _setup_saving_response_snippet(
response_snippet_filename
)

bare_snippet_name = f"{bare_snippet_name}.json"

with open(
os.path.join("responses", response_snippet_folder, bare_snippet_name), "w"
) as response_snippet_file:
json.dump(response_dict, response_snippet_file, indent=2)

response_snippet_file.write("\n")
45 changes: 45 additions & 0 deletions tools/docs/responses/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from setuptools import (
find_packages,
setup,
)


REQUIREMENTS = [
"easypost==7.*",
"python-dotenv",
]

DEV_REQUIREMENTS = [
"black==22.*",
"flake8==5.*",
"isort==5.*",
"pytest-vcr==1.*",
"pytest==7.*",
"vcrpy==4.*",
]

with open("README.md", encoding="utf-8") as f:
long_description = f.read()

setup(
name="easypost_responses",
version="0.1.0",
description="",
author="EasyPost",
author_email="[email protected]",
url="https://easypost.com/",
packages=find_packages(
exclude=[
"examples",
"tests",
]
),
install_requires=REQUIREMENTS,
extras_require={
"dev": DEV_REQUIREMENTS,
},
test_suite="test",
long_description=long_description,
long_description_content_type="text/markdown",
python_requires=">=3.6, <4",
)
Empty file.
Loading