Repository: hiredscorelabs/cornell
Branch: master
Commit: e9f49fef2dbb
Files: 47
Total size: 66.2 KB
Directory structure:
gitextract_5knty9pr/
├── .circleci/
│ └── config.yml
├── .coveragerc
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .pylintrc
├── .tool-versions
├── .travis.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cornell/
│ ├── __init__.py
│ ├── _version.py
│ ├── cornell_helpers.py
│ ├── cornell_server.py
│ ├── custom_matchers.py
│ ├── signals.py
│ └── vcr_settings.py
├── docs/
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── docs/
│ │ ├── examples.md
│ │ └── workflows/
│ │ ├── _category_.json
│ │ ├── basic_workflow.md
│ │ ├── custom_matchers.md
│ │ ├── own_module.md
│ │ └── subscribing_to_hooks.md
│ ├── docusaurus.config.js
│ ├── package.json
│ ├── sidebars.js
│ ├── src/
│ │ ├── components/
│ │ │ ├── HomepageFeatures.js
│ │ │ └── HomepageFeatures.module.css
│ │ ├── css/
│ │ │ └── custom.css
│ │ └── pages/
│ │ ├── index.js
│ │ └── index.module.css
│ └── static/
│ └── .nojekyll
├── setup.py
└── tests/
├── __init__.py
├── conftest.py
├── test_get_custom_vcr.py
├── test_odata_query_processing.py
├── test_record_and_replay.py
├── test_record_errors.py
├── test_request_path.py
├── test_signals.py
├── test_soap_processing.py
└── test_vcr_dir.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
version: 2.1
workflows:
build_and_deploy:
jobs:
- test-flow:
filters:
tags:
only: /.*/
jobs:
test-flow:
docker:
- image: cimg/python:3.11.5
steps:
- checkout
- run:
name: Create virtualenv
command: |
python -m venv ~/venv
- run:
name: Install dependencies
command: |
. ~/venv/bin/activate
pip install -U pip
pip install --progress-bar off -r requirements.txt -U -e .
- run:
name: Run pylint
command: |
. ~/venv/bin/activate
pylint --rcfile=.pylintrc cornell tests
- run:
name: Run pytest
command: |
. ~/venv/bin/activate
pytest
================================================
FILE: .coveragerc
================================================
[run]
source = cornell
[report]
omit = cornell/_version.py
fail_under = 70
================================================
FILE: .github/workflows/main.yml
================================================
name: Cornell CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
make configure
- name: Analysing the code with pylint
run: |
pylint --rcfile=.pylintrc cornell tests
- name: Unit Testig - pytest
run: |
pytest
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
.idea
.vscode
================================================
FILE: .pylintrc
================================================
[MESSAGES CONTROL]
disable=R,missing-docstring,redefined-outer-name
[FORMAT]
max-line-length=140
[REPORTS]
reports=no
================================================
FILE: .tool-versions
================================================
python 3.11.5
================================================
FILE: .travis.yml
================================================
language: python
python:
- "3.7.11"
- "3.8"
- "3.9"
install:
- make configure
script:
- make test
jobs:
include:
- stage: deploy
python: 3.9
deploy:
provider: script
script: make docker_build_push
on:
branch: master
================================================
FILE: Dockerfile
================================================
FROM python:3
RUN pip install cornell
EXPOSE 9000
ENTRYPOINT ["cornell"]
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 HiredScore inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
test:
pylint --rcfile=.pylintrc cornell tests
pytest --cov=cornell tests
configure:
pip install -e .'[dev]'
docker_build_push:
docker build . -t hiredscorelabs/cornell:latest
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker push hiredscorelabs/cornell:latest
publish:
python setup.py sdist
pip install twine
twine upload dist/*
docker_tag_push:
REPO="hiredscorelabs/cornell"
TAG="${REPO}:$(python cornell/_version.py)"
docker pull ${REPO}:latest
docker tag ${REPO}:latest ${TAG}
echo "${DOCKER_PASSWORD}" | docker login -u "${DOCKER_USERNAME}" --password-stdin
docker push ${TAG}
================================================
FILE: README.md
================================================
# Cornell: record & replay mock server
[](https://app.travis-ci.com/github/hiredscorelabs/cornell)
[](https://www.python.org/downloads/release/python-390/)
[](https://hub.docker.com/r/hiredscorelabs/cornell)
<p align="center">
<img src="https://imgur.com/ShxP4AI.png" alt="Cornell Logo">
</p>
> Cornell makes it dead simple, via its record and replay features to perform end-to-end testing in a fast and isolated testing environment.
When your application integrates with multiple web-based services, end-to-end testing is crucial before deploying to production.
Mocking is often a tedious task. It becomes even more tiresome when working with multiple APIs from multiple vendors.
[vcrpy](https://github.com/kevin1024/vcrpy) is an awesome library that records and replays HTTP interactions for unit tests. Its output is saved to reusable "cassette" files.
By wrapping vcrpy with Flask, Cornell provides a lightweight record and replay server that can be easily used during distributed system testing and simulate all HTTP traffic needed for your tests.
## Basic Use Case
When you're working with distributed systems, the test client entry point triggers a cascade of events that eventually send HTTP requests to an external server

With Cornell server started, it will act as a proxy (**record mode**) between the outgoing HTTP requests and the external server and will record all relevant interactions.
Once interactions are recorded, Cornell can work in replay mode, replacing the external server entirely, short-circuiting the calls and instead, replying back instantly with the previously recorded response.

## Installation
To install from [PyPI](https://pypi.org/project/cornell/), all you need to do is this:
```bash
pip install cornell
```
## Usage
```bash
Usage: cornell_server.py [OPTIONS]
Usage Examples: Record mode: `cornell --forward_uri="https://remote_server/api" --record -cd custom_cassette_dir`
Replay mode: `cornell -cd custom_cassette_dir
Options:
-h, --host TEXT Set listen ip address
-p, --port INTEGER
-ff, --forward_uri TEXT Must be provided in case of recording mode
- , --record-once / --record-all
Record each scenario only once, ignore the
rest
-r, --record Start server in record mode
-fp, --fixed-path Fixed cassettes path. If enabled, Cornell
will support only one server for recording
-cd, --cassettes-dir TEXT Cassettes parent directory, If not
specified, Cornell parent dir will be used
-re, --record-errors BOOLEAN If enabled, Cornell will record erroneous
responses
--help Show this message and exit.
```
## Demo - Full Example
Start Cornell in record mode:
```
cornell -ff https://api.github.com/ --record -cd cassettes
```
This will start the server in record-proxy mode on port `9000`, and will forward all requests to `https://api.github.com/`

When cornell is in record mode, it will forward all request to the specified forwarding URL, for example:
```
requests.get("http://127.0.0.1:9000/github/repos/kevin1024/vcrpy/license").json()
```
or
```
requests.get("http://127.0.0.1:9000/github/repos/kevin1024/vcrpy/contents").json()
```
or you can browse to the URL using your browser

Cornell will forward the request to the specified URL and will record both the request and the response.
The yaml cassettes will be recorded to a dedicated directory (by default, `cassettes` in the root dir)
For example:

__Note__
By default, `cassettes` directory will be created in cornell's root dir and will contain the cassette by destination hierarchy.
Use `-cd` to specify custom directory for your cassettes.
Mind that `-cd <custom_dir> should match for both record and replay modes
Once all the necessary interactions were recorded, stop cornell server using *ctrl+c*.
Once stopped, all interactions will be mapped via an auto-generated `index.yaml` file.
__Note__
In case the `index.yaml` is already present, it will be updated with new interactions. Otherwise, a new file will be created.
__Note__
Cornell doesn't record interactions with an erroneous response, by default (i.e response with 404, will omitted). If you wish to enable this option, run cornell with --record-errors flag
In this specific example, we can see that the 2 requests are mapped to the saved cassettes:

### Start cornell as docker container
```bash
docker run hiredscorelabs/cornell:latest
```
### Build cornell as docker container
```bash
docker build -t cornell .
docker run cornell --help
```
You will probably need to import cassettes from a local directory from your computer.
To do that, use the following command to mount a local directory as a volume in the container.
```bash
docker run -v ~/cassettes:/var/cassettes cornell -cd /var/cassettes
```
In some case, you want to use another port with cornell. If you need to do that, you should use
docker port mapping as in the following where cornell will listen on port `9020`.
```bash
docker run -p 9020:9000 cornell
```
## Features
### Request Matchers
In addition to the [vcrpy matchers](https://vcrpy.readthedocs.io/en/latest/configuration.html#request-matching), cornell provides the following custom request matchers:
- [OData](https://www.odata.org/getting-started/basic-tutorial/) request query matcher
- [SOAP](https://stoplight.io/api-types/soap-api/) request body matcher
### Environment Variables
Since Cornell is a testing server it's executed by default with `FLASK_ENV=local`.
You can modify this as described in [flask configuration](https://flask.palletsprojects.com/en/2.0.x/config/#configuration-handling)
### Advanced Features
Can be found in the [documentation](https://hiredscorelabs.github.io/cornell/docs/examples/)
## Contributing
Yes please! contributions are more than welcome!
Please follow [PEP8](https://www.python.org/dev/peps/pep-0008/) and the [Python Naming Conventions](https://pep8.org/#prescriptive-naming-conventions)
Add tests when you're adding new functionality and make sure all the existing tests are happy and green :)
To set up development environment:
```sh
python -m venv venv
source venv/bin/activate
make configure
```
## Running Tests
To run tests, run the following command
```bash
python -m venv venv
source venv/bin/activate
make test
```
================================================
FILE: cornell/__init__.py
================================================
================================================
FILE: cornell/_version.py
================================================
__version__ = (1, 1, 0)
if __name__ == "__main__":
print('.'.join(map(str, __version__)))
================================================
FILE: cornell/cornell_helpers.py
================================================
import logging
from collections.abc import Iterable
from contextlib import contextmanager
from urllib.parse import urlparse
import xmltodict
import yaml
from flask import request, current_app
from toolz import get_in
from yarl import URL
ODATA_EXPEND_FILTER = "$expand"
def get_vcr_dir_from_request():
return URL(request.path.lstrip("/")).parts[0]
def get_localhost_uri():
if current_app.config.fixed_path:
return URL(request.host_url)
return str(URL(request.host_url) / get_vcr_dir_from_request())
def get_paths_in_nested_dict_by_condition(response_body, condition, path=None):
path = path or []
if isinstance(response_body, Iterable) and not isinstance(
response_body, (str, int, bool)
):
items = (
enumerate(response_body)
if isinstance(response_body, list)
else response_body.items()
)
for index_or_key, value in items:
new_path = list(path)
new_path.append(index_or_key)
for result in get_paths_in_nested_dict_by_condition(
value, condition, path=new_path
):
yield result
if isinstance(response_body, dict) and condition(index_or_key, value):
new_path = list(path)
new_path.append(index_or_key)
yield new_path
def update_nested_dict_value(original_dict, old_value, new_value):
if isinstance(original_dict, dict):
for key, value in original_dict.copy().items():
original_dict[key] = update_nested_dict_value(value, old_value, new_value)
return original_dict
elif isinstance(original_dict, list):
for index in original_dict.copy():
original_dict.append(update_nested_dict_value(index, old_value, new_value))
return original_dict
return original_dict if original_dict != old_value else new_value
def replace_locations_in_xml(response_body):
def match_location_with_uri(key, value):
if key == "@location":
url = urlparse(value)
return all([url.scheme, url.netloc, url.path])
response_body_dict = xmltodict.parse(response_body)
for location_paths in get_paths_in_nested_dict_by_condition(
response_body_dict, match_location_with_uri
):
old_location = get_in(location_paths, response_body_dict)
new_location = request.url.split("?wsdl")[0]
response_body_dict = update_nested_dict_value(
response_body_dict, old_location, new_location
)
return xmltodict.unparse(response_body_dict)
def strip_soap_namespaces_from_body(request_data):
processed_data = xmltodict.parse(request_data, process_namespaces=True)
namespace_paths = sum(
list(
get_paths_in_nested_dict_by_condition(
processed_data, condition=lambda key, _: key == "@xmlns"
)
),
[],
)
if not namespace_paths:
processed_body = request_data
else:
body = _get_xml_body_without_namespaces(
namespace_paths=namespace_paths,
processed_data=processed_data,
request_data=request_data,
)
processed_body = xmltodict.unparse(body)
return (
processed_body.decode() if isinstance(processed_body, bytes) else processed_body
)
def _get_xml_body_without_namespaces(*, namespace_paths, processed_data, request_data):
namespaces = get_in(namespace_paths, processed_data)
stripped_namespaces = {value: None for value in namespaces.values()}
without_namespaces = xmltodict.parse(
request_data, process_namespaces=True, namespaces=stripped_namespaces
)
body_path = sum(
list(
get_paths_in_nested_dict_by_condition(
without_namespaces, condition=lambda key, _: key == "Body"
)
),
[],
)
return get_in(body_path, without_namespaces)
def xml_in_headers(entity):
return "/xml" in entity.headers.get("Content-Type", "")
def json_in_headers(entity):
return "application/json" in entity.headers.get("Content-Type", "")
def expand_in_query(entity):
return (
entity.query
and isinstance(entity.query, list)
and [items for items in entity.query if ODATA_EXPEND_FILTER in items]
)
@contextmanager
def set_underlying_vcr_logging_level(logging_level=logging.WARNING):
vcr_logger = logging.getLogger("vcr.cassette")
orig_level = vcr_logger.level
vcr_logger.setLevel(logging_level)
try:
yield
finally:
vcr_logger.setLevel(orig_level)
def saved_data_to_yaml(data, yaml_path):
yaml.safe_dump(
dict(data), open(yaml_path, "w", encoding="utf-8"), encoding="utf-8", allow_unicode=True
)
================================================
FILE: cornell/cornell_server.py
================================================
#!/usr/bin/env python
# pylint: disable=no-member
import atexit
import logging
from collections import defaultdict
from contextlib import contextmanager, nullcontext
from functools import partial
from http import HTTPStatus
from os import environ
from pathlib import Path
import signal
import click
import requests
from requests import RequestException
import yaml
from flask import Flask, request
from structlog import get_logger
from toolz import get_in
from werkzeug.exceptions import NotFound
from werkzeug.utils import secure_filename
from yarl import URL
from cornell.vcr_settings import get_custom_vcr, request_has_matches
from cornell.cornell_helpers import (get_vcr_dir_from_request, replace_locations_in_xml, xml_in_headers,
set_underlying_vcr_logging_level, saved_data_to_yaml)
from cornell.signals import on_cornell_exit, signal_context
app = Flask("CornellMock")
DUMMY_URL = "http://cornell-proxy/"
SUPPORTED_METHODS = ['GET', 'POST', 'PUT', 'DELETE']
ROOT_NAME = "root"
class CassetteFileMissing(NotFound):
pass
@app.route("/ping", methods=["GET"])
def ping():
return "Pong!"
@app.route('/', defaults={'path': '', }, methods=SUPPORTED_METHODS)
@app.route('/<path:path>', methods=SUPPORTED_METHODS)
def handle_requests(path):
app.logger.info("Got request", url=request.url, method=request.method, args=request.args, body=request.get_data())
with _cassette_url_player_context(path) as updated_path:
initial_request = _build_initial_request(updated_path)
resp = requests.Session().send(initial_request.prepare(), stream=True)
response_body = _process_response_body(resp)
_processes_headers(resp)
resp.raise_for_status()
return response_body, resp.status_code, resp.headers.items()
def _build_initial_request(path):
headers = dict(request.headers)
headers.pop("Host")
url = URL(app.config.base_uri) / path
if 'wsdl' in request.args:
url = url.with_query("wsdl")
request.args = None # pylint: disable=assigning-non-slot
return requests.Request(request.method, url=url, headers=headers, data=request.data, params=request.args)
def _process_response_body(response):
response_body = response.raw.read()
if app.config.record and xml_in_headers(response):
response_body = replace_locations_in_xml(response_body)
return response_body
def _processes_headers(resp):
if resp.status_code not in (HTTPStatus.TEMPORARY_REDIRECT, HTTPStatus.PERMANENT_REDIRECT):
# https://github.com/psf/requests/issues/3490
for header in ('Content-Length', 'Content-Type', 'Transfer-Encoding'):
resp.headers.pop(header, None)
@contextmanager
def _cassette_url_player_context(url_path):
vcr_dir = ROOT_NAME
if not app.config.fixed_path:
vcr_dir = get_vcr_dir_from_request()
url_path = "/".join(URL(url_path).parts[1:])
app.logger.info(f"url path updated to {url_path}")
saved_cassette_name = get_in([vcr_dir, f"/{url_path}"], app.config.cassette_paths)
cassette_file_path = _determine_cassette_path(saved_cassette_name, vcr_dir)
with signal_context("process_cassette_file", cassette_file_path) as cassette_file:
cassette_file = cassette_file or cassette_file_path
cassette_path = _determine_cassette_path(cassette_file, vcr_dir)
if not cassette_path or not cassette_path.exists():
if not app.config.record:
raise CassetteFileMissing(description=f"Cassette file for {url_path} is missing."
f" Did you forget to record it?")
cassette_path = _generate_cassette_path(vcr_dir, url_path)
app.logger.info(f"Cassette file will be recorded to {cassette_path}")
player_context = partial(app.config.vcr.use_cassette, cassette_path)
else:
player_context = _obtain_player_context(str(cassette_path))
with set_underlying_vcr_logging_level(), player_context():
save_cassette_to_index = True
try:
yield url_path
except RequestException:
save_cassette_to_index = not app.config.record or app.config.record_errors
finally:
if save_cassette_to_index:
added_path = {f"/{url_path}": Path(cassette_file).name if cassette_file else Path(cassette_path).name}
app.config.cassette_paths.setdefault(vcr_dir, {}).update(added_path)
elif app.config.record:
app.logger.info("Error encountered, cassette won't be recorded", cassette_path={cassette_path})
def _determine_cassette_path(cassette_file, vcr_dir):
if not cassette_file:
return
if app.config.fixed_path:
return (Path(app.config.cassettes_dir)/cassette_file).absolute()
return (Path(app.config.cassettes_dir)/vcr_dir/cassette_file).absolute()
def _obtain_player_context(cassette_path):
app.logger.info("Using cassette", cassette_path=cassette_path)
if app.config.record and app.config.record_once and request_has_matches(cassette_path, request):
app.logger.info("Request already found in cassette, not recording", cassette_path=cassette_path, url=request.url)
return nullcontext
return partial(app.config.vcr.use_cassette, cassette_path, record_mode="new_episodes")
def _generate_cassette_path(vcr_dir, url_path):
file_path = f'{secure_filename(url_path)}.yaml' if url_path else "null"
record_path = app.config.cassettes_dir if app.config.fixed_path else _get_vcr_dir(vcr_dir)
return str((record_path / file_path).absolute())
def _load_cassette_paths(parent_dir):
if app.config.index_file.exists():
return yaml.safe_load(app.config.index_file.read_text())
app.logger.info("Could not find index file. Creating new file", parent_dir=parent_dir)
cassette_paths = {}
for file_path in parent_dir.glob("**/*.yaml"):
cassette_data = yaml.safe_load(file_path.read_text())
for interaction in cassette_data.get("interactions", []):
relative_url = URL(get_in(["request", "uri"], interaction)).relative().raw_path
cassette_paths.setdefault(relative_url, str(file_path))
return _generate_index_yaml(parent_dir, cassette_paths)
def _generate_index_yaml(parent_dir, cassette_paths):
index_dict = defaultdict(dict)
for url, cassette_path in cassette_paths.items():
parent = Path(cassette_path).relative_to(parent_dir).parent
file_name = Path(cassette_path).relative_to(parent_dir).name
parent = parent if parent.stem else "root"
index_dict[str(parent)].update({url: file_name})
saved_data_to_yaml(index_dict, app.config.index_file)
app.logger.info("Index file was created", parent_dir=parent_dir, index_file_path=app.config.index_file)
return index_dict
def _get_vcr_dir(cassettes_dir):
record_path = app.config.cassettes_dir / Path(cassettes_dir).expanduser()
return create_cassettes_dir(record_path)
def create_cassettes_dir(cassettes_dir):
home_dir = cassettes_dir or Path(__file__).absolute().parent / "cassettes"
Path(home_dir).mkdir(parents=True, exist_ok=True)
return Path(home_dir)
def _setup_app_config(*, app, cassettes_dir, fixed_path, forward_uri, record, record_once, additional_vcr_matchers,
record_errors):
environ["FLASK_ENV"] = environ.get("FLASK_ENV") or "local"
app.config.cassettes_dir = create_cassettes_dir(cassettes_dir)
app.config.record = record
app.config.record_once = record_once
app.config.base_uri = forward_uri if record else DUMMY_URL
app.config.fixed_path = fixed_path
app.config.index_file = (Path(app.config.cassettes_dir)/"index.yaml").absolute()
app.config.cassette_paths = _load_cassette_paths(app.config.cassettes_dir)
app.config.vcr = get_custom_vcr(base_uri=app.config.base_uri, mock_uri=DUMMY_URL, record_errors=record_errors,
*additional_vcr_matchers)
app.config.record_errors = record_errors
def _get_logging_service():
logger = get_logger()
logger.level = logging.INFO
return logger
class CornellCmdOptions(click.Command):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
options = [click.core.Option(("-cd", "--cassettes-dir"), required=False,
help="Cassettes parent directory, If not specified, Cornell parent dir will be used"),
click.core.Option(("-fp", "--fixed-path"), required=False, default=False, is_flag=True,
help="Fixed cassettes path. If enabled, Cornell will support only one server for recording"),
click.core.Option(("-r", "--record"), default=False, is_flag=True,
help="Start server in record mode"),
click.core.Option(("-", "--record-once/--record-all"), default=True, is_flag=True,
help="Record each scenario only once, ignore the rest"),
click.core.Option(("-ff", "--forward_uri"), help="Must be provided in case of recording mode"),
click.core.Option(("-h", "--host"), default="127.0.0.1"),
click.core.Option(("-p", "--port"), default=9000),
click.core.Option(("-re", "--record-errors"), default=False, is_flag=True,
help="If enabled, Cornell will record erroneous responses")]
for option in options:
self.params.insert(0, option)
@click.command(cls=CornellCmdOptions)
def start_mock_service(cassettes_dir, fixed_path, record, record_once, forward_uri, host, port, record_errors):
"""
Usage Examples:
Record mode: `cornell --forward_uri="https://remote_server/api" --record -cd custom_cassette_dir`
Replay mode: `cornell -cd custom_cassette_dir
"""
start_cornell(cassettes_dir=cassettes_dir, forward_uri=forward_uri, host=host, port=port, record=record,
record_once=record_once, fixed_path=fixed_path, record_errors=record_errors)
def start_cornell(*, cassettes_dir, forward_uri, host, port, record, record_once, fixed_path, record_errors, additional_vcr_matchers=()):
app.config.update(PROPAGATE_EXCEPTIONS=True)
if record and not forward_uri:
raise click.ClickException("Record mode requires forward URI")
with signal_context("logging_setup") as logging_service:
app.logger = logging_service or _get_logging_service()
_setup_app_config(app=app, cassettes_dir=cassettes_dir, fixed_path=fixed_path, forward_uri=forward_uri,
record=record, record_once=record_once, additional_vcr_matchers=additional_vcr_matchers,
record_errors=record_errors)
app.logger.info("Starting Cornell", app_name=app.name, host=host, port=port, record=record, record_once=record_once,
fixed_path=fixed_path, forward_uri=forward_uri, cassettes_dir=str(cassettes_dir),
record_errors=record_errors)
atexit.register(on_cornell_exit, app=app)
signal.signal(signal.SIGTERM, lambda: on_cornell_exit(app))
app.run(host=host, port=port, threaded=False)
if __name__ == "__main__":
start_mock_service() # pylint: disable=no-value-for-parameter
================================================
FILE: cornell/custom_matchers.py
================================================
import functools
from vcr.matchers import query, body
from vcr.util import read_body
from cornell.cornell_helpers import expand_in_query, ODATA_EXPEND_FILTER, xml_in_headers, \
strip_soap_namespaces_from_body
from cornell.signals import signal_context
def requests_match_conditions(*conditions):
def decorator(func):
@functools.wraps(func)
def wrapper(received_request, cassette_request):
if all([condition(received_request) and condition(cassette_request) for condition in conditions]):
return func(received_request, cassette_request)
# Condition unmet, return True to skip the matcher
return True
return wrapper
return decorator
def replace_matchers(custom_matcher, *conditions):
"""
If conditions are met, replace matcher with custom matcher
"""
def decorator(func):
@functools.wraps(func)
def wrapper(received_request, cassette_request):
if all([condition(received_request) and condition(cassette_request) for condition in conditions]):
return custom_matcher(received_request, cassette_request)
return func(received_request, cassette_request)
return wrapper
return decorator
def _vcr_odata_query_matcher(received_request, cassette_request):
received_request_query, cassette_request_query = dict(received_request.query), dict(cassette_request.query)
received_expand_query = received_request_query[ODATA_EXPEND_FILTER]
cassette_expand_query = cassette_request_query[ODATA_EXPEND_FILTER]
received_request_query.pop(ODATA_EXPEND_FILTER)
cassette_request_query.pop(ODATA_EXPEND_FILTER)
assert received_request_query == cassette_request_query, "OData queries don't match"
assert set(received_expand_query.split(",")) == set(cassette_expand_query.split(",")), f"Odata {ODATA_EXPEND_FILTER} don't match"
@replace_matchers(_vcr_odata_query_matcher, expand_in_query, lambda request: request.query)
def extended_query_matcher(received_request, cassette_request):
query(received_request, cassette_request)
def _vcr_xml_body_matcher(cassette_request, received_request):
received_request_body = read_body(received_request)
cassette_request_body = read_body(cassette_request)
assert strip_soap_namespaces_from_body(received_request_body) == strip_soap_namespaces_from_body(cassette_request_body)
@replace_matchers(_vcr_xml_body_matcher, xml_in_headers, lambda request: request.body)
def extended_vcr_body_matcher(received_request, cassette_request):
with signal_context("additional_body_matching", dict(cassette_request=cassette_request,
received_request=received_request)) as body_matched:
return body_matched or body(cassette_request, received_request)
================================================
FILE: cornell/signals.py
================================================
from contextlib import contextmanager
import yaml
from blinker import Namespace
from cornell.cornell_helpers import saved_data_to_yaml
cornell_signals = Namespace()
# Supported Signals:
logging_setup = cornell_signals.signal("logging_setup")
process_cassette_file = cornell_signals.signal("process_cassette_file")
additional_body_matching = cornell_signals.signal("additional_body_matching")
class MultipleSignalSubscribers(Exception):
pass
class SignalNotRegistered(Exception):
pass
def on_cornell_exit(app):
if app.config.record:
index_dict = yaml.safe_load(app.config.index_file.read_text())
index_dict.update(dict(app.config.cassette_paths))
saved_data_to_yaml(index_dict, app.config.index_file)
app.logger.info("Index file was updated", index_file_path=app.config.index_file)
@contextmanager
def signal_context(signal_name, *args, **kwargs):
pending_signal = cornell_signals.get(signal_name)
if not pending_signal:
raise SignalNotRegistered(f"Signal {signal_name} not registered")
if len(pending_signal.receivers) > 1:
raise MultipleSignalSubscribers(f"Only one subscriber allowed for {signal_name}. Found: {pending_signal.receivers}")
results = pending_signal.send(*args, **kwargs)
yield results[0][1] if results else None
================================================
FILE: cornell/vcr_settings.py
================================================
from http import HTTPStatus
import vcr
from flask import request
from toolz import get_in
from vcr.cassette import Cassette
from vcr.request import Request
from vcr.matchers import method
from vcr.persisters.filesystem import FilesystemPersister
from cornell.cornell_helpers import (replace_locations_in_xml, xml_in_headers, strip_soap_namespaces_from_body,
set_underlying_vcr_logging_level)
from cornell.custom_matchers import extended_query_matcher, extended_vcr_body_matcher
MATCHERS = {'host', 'method', 'path', 'port', 'scheme'}
class CustomPersister(FilesystemPersister):
base_uri = None
mock_url = None
record_errors = False
@classmethod
def save_cassette(cls, cassette_path, cassette_dict, serializer):
for cassette_request, cassette_response in zip(cassette_dict["requests"], cassette_dict["responses"]):
if not cls.record_errors and get_in(["status", "code"], cassette_response) >= HTTPStatus.BAD_REQUEST:
return
cassette_request.uri = cassette_request.uri.replace(cls.base_uri, cls.mock_url)
if cassette_request.body and xml_in_headers(cassette_request):
cassette_request.body = strip_soap_namespaces_from_body(cassette_request.body)
if any("xml" in content_type for content_type in cassette_response["headers"].get('Content-Type', [])):
cassette_response["body"]["string"] = replace_locations_in_xml(cassette_response["body"]["string"])
FilesystemPersister.save_cassette(cassette_path, cassette_dict, serializer)
def get_custom_vcr(*additional_vcr_matchers, base_uri, mock_uri, record_errors):
custom_vcr = vcr.VCR(decode_compressed_response=True)
CustomPersister.base_uri = base_uri.rstrip("/")
CustomPersister.mock_url = mock_uri.rstrip("/")
CustomPersister.record_errors = record_errors
custom_vcr.register_persister(CustomPersister)
match_on_list = MATCHERS
match_on_list.update(_register_additional_matchers(custom_vcr, *additional_vcr_matchers, extended_vcr_body_matcher,
extended_query_matcher))
custom_vcr.match_on = tuple(match_on_list)
return custom_vcr
def _register_additional_matchers(custom_vcr, *additional_vcr_matchers):
matchers = set()
for matcher in additional_vcr_matchers:
assert callable(matcher), f"VCR matcher must be callable, for {type(matcher)} instead"
custom_vcr.register_matcher(matcher.__name__, matcher)
matchers.add(matcher.__name__)
return matchers
def request_has_matches(cassette_path, flask_request):
cassette_requests = Request(method=flask_request.method, uri=request.url, body=flask_request.data, headers=flask_request.headers)
with set_underlying_vcr_logging_level():
cassette = Cassette.load(path=cassette_path, match_on=(extended_vcr_body_matcher, extended_query_matcher,
method, extended_vcr_body_matcher))
for matches in cassette.find_requests_with_most_matches(cassette_requests):
*_, failure = matches
if not failure:
return True
return False
================================================
FILE: docs/.gitignore
================================================
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: docs/README.md
================================================
# Website
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
## Installation
```console
yarn install
```
## Local Development
```console
yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
## Build
```console
yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
## Deployment
```console
GIT_USER=<Your GitHub username> USE_SSH=true yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
================================================
FILE: docs/babel.config.js
================================================
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};
================================================
FILE: docs/docs/examples.md
================================================
---
sidebar_position: 1
sidebar_label: System in Action
title: System in Action
---

================================================
FILE: docs/docs/workflows/_category_.json
================================================
{
"label": "Workflows",
"position": 2
}
================================================
FILE: docs/docs/workflows/basic_workflow.md
================================================
---
sidebar_position: 1
sidebar_label: Basic Workflow
title: Basic Workflow
---
Staring Cornell in record mode:
```
cornell -ff https://api.github.com/ --record -cd cassettes
```
This will start the server in record-proxy mode on port `9000`, and will forward all requests to `https://api.github.com/`

