Full Code of hiredscorelabs/cornell for AI

master e9f49fef2dbb cached
47 files
66.2 KB
17.3k tokens
78 symbols
1 requests
Download .txt
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

[![Build Status](https://travis-ci.com/hiredscorelabs/cornell.svg?branch=master)](https://app.travis-ci.com/github/hiredscorelabs/cornell)
[![Python Version](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9-blue)](https://www.python.org/downloads/release/python-390/)
[![Docker Hub](https://img.shields.io/docker/pulls/hiredscorelabs/cornell.svg)](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

![System in test](https://imgur.com/OlDNTiD.jpg) 

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.

![System in test](https://imgur.com/ZXTFgaP.jpg) 


## 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/`

![Cornell demo](https://imgur.com/ky5NBPf.gif)

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

![Browser](https://imgur.com/GMgF6Cx.gif)

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:

![Cassette dir](https://imgur.com/cZExEpu.gif)


__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:

![Index file](https://imgur.com/IYjiJx6.gif)

### 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
---

![Cornell demo](https://imgur.com/ky5NBPf.gif)



================================================
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/`

![Cornell demo](https://imgur.com/ky5NBPf.gif)

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

![Browser](https://imgur.com/GMgF6Cx.gif)

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:

![Cassette dir](https://imgur.com/cZExEpu.gif)


__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:

![Index file](https://imgur.com/IYjiJx6.gif)


================================================
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'}}
Download .txt
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
Download .txt
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[![Build Status](https://travis-ci.com/hiredscorelabs/cornell.svg?branch=master)"
  },
  {
    "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![Cornell demo](https://imgur.com/k"
  },
  {
    "path": "docs/docs/workflows/_category_.json",
    "chars": 44,
    "preview": "{\n  \"label\": \"Workflows\",\n  \"position\": 2\n}\n"
  },
  {
    "path": "docs/docs/workflows/basic_workflow.md",
    "chars": 1949,
    "preview": "---\nsidebar_position: 1\nsidebar_label: Basic Workflow\ntitle: Basic Workflow\n---\n\nStaring Cornell in record mode:\n\n```\nco"
  },
  {
    "path": "docs/docs/workflows/custom_matchers.md",
    "chars": 1862,
    "preview": "---\nsidebar_position: 3\nsidebar_label: Adding Custom Matchers\ntitle: Adding Custom Matchers\n---\n\nIn some cases you'd wan"
  },
  {
    "path": "docs/docs/workflows/own_module.md",
    "chars": 1489,
    "preview": "---\nsidebar_position: 2\nsidebar_label: Starting Cornell from Your Own Module\ntitle: Starting Cornell from Your Own Modul"
  },
  {
    "path": "docs/docs/workflows/subscribing_to_hooks.md",
    "chars": 1474,
    "preview": "---\nsidebar_position: 4\nsidebar_label: Subscribing to Hooks\ntitle: Subscribing to Hooks\n---\n\nDuring runtime, Cornell tri"
  },
  {
    "path": "docs/docusaurus.config.js",
    "chars": 1845,
    "preview": "const lightCodeTheme = require('prism-react-renderer/themes/github');\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.

Copied to clipboard!