When cornell is in record mode, it will forward all request to the specified forwarding URL, for example:
```
requests.get("http://127.0.0.1:9000/github/repos/kevin1024/vcrpy/license").json()
```
or
```
requests.get("http://127.0.0.1:9000/github/repos/kevin1024/vcrpy/contents").json()
```
or you can browse to the URL using your browser

Cornell will forward the request to the specified URL and will record both the request and the response.
The yaml cassettes will be recorded to a dedicated directory (by default, `cassettes` in the root dir)
For example:

__Note__
By default, `cassettes` directory will be created in cornell's root dir and will contain the cassette by destination hierarchy.
Use `-cd` to specify custom directory for your cassettes.
Mind that `-cd <custom_dir> should match for both record and replay modes
Once all the necessary interactions were recorded, stop cornell server using *ctrl+c*.
Once stopped, all interactions will be mapped via an auto-generated `index.yaml` file.
__Note__
In case the `index.yaml` was already present, it will be updated with new interactions, otherwise new file will be created.
__Note__
Cornell doesn't record interactions with an erroneous response, by default (i.e response with 404, will omitted). If you wish to enable this option, run cornell with --record-errors flag
In this specific example, we can see that the 2 requests are mapped to the saved cassettes:

================================================
FILE: docs/docs/workflows/custom_matchers.md
================================================
---
sidebar_position: 3
sidebar_label: Adding Custom Matchers
title: Adding Custom Matchers
---
In some cases you'd want to add [custom request macthers](https://vcrpy.readthedocs.io/en/latest/advanced.html#register-your-own-request-matcher) to Cornell.
This can be easily done using the wrapper we created in the above example, with the `additional_vcr_matchers` param:
```python
#!/usr/bin/env python
import click
import json
from vcr.util import read_body
from cornell.cornell_server import CornellCmdOptions, start_cornell
from cornell.cornell_helpers import json_in_headers
from cornell.custom_matchers import requests_match_conditions
# Custom Matcher
@requests_match_conditions(json_in_headers, lambda request: request.body)
def vcr_json_custom_body_matcher(received_request, cassette_request):
received_request_dict = json.loads(read_body(received_request))
cassette_request_dict = json.loads(read_body(cassette_request))
if received_request_dict == cassette_request_dict or "special_params" not in received_request_dict:
return True
return is_specially_matched(received_request_dict, cassette_request_dict)
@click.command(cls=CornellCmdOptions)
def start_mock_service(**kwargs):
start_cornell(additional_vcr_matchers=[vcr_json_custom_body_matcher], **kwargs)
if __name__ == "__main__":
start_mock_service()
```
In this example, we've added `vcr_json_custom_body_matcher` as an `additional_vcr_matchers`.
Notice that Cornell also provides the `requests_match_conditions` decorator, in case you'd want to activate your matcher only under specific circumstances.
**Note**: If you're adding a custom matcher that actually implements standard protocols that can be widely used, kindly consider adding it as [PR](https://github.com/hiredscorelabs/cornell) to Cornell.
Your contribution will be really appreciated!
================================================
FILE: docs/docs/workflows/own_module.md
================================================
---
sidebar_position: 2
sidebar_label: Starting Cornell from Your Own Module
title: Starting Cornell from Your Own Module
---
In order to extend Cornell with additional matchers, or register to its hooks,
you will first need to start the Cornell service from your own internal module.
This can be easily done by inheriting `CornellCmdOptions` [click](https://click.palletsprojects.com/en/8.0.x/) from `cornell.cornell_server`
For example:
In a separate module (i.e. `cornell_wrapper.py`), create the following:
```python
#!/usr/bin/env python
import click
from pathlib import Path
from cornell.cornell_server import CornellCmdOptions, start_cornell
@click.command(cls=CornellCmdOptions)
@click.option('--hello', default=False, is_flag=True, help="Say hello")
def start_mock_service(hello, **kwargs):
cassettes_dir = Path(__file__).absolute().parent/"mock_service"
if hello:
print("Hello from Cornell :)")
return
start_cornell(cassettes_dir=cassettes_dir, **kwargs)
if __name__ == "__main__":
start_mock_service()
```
In this example, we modified the following:
* Set a default `cassettes_dir`. When the wrapper is executed, it will be used instead of the default directory
* Added another command argument, to extend possible functionality
Running:
` ./tasks_worker/tests/cornell_wrapper.py --hello`
will result in:
`Hello from Cornell :)`
Running the same command without arguments will start Cornell with its default cassettes_dir.
================================================
FILE: docs/docs/workflows/subscribing_to_hooks.md
================================================
---
sidebar_position: 4
sidebar_label: Subscribing to Hooks
title: Subscribing to Hooks
---
During runtime, Cornell triggers [blinker signals](https://pythonhosted.org/blinker/) that
will allow you to modify or extend some of the out-of-the-box functionality. At this point,
the following is available:
* Replacing default logging service
* Modifying the listed cassette path (for example, if you prefer not to save your cassettes locally)
The list of signals can be found in [cornell/signals.py](https://github.com/hiredscorelabs/cornell/blob/master/cornell/signals.py#L11)
Example:
```python
from cornell.signals import logging_setup, process_cassette_file
@logging_setup.connect
def setup_logging_service(_):
return logging_service
@process_cassette_file.connect
def download_cassette_file(cassette_file_path):
storage = CornellCassettesStorage(logging_service)
return storage.download(cassette_file_path)
@click.command(cls=CornellCmdOptions)
def start_mock_service(**kwargs):
start_cornell(**kwargs)
if __name__ == "__main__":
start_mock_service()
```
In the above example:
* We're replacing the default logging service with our own.
* Every time Cornell requires a cassette file in runtime, we're downloading it from our dedicated storage.
**Note**: Additional signals can be easily added. Please feel free to open a [PR](https://github.com/hiredscorelabs/cornell) or an [Issue](https://github.com/hiredscorelabs/cornell/issues)!
================================================
FILE: docs/docusaurus.config.js
================================================
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').DocusaurusConfig} */
module.exports = {
title: 'Cornell',
tagline: 'Record & replay mock server',
url: 'https://hiredscorelabs.github.io/',
baseUrl: '/cornell/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
trailingSlash: true,
organizationName: 'Hiredscorelabs',
projectName: 'cornell',
themeConfig: {
navbar: {
title: 'Cornell',
logo: {
alt: 'Cornell Logo',
src: 'img/cornell.png',
},
items: [
{
type: 'doc',
docId: 'examples',
position: 'left',
label: 'Documentation',
}
],
},
footer: {
style: 'dark',
links: [
{
title: 'HiredScore',
items: [
{
label: 'Github',
to: 'https://github.com/hiredscorelabs/cornell',
},
{
label: 'HiredScore Website',
to: 'https://hiredscore.com',
},
{
label: 'HiredScore Blog',
to: 'https://blog.hiredscore.com/',
},
{
label: 'HiredScore Engineering Blog',
to: 'https://medium.com/hiredscore-engineering',
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} HiredScore, Inc. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
},
presets: [
[
'@docusaurus/preset-classic',
{
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
},
],
],
};
================================================
FILE: docs/package.json
================================================
{
"name": "docs-website",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "2.0.0-beta.3",
"@docusaurus/preset-classic": "2.0.0-beta.3",
"@mdx-js/react": "^1.6.21",
"@svgr/webpack": "^5.5.0",
"clsx": "^1.1.1",
"file-loader": "^6.2.0",
"prism-react-renderer": "^1.2.1",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"url-loader": "^4.1.1"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
================================================
FILE: docs/sidebars.js
================================================
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
module.exports = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{type: 'autogenerated', dirName: '.'}],
// But you can create a sidebar manually
/*
tutorialSidebar: [
{
type: 'category',
label: 'Tutorial',
items: ['hello'],
},
],
*/
};
================================================
FILE: docs/src/components/HomepageFeatures.js
================================================
import React from 'react';
import clsx from 'clsx';
import styles from './HomepageFeatures.module.css';
import JBCaptainImageUrl from '../../static/img/jb_captian.png';
import JBHeroImageUrl from '../../static/img/jb_hero.png';
import JBRockstarImageUrl from '../../static/img/jb_rockstar.png';
const FeatureList = [
{
title: 'Record API Interaction',
png: JBCaptainImageUrl,
description: (
<>
An HTTP proxy mode in which all interactions are recorded and saved into "cassettes" for future use.
</>
),
},
{
title: 'Replay for Testing',
png: JBHeroImageUrl,
description: (
<>
Use your library of "cassettes" to replay pre-recorded responses instead of hitting the real API endpoint.
Save time on testing cycles and avoid flakiness by using a consistent response.
</>
),
},
{
title: 'Extend Cornell',
png: JBRockstarImageUrl,
description: (
<>
Designed with flexibility in mind. Easily extend and customize the default behavior.
</>
),
},
];
function Feature({png, title, description}) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">
<img src={png} className={styles.featureSvg} alt={title}/>
</div>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures() {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}
================================================
FILE: docs/src/components/HomepageFeatures.module.css
================================================
/* stylelint-disable docusaurus/copyright-header */
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
}
================================================
FILE: docs/src/css/custom.css
================================================
/* stylelint-disable docusaurus/copyright-header */
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #25c2a0;
--ifm-color-primary-dark: rgb(33, 175, 144);
--ifm-color-primary-darker: rgb(31, 165, 136);
--ifm-color-primary-darkest: rgb(26, 136, 112);
--ifm-color-primary-light: rgb(70, 203, 174);
--ifm-color-primary-lighter: rgb(102, 212, 189);
--ifm-color-primary-lightest: rgb(146, 224, 208);
--ifm-code-font-size: 95%;
}
.docusaurus-highlight-code-line {
background-color: rgba(0, 0, 0, 0.1);
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);
}
html[data-theme='dark'] .docusaurus-highlight-code-line {
background-color: rgba(0, 0, 0, 0.3);
}
================================================
FILE: docs/src/pages/index.js
================================================
import React from 'react';
import clsx from 'clsx';
import Layout from '@theme/Layout';
import Link from '@docusaurus/Link';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import styles from './index.module.css';
import HomepageFeatures from '../components/HomepageFeatures';
function HomepageHeader() {
const {siteConfig} = useDocusaurusContext();
return (
<header className={clsx('hero hero--primary', styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div style={{display: 'flex', flexDirection:'row', justifyContent:'center', gap: '10px'}}>
<Link
className="button button--secondary button--lg header-github-link"
to="https://github.com/hiredscorelabs/cornell">
GITHUB
</Link>
<Link
className="button button--secondary button--lg header-github-link"
to="docs/examples">
DOCS
</Link>
</div>
</div>
</header>
);
}
export default function Home() {
const {siteConfig} = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Cornell: record & replay mock server">
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}
================================================
FILE: docs/src/pages/index.module.css
================================================
/* stylelint-disable docusaurus/copyright-header */
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 966px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}
================================================
FILE: docs/static/.nojekyll
================================================
================================================
FILE: setup.py
================================================
# pylint: ignore-errors
from pathlib import Path
from setuptools import setup, find_packages
readme = Path("README.md").read_text(encoding="utf-8")
exec(Path("cornell/_version.py").read_text(encoding="utf-8"))
VERSION = '.'.join(str(i) for i in __version__)
requirements = [
'flask>=2.0.0',
'yarl',
'requests',
'structlog',
'toolz',
'vcrpy~=5.0',
'xmltodict',
'click',
'blinker'
]
dev_requirements = {"dev" : [
'pylint>=2.6.0',
'requests-mock>=1.8.0',
'pytest>=6.2.2',
'pytest-cov~=2.12',
]}
setup(
name='cornell',
packages=find_packages(),
version=VERSION,
description="Cornell: record & replay mock server",
long_description=readme,
long_description_content_type='text/markdown',
url="https://hiredscorelabs.github.io/cornell/",
author="Yael Mintz",
author_email="yaelmi3@gmail.com",
license="MIT",
entry_points={'console_scripts': ['cornell = cornell.cornell_server:start_mock_service']},
install_requires=requirements,
extras_require=dev_requirements,
project_urls={
'Documentation': 'https://hiredscorelabs.github.io/cornell/',
'Source': 'https://github.com/hiredscorelabs/cornell',
},
)
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/conftest.py
================================================
from unittest.mock import MagicMock
import pytest
from cornell.cornell_server import app, _setup_app_config
from cornell.signals import process_cassette_file
TEST_URL = "http://some-address.com/some/path"
@pytest.fixture
def cornell_proxy(tmpdir, monkeypatch):
_setup_app_config(app=app, cassettes_dir=tmpdir, forward_uri=TEST_URL, record=True, record_once=True,
fixed_path=True, record_errors=False, additional_vcr_matchers=())
cornell_proxy = app.test_client()
monkeypatch.setattr(app.config.vcr, "use_cassette", MagicMock()) # pylint: disable=no-member
return cornell_proxy
@pytest.fixture(autouse=True)
def mock_requests(requests_mock):
mocked_requests_data = {"hi": "hi", "account_id/hi": "hi", "bye": "bye", "account_id/bye": "bye",
"parent/hi": "parent/hi", "hi?wsdl": "with_wsdl"}
for url_path, output in mocked_requests_data.items():
requests_mock.get(f"{TEST_URL}/{url_path}", text=output)
requests_mock.get(f"{TEST_URL}/not_found", status_code=400)
@pytest.fixture
def temp_yaml_file(tmpdir):
expected_file = tmpdir/"hi.yaml"
expected_file.write("0")
return expected_file
@process_cassette_file.connect
def download_cassette_file(cassette_file):
return cassette_file
================================================
FILE: tests/test_get_custom_vcr.py
================================================
from unittest.mock import MagicMock
import cornell.vcr_settings
from cornell.vcr_settings import get_custom_vcr
def test_get_custom_vcr(monkeypatch):
def custom_matcher():
return
vcr_mock = MagicMock()
monkeypatch.setattr(cornell.vcr_settings, "vcr", vcr_mock)
custom_vcr = get_custom_vcr(base_uri="base_uri", mock_uri="mock_uri", record_errors=True,
*[lambda x: x, custom_matcher])
vcr_mock.VCR.assert_called_with(decode_compressed_response=True)
assert sorted(custom_vcr.match_on) == sorted(('port', 'host', 'extended_vcr_body_matcher', 'scheme',
'extended_query_matcher', 'custom_matcher', '<lambda>', 'path',
'method'))
================================================
FILE: tests/test_odata_query_processing.py
================================================
import random
import copy
from unittest.mock import MagicMock
import pytest
from cornell.cornell_helpers import ODATA_EXPEND_FILTER
from cornell.custom_matchers import extended_query_matcher
@pytest.fixture
def odata_query():
return {'$expand':
'veteranStatus/picklistLabels,QueryQuestionResponse,item/state/picklistLabels,'
'resume,workAuth/picklistLabels,referralSource/picklistLabels,item/education/degreeNav/picklistLabels,'
'education,item/mobility/willingnessNav/picklistLabels,item/outsideWorkExperience,Requisition,AppStatus,'
'QueryStatusAuditTrail/AppStatus,highestEducation/picklistLabels,coverLetter,race/picklistLabels,'
'item/ReqFwditems,item/mobility/locationNav/picklistLabels,supportingDoc',
'$filter': "QueryId eq '666'",
'$format': 'json',
'$inlinecount': 'allpages'}
def test_odata_expand_identical(odata_query):
cassette_request, received_request = MagicMock(), MagicMock()
received_odata_query = copy.deepcopy(odata_query)
query = odata_query[ODATA_EXPEND_FILTER].split(",")
received_odata_query[ODATA_EXPEND_FILTER] = ",".join(random.sample(query, len(query)))
cassette_request.query = list(received_odata_query.items())
received_request.query = list(odata_query.items())
extended_query_matcher(received_request, cassette_request)
def test_odata_expand_different_content(odata_query):
cassette_request, received_request = MagicMock(), MagicMock()
received_odata_query = copy.deepcopy(odata_query)
odata_query[ODATA_EXPEND_FILTER] += ",more,items"
cassette_request.query, received_request.query = list(received_odata_query.items()), list(odata_query.items())
with pytest.raises(AssertionError) as err:
extended_query_matcher(received_request, cassette_request)
err.match(f"Odata \\{ODATA_EXPEND_FILTER} don't match")
def test_odata_expand_different_query(odata_query):
cassette_request, received_request = MagicMock(), MagicMock()
received_odata_query = copy.deepcopy(odata_query)
odata_query["$filter"] = "different query"
cassette_request.query, received_request.query = list(received_odata_query.items()), list(odata_query.items())
with pytest.raises(AssertionError) as err:
extended_query_matcher(received_request, cassette_request)
err.match("OData queries don't match")
def test_odata_identical_without_expend(odata_query):
cassette_request, received_request = MagicMock(), MagicMock()
odata_query.pop(ODATA_EXPEND_FILTER)
cassette_request.query, received_request.query = list(odata_query.items()), list(odata_query.items())
extended_query_matcher(received_request, cassette_request)
def test_odata_different_without_expend(odata_query):
cassette_request, received_request = MagicMock(), MagicMock()
odata_query.pop(ODATA_EXPEND_FILTER)
received_odata_query = copy.deepcopy(odata_query)
odata_query["$format"] = "different format"
cassette_request.query, received_request.query = list(received_odata_query.items()), list(odata_query.items())
with pytest.raises(AssertionError) as err:
extended_query_matcher(received_request, cassette_request)
err.match("!=")
================================================
FILE: tests/test_record_and_replay.py
================================================
from pathlib import Path
import pytest
from werkzeug.exceptions import NotFound
def test_record_once_new(tmpdir, cornell_proxy, temp_yaml_file):
for _ in range(2):
cornell_proxy.get('hi')
cornell_proxy.application.config.vcr.use_cassette.assert_called_once_with(temp_yaml_file)
assert cornell_proxy.application.config.cassette_paths == {'root': {'/hi': temp_yaml_file.basename}}
cornell_proxy.get('bye')
assert cornell_proxy.application.config.vcr.use_cassette.call_count == 2
cornell_proxy.application.config.vcr.use_cassette.assert_called_with(tmpdir/"bye.yaml")
assert cornell_proxy.application.config.cassette_paths == {"root": {'/hi': temp_yaml_file.basename, '/bye': "bye.yaml"}}
@pytest.mark.parametrize("fixed_path, root_path, saved_path, cassette_path", ((True, "root", "/account_id/hi", "account_id_hi"),
(False, "account_id", "/hi", "account_id/hi")),
ids=("fixed_path", "not_fixed_path"))
def test_fixed_path(tmpdir, cornell_proxy, fixed_path, root_path, saved_path, cassette_path):
cornell_proxy.application.config.fixed_path = fixed_path
cornell_proxy.get('account_id/hi')
args, *_ = cornell_proxy.application.config.vcr.use_cassette.call_args
assert args == (str(tmpdir / f"{cassette_path}.yaml"),)
assert cornell_proxy.application.config.cassette_paths == {root_path: {saved_path: f"{Path(cassette_path).name}.yaml"}}
def test_record_all_scenarios(cornell_proxy, temp_yaml_file):
cornell_proxy.application.config.record_once = False
cornell_proxy.get('hi')
cornell_proxy.application.config.vcr.use_cassette.assert_called_once_with(temp_yaml_file)
cornell_proxy.get('hi')
assert cornell_proxy.application.config.vcr.use_cassette.call_count == 2
cornell_proxy.get('bye')
assert cornell_proxy.application.config.vcr.use_cassette.call_count == 3
def test_play_missing_cassette(cornell_proxy):
cornell_proxy.application.config.record = False
res = cornell_proxy.get('hi')
assert res.status_code == NotFound.code
cornell_proxy.application.config.vcr.use_cassette.assert_not_called()
def test_play_cassette_exists(cornell_proxy, temp_yaml_file):
cornell_proxy.application.config.record = False
cornell_proxy.application.config.cassette_paths = {"root": {'/hi': temp_yaml_file}}
cornell_proxy.get('hi')
cornell_proxy.application.config.vcr.use_cassette.assert_called_once_with(temp_yaml_file, record_mode='new_episodes')
================================================
FILE: tests/test_record_errors.py
================================================
import pytest
import yaml
from vcr.serializers import yamlserializer
from vcr.request import Request
from cornell.vcr_settings import CustomPersister
@pytest.mark.parametrize("record_errors", (True, False))
def test_record_errors_toggle(cornell_proxy, record_errors):
cornell_proxy.application.config.record_errors = record_errors
cornell_proxy.get("not_found")
assert (cornell_proxy.application.config.cassette_paths["root"] == {}) != record_errors
@pytest.mark.parametrize("record_errors", (True, False))
def test_record_errors_to_cassette(temp_yaml_file, record_errors):
custom_persister = CustomPersister()
cassette_request = Request(
**{'method': 'GET', 'uri': 'http://127.0.0.1:8080/test',
'body': None,
'headers': {'User-Agent': 'python-requests/2.26.0', 'Accept-Encoding': 'gzip, deflate',
'Accept': '*/*', 'Connection': 'keep-alive'}})
cassette_response = {'status': {'code': 401, 'message': 'Unauthorized'},
'headers': {'Server': ['http-kit'], 'Content-Length': ['19'],
'Date': ['Fri, 15 Oct 2021 10:41:06 GMT']},
'body': {'string': b'Unauthorized access'}}
cassette_request.uri = 'http://127.0.0.1:8080/test'
cassette_dict = {"requests": [cassette_request],
"responses": [cassette_response]}
CustomPersister.record_errors = record_errors
CustomPersister.base_uri = "http://127.0.0.1:8080"
CustomPersister.mock_url = "http://cornell"
custom_persister.save_cassette(temp_yaml_file, cassette_dict, yamlserializer)
output = open(temp_yaml_file, encoding="utf-8")
if record_errors:
assert yaml.safe_load(output)["interactions"] == [{"request": cassette_request._to_dict(), # pylint: disable=protected-access
"response": cassette_response}]
else:
assert output.read() == "0"
================================================
FILE: tests/test_request_path.py
================================================
import pytest
@pytest.mark.parametrize(("query_string", "expected"), [({"wsdl": ""}, "with_wsdl"), ({"no_wsdl": "no_wsdl"}, "hi")])
def test_wsdl(cornell_proxy, query_string, expected):
resp = cornell_proxy.get('hi', query_string=query_string)
assert resp.data.decode() == expected
================================================
FILE: tests/test_signals.py
================================================
import pytest
from cornell.signals import cornell_signals, SignalNotRegistered, signal_context, MultipleSignalSubscribers
def test_unregistered_signal():
with pytest.raises(SignalNotRegistered) as exep:
with signal_context("nope"):
pass
assert exep.match('Signal nope not registered')
def test_multiple_subscribers():
new_signal = cornell_signals.signal("new_signal")
@new_signal.connect
def one(): # pylint:disable=unused-variable
pass
@new_signal.connect
def two(): # pylint:disable=unused-variable
pass
with pytest.raises(MultipleSignalSubscribers) as exep:
with signal_context("new_signal"):
pass
assert exep.match("Only one subscriber allowed for new_signal")
def test_signal_with_result():
result_func = cornell_signals.signal("result_func")
@result_func.connect
def func_with_result(num): # pylint:disable=unused-variable
return num * num
with signal_context("result_func", 2) as result:
assert result == 4
================================================
FILE: tests/test_soap_processing.py
================================================
from unittest import mock
from unittest.mock import MagicMock
import xmltodict
from toolz import get_in
from cornell.cornell_helpers import replace_locations_in_xml, get_paths_in_nested_dict_by_condition, strip_soap_namespaces_from_body
from .conftest import TEST_URL
TEST_XML = '<SOAP-ENV:Envelope xmlns:SOAP-ENV=' \
'"http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:' \
'wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:ns0=' \
'"http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="urn:com.monday.report/HiredScore_Candidate_History" xmlns:' \
'cus="urn:com.monday/tenants/ochsner/data/custom"><wsse:Test location=' \
'"http://another/location/address"></wsse:Test><SOAP-ENV:Header><wsse:Security mustUnderstand="true">' \
'<wsse:UsernameToken><wsse:Username>ISU_HiredScore@ochsner</wsse:Username><wsse:Password>pass!' \
'</wsse:Password></wsse:UsernameToken></wsse:Security><wsse:Test location=' \
'"http://stam.com/address"></wsse:Test></SOAP-ENV:Header><ns0:Body><ns1:Execute_Report><ns1:' \
'Report_Parameters><ns1:Candidate_ID>CAN_504434</ns1:Candidate_ID></ns1:Report_Parameters>' \
'</ns1:Execute_Report></ns0:Body></SOAP-ENV:Envelope>'
def test_replace_locations_in_xml():
with mock.patch("cornell.cornell_helpers.request", MagicMock()) as flask_request_mock:
flask_request_mock.url = f"{TEST_URL}?wsdl"
updated_xml = replace_locations_in_xml(TEST_XML)
xml_dict = xmltodict.parse(updated_xml)
paths = get_paths_in_nested_dict_by_condition(xml_dict, lambda key, value: key == "@location")
for path in paths:
assert get_in(path, xml_dict) == TEST_URL
def test_strip_namespaces_from_xml():
xml_body = strip_soap_namespaces_from_body(TEST_XML)
assert xml_body == '<?xml version="1.0" encoding="utf-8"?>\n<Execute_Report><Report_Parameters>' \
'<Candidate_ID>CAN_504434</Candidate_ID></Report_Parameters></Execute_Report>'
def test_strip_namespaces_from_xml_no_namespaces():
no_namespaces_xml = '<Hey><Body><ID>666</ID></Body></Hey>'
assert strip_soap_namespaces_from_body(no_namespaces_xml) == no_namespaces_xml
================================================
FILE: tests/test_vcr_dir.py
================================================
NESTED_URL = '/parent/hi'
def test_dir_from_path(cornell_proxy):
cornell_proxy.application.config.fixed_path = False
res = cornell_proxy.get(NESTED_URL)
assert res.status_code == 200
assert res.get_data().decode() == 'hi'
assert cornell_proxy.application.config.cassette_paths == {"parent": {'/hi': 'hi.yaml'}}
def test_fixed_vcr_path(cornell_proxy):
res = cornell_proxy.get(NESTED_URL)
assert res.status_code == 200
assert res.get_data().decode() == "parent/hi"
assert cornell_proxy.application.config.cassette_paths == {"root": {NESTED_URL: 'parent_hi.yaml'}}
gitextract_5knty9pr/
├── .circleci/
│ └── config.yml
├── .coveragerc
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .pylintrc
├── .tool-versions
├── .travis.yml
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── cornell/
│ ├── __init__.py
│ ├── _version.py
│ ├── cornell_helpers.py
│ ├── cornell_server.py
│ ├── custom_matchers.py
│ ├── signals.py
│ └── vcr_settings.py
├── docs/
│ ├── .gitignore
│ ├── README.md
│ ├── babel.config.js
│ ├── docs/
│ │ ├── examples.md
│ │ └── workflows/
│ │ ├── _category_.json
│ │ ├── basic_workflow.md
│ │ ├── custom_matchers.md
│ │ ├── own_module.md
│ │ └── subscribing_to_hooks.md
│ ├── docusaurus.config.js
│ ├── package.json
│ ├── sidebars.js
│ ├── src/
│ │ ├── components/
│ │ │ ├── HomepageFeatures.js
│ │ │ └── HomepageFeatures.module.css
│ │ ├── css/
│ │ │ └── custom.css
│ │ └── pages/
│ │ ├── index.js
│ │ └── index.module.css
│ └── static/
│ └── .nojekyll
├── setup.py
└── tests/
├── __init__.py
├── conftest.py
├── test_get_custom_vcr.py
├── test_odata_query_processing.py
├── test_record_and_replay.py
├── test_record_errors.py
├── test_request_path.py
├── test_signals.py
├── test_soap_processing.py
└── test_vcr_dir.py
SYMBOL INDEX (78 symbols across 16 files)
FILE: cornell/cornell_helpers.py
function get_vcr_dir_from_request (line 16) | def get_vcr_dir_from_request():
function get_localhost_uri (line 20) | def get_localhost_uri():
function get_paths_in_nested_dict_by_condition (line 26) | def get_paths_in_nested_dict_by_condition(response_body, condition, path...
function update_nested_dict_value (line 49) | def update_nested_dict_value(original_dict, old_value, new_value):
function replace_locations_in_xml (line 61) | def replace_locations_in_xml(response_body):
function strip_soap_namespaces_from_body (line 79) | def strip_soap_namespaces_from_body(request_data):
function _get_xml_body_without_namespaces (line 103) | def _get_xml_body_without_namespaces(*, namespace_paths, processed_data,...
function xml_in_headers (line 120) | def xml_in_headers(entity):
function json_in_headers (line 124) | def json_in_headers(entity):
function expand_in_query (line 128) | def expand_in_query(entity):
function set_underlying_vcr_logging_level (line 137) | def set_underlying_vcr_logging_level(logging_level=logging.WARNING):
function saved_data_to_yaml (line 147) | def saved_data_to_yaml(data, yaml_path):
FILE: cornell/cornell_server.py
class CassetteFileMissing (line 36) | class CassetteFileMissing(NotFound):
function ping (line 41) | def ping():
function handle_requests (line 47) | def handle_requests(path):
function _build_initial_request (line 58) | def _build_initial_request(path):
function _process_response_body (line 68) | def _process_response_body(response):
function _processes_headers (line 75) | def _processes_headers(resp):
function _cassette_url_player_context (line 83) | def _cassette_url_player_context(url_path):
function _determine_cassette_path (line 120) | def _determine_cassette_path(cassette_file, vcr_dir):
function _obtain_player_context (line 128) | def _obtain_player_context(cassette_path):
function _generate_cassette_path (line 136) | def _generate_cassette_path(vcr_dir, url_path):
function _load_cassette_paths (line 142) | def _load_cassette_paths(parent_dir):
function _generate_index_yaml (line 155) | def _generate_index_yaml(parent_dir, cassette_paths):
function _get_vcr_dir (line 168) | def _get_vcr_dir(cassettes_dir):
function create_cassettes_dir (line 173) | def create_cassettes_dir(cassettes_dir):
function _setup_app_config (line 179) | def _setup_app_config(*, app, cassettes_dir, fixed_path, forward_uri, re...
function _get_logging_service (line 194) | def _get_logging_service():
class CornellCmdOptions (line 200) | class CornellCmdOptions(click.Command):
method __init__ (line 201) | def __init__(self, *args, **kwargs):
function start_mock_service (line 221) | def start_mock_service(cassettes_dir, fixed_path, record, record_once, f...
function start_cornell (line 231) | def start_cornell(*, cassettes_dir, forward_uri, host, port, record, rec...
FILE: cornell/custom_matchers.py
function requests_match_conditions (line 11) | def requests_match_conditions(*conditions):
function replace_matchers (line 23) | def replace_matchers(custom_matcher, *conditions):
function _vcr_odata_query_matcher (line 37) | def _vcr_odata_query_matcher(received_request, cassette_request):
function extended_query_matcher (line 51) | def extended_query_matcher(received_request, cassette_request):
function _vcr_xml_body_matcher (line 55) | def _vcr_xml_body_matcher(cassette_request, received_request):
function extended_vcr_body_matcher (line 62) | def extended_vcr_body_matcher(received_request, cassette_request):
FILE: cornell/signals.py
class MultipleSignalSubscribers (line 17) | class MultipleSignalSubscribers(Exception):
class SignalNotRegistered (line 21) | class SignalNotRegistered(Exception):
function on_cornell_exit (line 25) | def on_cornell_exit(app):
function signal_context (line 34) | def signal_context(signal_name, *args, **kwargs):
FILE: cornell/vcr_settings.py
class CustomPersister (line 18) | class CustomPersister(FilesystemPersister):
method save_cassette (line 24) | def save_cassette(cls, cassette_path, cassette_dict, serializer):
function get_custom_vcr (line 38) | def get_custom_vcr(*additional_vcr_matchers, base_uri, mock_uri, record_...
function _register_additional_matchers (line 51) | def _register_additional_matchers(custom_vcr, *additional_vcr_matchers):
function request_has_matches (line 60) | def request_has_matches(cassette_path, flask_request):
FILE: docs/src/components/HomepageFeatures.js
function Feature (line 41) | function Feature({png, title, description}) {
function HomepageFeatures (line 55) | function HomepageFeatures() {
FILE: docs/src/pages/index.js
function HomepageHeader (line 9) | function HomepageHeader() {
function Home (line 34) | function Home() {
FILE: tests/conftest.py
function cornell_proxy (line 10) | def cornell_proxy(tmpdir, monkeypatch):
function mock_requests (line 19) | def mock_requests(requests_mock):
function temp_yaml_file (line 28) | def temp_yaml_file(tmpdir):
function download_cassette_file (line 35) | def download_cassette_file(cassette_file):
FILE: tests/test_get_custom_vcr.py
function test_get_custom_vcr (line 7) | def test_get_custom_vcr(monkeypatch):
FILE: tests/test_odata_query_processing.py
function odata_query (line 11) | def odata_query():
function test_odata_expand_identical (line 23) | def test_odata_expand_identical(odata_query):
function test_odata_expand_different_content (line 33) | def test_odata_expand_different_content(odata_query):
function test_odata_expand_different_query (line 43) | def test_odata_expand_different_query(odata_query):
function test_odata_identical_without_expend (line 53) | def test_odata_identical_without_expend(odata_query):
function test_odata_different_without_expend (line 60) | def test_odata_different_without_expend(odata_query):
FILE: tests/test_record_and_replay.py
function test_record_once_new (line 7) | def test_record_once_new(tmpdir, cornell_proxy, temp_yaml_file):
function test_fixed_path (line 21) | def test_fixed_path(tmpdir, cornell_proxy, fixed_path, root_path, saved_...
function test_record_all_scenarios (line 29) | def test_record_all_scenarios(cornell_proxy, temp_yaml_file):
function test_play_missing_cassette (line 39) | def test_play_missing_cassette(cornell_proxy):
function test_play_cassette_exists (line 46) | def test_play_cassette_exists(cornell_proxy, temp_yaml_file):
FILE: tests/test_record_errors.py
function test_record_errors_toggle (line 9) | def test_record_errors_toggle(cornell_proxy, record_errors):
function test_record_errors_to_cassette (line 16) | def test_record_errors_to_cassette(temp_yaml_file, record_errors):
FILE: tests/test_request_path.py
function test_wsdl (line 5) | def test_wsdl(cornell_proxy, query_string, expected):
FILE: tests/test_signals.py
function test_unregistered_signal (line 5) | def test_unregistered_signal():
function test_multiple_subscribers (line 12) | def test_multiple_subscribers():
function test_signal_with_result (line 30) | def test_signal_with_result():
FILE: tests/test_soap_processing.py
function test_replace_locations_in_xml (line 23) | def test_replace_locations_in_xml():
function test_strip_namespaces_from_xml (line 33) | def test_strip_namespaces_from_xml():
function test_strip_namespaces_from_xml_no_namespaces (line 39) | def test_strip_namespaces_from_xml_no_namespaces():
FILE: tests/test_vcr_dir.py
function test_dir_from_path (line 4) | def test_dir_from_path(cornell_proxy):
function test_fixed_vcr_path (line 12) | def test_fixed_vcr_path(cornell_proxy):
Condensed preview — 47 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (73K chars).
[
{
"path": ".circleci/config.yml",
"chars": 799,
"preview": "version: 2.1\nworkflows:\n build_and_deploy:\n jobs:\n - test-flow:\n filters:\n tags:\n "
},
{
"path": ".coveragerc",
"chars": 76,
"preview": "[run]\nsource = cornell\n\n[report]\nomit = cornell/_version.py\n\nfail_under = 70"
},
{
"path": ".github/workflows/main.yml",
"chars": 619,
"preview": "name: Cornell CI\n\non: [push]\n\njobs:\n build:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n python-versi"
},
{
"path": ".gitignore",
"chars": 1814,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": ".pylintrc",
"chars": 119,
"preview": "[MESSAGES CONTROL]\ndisable=R,missing-docstring,redefined-outer-name\n\n[FORMAT]\nmax-line-length=140\n\n[REPORTS]\nreports=no"
},
{
"path": ".tool-versions",
"chars": 14,
"preview": "python 3.11.5\n"
},
{
"path": ".travis.yml",
"chars": 279,
"preview": "language: python\npython:\n - \"3.7.11\"\n - \"3.8\"\n - \"3.9\"\ninstall:\n - make configure\nscript:\n - make test\n\njobs:\n inc"
},
{
"path": "Dockerfile",
"chars": 75,
"preview": "FROM python:3\n\nRUN pip install cornell\n\nEXPOSE 9000\n\nENTRYPOINT [\"cornell\"]"
},
{
"path": "LICENSE",
"chars": 1072,
"preview": "MIT License\n\nCopyright (c) 2021 HiredScore inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a"
},
{
"path": "Makefile",
"chars": 639,
"preview": "test:\n\tpylint --rcfile=.pylintrc cornell tests\n\tpytest --cov=cornell tests\n\nconfigure:\n\tpip install -e .'[dev]'\n\ndocker_"
},
{
"path": "README.md",
"chars": 7074,
"preview": "# Cornell: record & replay mock server\n\n["
},
{
"path": "cornell/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "cornell/_version.py",
"chars": 95,
"preview": "__version__ = (1, 1, 0)\n\nif __name__ == \"__main__\":\n print('.'.join(map(str, __version__)))\n"
},
{
"path": "cornell/cornell_helpers.py",
"chars": 4799,
"preview": "import logging\nfrom collections.abc import Iterable\nfrom contextlib import contextmanager\nfrom urllib.parse import urlpa"
},
{
"path": "cornell/cornell_server.py",
"chars": 11378,
"preview": "#!/usr/bin/env python\n# pylint: disable=no-member\nimport atexit\nimport logging\nfrom collections import defaultdict\nfrom "
},
{
"path": "cornell/custom_matchers.py",
"chars": 2846,
"preview": "import functools\n\nfrom vcr.matchers import query, body\nfrom vcr.util import read_body\n\nfrom cornell.cornell_helpers impo"
},
{
"path": "cornell/signals.py",
"chars": 1324,
"preview": "from contextlib import contextmanager\n\nimport yaml\n\nfrom blinker import Namespace\nfrom cornell.cornell_helpers import sa"
},
{
"path": "cornell/vcr_settings.py",
"chars": 3227,
"preview": "from http import HTTPStatus\n\nimport vcr\nfrom flask import request\nfrom toolz import get_in\nfrom vcr.cassette import Cass"
},
{
"path": "docs/.gitignore",
"chars": 233,
"preview": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.lo"
},
{
"path": "docs/README.md",
"chars": 743,
"preview": "# Website\n\nThis website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.\n\n## In"
},
{
"path": "docs/babel.config.js",
"chars": 89,
"preview": "module.exports = {\n presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
},
{
"path": "docs/docs/examples.md",
"chars": 133,
"preview": "---\nsidebar_position: 1\nsidebar_label: System in Action\ntitle: System in Action\n---\n\n;\nconst darkCodeTheme = require('prism-react-rendere"
},
{
"path": "docs/package.json",
"chars": 1015,
"preview": "{\n \"name\": \"docs-website\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"scripts\": {\n \"docusaurus\": \"docusaurus\",\n "
},
{
"path": "docs/sidebars.js",
"chars": 613,
"preview": "/**\n * Creating a sidebar enables you to:\n - create an ordered group of docs\n - render a sidebar for each doc of that gr"
},
{
"path": "docs/src/components/HomepageFeatures.js",
"chars": 1752,
"preview": "import React from 'react';\nimport clsx from 'clsx';\nimport styles from './HomepageFeatures.module.css';\n\nimport JBCaptai"
},
{
"path": "docs/src/components/HomepageFeatures.module.css",
"chars": 175,
"preview": "/* stylelint-disable docusaurus/copyright-header */\n\n.features {\n display: flex;\n align-items: center;\n padding: 2rem"
},
{
"path": "docs/src/css/custom.css",
"chars": 940,
"preview": "/* stylelint-disable docusaurus/copyright-header */\n/**\n * Any CSS included here will be global. The classic template\n *"
},
{
"path": "docs/src/pages/index.js",
"chars": 1427,
"preview": "import React from 'react';\nimport clsx from 'clsx';\nimport Layout from '@theme/Layout';\nimport Link from '@docusaurus/Li"
},
{
"path": "docs/src/pages/index.module.css",
"chars": 418,
"preview": "/* stylelint-disable docusaurus/copyright-header */\n\n/**\n * CSS files with the .module.css suffix will be treated as CSS"
},
{
"path": "docs/static/.nojekyll",
"chars": 0,
"preview": ""
},
{
"path": "setup.py",
"chars": 1225,
"preview": "# pylint: ignore-errors\nfrom pathlib import Path\nfrom setuptools import setup, find_packages\n\nreadme = Path(\"README.md\")"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/conftest.py",
"chars": 1289,
"preview": "from unittest.mock import MagicMock\nimport pytest\nfrom cornell.cornell_server import app, _setup_app_config\nfrom cornell"
},
{
"path": "tests/test_get_custom_vcr.py",
"chars": 792,
"preview": "from unittest.mock import MagicMock\n\nimport cornell.vcr_settings\nfrom cornell.vcr_settings import get_custom_vcr\n\n\ndef t"
},
{
"path": "tests/test_odata_query_processing.py",
"chars": 3265,
"preview": "import random\nimport copy\nfrom unittest.mock import MagicMock\nimport pytest\n\nfrom cornell.cornell_helpers import ODATA_E"
},
{
"path": "tests/test_record_and_replay.py",
"chars": 2556,
"preview": "from pathlib import Path\n\nimport pytest\nfrom werkzeug.exceptions import NotFound\n\n\ndef test_record_once_new(tmpdir, corn"
},
{
"path": "tests/test_record_errors.py",
"chars": 1982,
"preview": "import pytest\nimport yaml\nfrom vcr.serializers import yamlserializer\nfrom vcr.request import Request\nfrom cornell.vcr_se"
},
{
"path": "tests/test_request_path.py",
"chars": 292,
"preview": "import pytest\n\n\n@pytest.mark.parametrize((\"query_string\", \"expected\"), [({\"wsdl\": \"\"}, \"with_wsdl\"), ({\"no_wsdl\": \"no_ws"
},
{
"path": "tests/test_signals.py",
"chars": 1052,
"preview": "import pytest\nfrom cornell.signals import cornell_signals, SignalNotRegistered, signal_context, MultipleSignalSubscriber"
},
{
"path": "tests/test_soap_processing.py",
"chars": 2319,
"preview": "from unittest import mock\nfrom unittest.mock import MagicMock\n\nimport xmltodict\nfrom toolz import get_in\n\nfrom cornell.c"
},
{
"path": "tests/test_vcr_dir.py",
"chars": 602,
"preview": "NESTED_URL = '/parent/hi'\n\n\ndef test_dir_from_path(cornell_proxy):\n cornell_proxy.application.config.fixed_path = Fal"
}
]
About this extraction
This page contains the full source code of the hiredscorelabs/cornell GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 47 files (66.2 KB), approximately 17.3k tokens, and a symbol index with 78 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.