# Assertical (assertical)
Assertical is a library for helping write (async) integration/unit tests for fastapi/postgres/other projects. It has been developed by the Battery Storage and Grid Integration Program (BSGIP) at the Australian National University (https://bsgip.com/) for use with a variety of our internal libraries/packages.
It's attempting to be lightweight and modular, if you're not using `pandas` then just don't import the pandas asserts.
Contributions/PR's are welcome
## Example Usage
### Generating Class Instances
Say you have an SQLAlchemy model (the below also supports dataclasses, pydantic models and any type that expose its properties/types at runtime)
```
class Student(DeclarativeBase):
student_id: Mapped[int] = mapped_column(INTEGER, primary_key=True)
date_of_birth: Mapped[datetime] = mapped_column(DateTime)
name_full: Mapped[str] = mapped_column(VARCHAR(128))
name_preferred: Mapped[Optional[str]] = mapped_column(VARCHAR(128), nullable=True)
height: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(7, 2), nullable=True)
weight: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(7, 2), nullable=True)
```
Instead of writing the following boilerplate in your tests:
```
def test_my_insert():
# Arrange
s1 = Student(student_id=1, date_of_birth=datetime(2014, 1, 25), name_full="Bobby Tables", name_preferred="Bob", height=Decimal("185.5"), weight=Decimal("85.2"))
s2 = Student(student_id=2, date_of_birth=datetime(2015, 9, 23), name_full="Carly Chairs", name_preferred="CC", height=Decimal("175.5"), weight=Decimal("65"))
# Act ...
```
It can be simplified to:
```
def test_my_insert():
# Arrange
s1 = generate_class_instance(Student, seed=1)
s2 = generate_class_instance(Student, seed=2)
# Act ...
```
Which will generate two instances of Student with every property being set with appropriately typed values and unique values. Eg s1/s2 will be proper `Student` instances with values like:
| field | s1 | s2 |
| ----- | -- | -- |
| student_id | 5 (int) | 6 (int) |
| date_of_birth | '2010-01-02T00:00:01Z' (datetime) | '2010-01-03T00:00:02Z' (datetime) |
| name_full | '3-str' (str) | '4-str' (str) |
| name_preferred | '4-str' (Decimal) | '5-str' (Decimal) |
| height | 2 (Decimal) | 3 (Decimal) |
| weight | 6 (Decimal) | 7 (Decimal) |
Passing property name/values via kwargs is also supported :
`generate_class_instance(Student, seed=1, height=Decimal("12.34"))` will generate a `Student` instance similar to `s1` above but where `height` is `Decimal("12.34")`
You can also control the behaviour of `Optional` properties - by default they will populate with the full type but using `generate_class_instance(Student, optional_is_none=True)` will generate a `Student` instance where `height`, `weight` and `name_preferred` are `None`.
Finally, say we add the following "child" class `TestResult`:
```
class TestResult(DeclarativeBase):
test_result_id = mapped_column(INTEGER, primary_key=True)
student_id: Mapped[int] = mapped_column(INTEGER)
class: Mapped[str] = mapped_column(VARCHAR(128))
grade: Mapped[str] = mapped_column(VARCHAR(8))
```
And assuming `Student` has a property `all_results: Mapped[list[TestResult]]`. `generate_class_instance(Student)` will NOT supply a value for `all_results`. But by setting `generate_class_instance(Student, generate_relationships=True)` the generation will recurse into any generatable / list of generatable type instances.
#### Registering New Types
By default a number of common types / base classes will be registered but these can be extended with:
`assertical.fake.generator.register_value_generator(t, gen)` allows you to register a function that can generate an instance of type t given an integer seed value. The following example registers `MyType` so that other classes can have a property `my_type: Optional[MyType]` and have the values generated according to the supplied generator function:
```
class MyType:
val: int
def __init__(self, val):
self.val = val
register_value_generator(MyType, lambda seed: MyType(seed))
```
`assertical.fake.generator.register_base_type(base_t, generate_instance, list_members)` allows you to register a base type so that instances of subclasses of this base type can be generated using `generate_class_instance`. For example, the following registers a more complex type:
```
class MyBaseType:
def __init__(self):
pass
class MyComplexType(MyBaseType):
id: int
name: str
def __init__(self, id, name):
super.__init__()
self.id = id
self.name = name
register_base_type(MyBaseType, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER)
```
**Note:** All registrations apply globally. If you plan on using tests that modify the registry in different ways, there is a fixture `assertical.fixtures.generator.generator_registry_snapshot` that provides a context manager that will preserve and reset the global registry between tests.
eg:
```
def test_function()
with generator_registry_snapshot():
register_value_generator(MyPrimitiveType, lambda seed: MyPrimitiveType(seed))
register_base_type(
MyBaseType,
DEFAULT_CLASS_INSTANCE_GENERATOR,
DEFAULT_MEMBER_FETCHER,
)
# Do test body
```
### Mocking HTTP AsyncClient
`MockedAsyncClient` is a duck typed equivalent to `from httpx import AsyncClient` that can be useful fo injecting into classes that depend on a AsyncClient implementation.
Example usage that injects a MockedAsyncClient that will always return a `HTTPStatus.NO_CONTENT` for all requests:
```
mock_async_client = MockedAsyncClient(Response(status_code=HTTPStatus.NO_CONTENT))
with mock.patch("my_package.my_module.AsyncClient") as mock_client:
# test body here
assert mock_client.call_count_by_method[HTTPMethod.GET] > 0
```
The constructor for `MockedAsyncClient` allows you to setup either constant or varying responses. Eg: by supplying a list of responses you can mock behaviour that changes over multiple requests.
Eg: This instance will raise an Exception, then return a HTTP 500 then a HTTP 200
```
MockedAsyncClient([
Exception("My mocked error that will be raised"),
Response(status_code=HTTPStatus.NO_CONTENT),
Response(status_code=HTTPStatus.OK),
])
```
Response behavior can also be also be specified per remote uri:
```
MockedAsyncClient({
"http://first.example.com/": [
Exception("My mocked error that will be raised"),
Response(status_code=HTTPStatus.NO_CONTENT),
Response(status_code=HTTPStatus.OK),
],
"http://second.example.com/": Response(status_code=HTTPStatus.NO_CONTENT),
})
```
### Environment Management
If you have tests that depend on environment variables, the `assertical.fixtures.environment` module has utilities to aid in snapshotting/restoring the state of the operating system environment variables.
Eg: This `environment_snapshot` context manager will snapshot the environment allowing a test to freely modify it and then reset everything to before the test run
```
import os
from assertical.fixtures.environment import environment_snapshot
def test_my_custom_test():
with environment_snapshot():
os.environ["MY_ENV"] = new_value
# Do test body
```
This can also be simplified by using a fixture:
```
@pytest.fixture
def preserved_environment():
with environment_snapshot():
yield
def test_my_custom_test_2(preserved_environment):
os.environ["MY_ENV"] = new_value
# Do test body
```
### Running Testing FastAPI Apps
FastAPI (or ASGI apps) can be loaded for integration testing in two ways with Assertical:
1. Creating a lightweight httpx.AsyncClient wrapper around the app instance
1. Running a full uvicorn instance
#### AsyncClient Wrapper
`assertical.fixtures.fastapi.start_app_with_client` will act as an async context manager that can wrap an ASGI app instance and yield a `httpx.AsyncClient` that will communicate directly with that app instance.
Eg: This fixture will start an app instance and tests can depend on it to start up a fresh app instance for every test
```
@pytest.fixture
async def custom_test_client():
app: FastApi = generate_app() # This is just a reference to a fully constructed instance of your FastApi app
async with start_app_with_client(app) as c:
yield c # c is an instance of httpx.AsyncClient
@pytest.mark.anyio
async def test_thing(custom_test_client: AsyncClient):
response = await custom_test_client.get("/my_endpoint")
assert response.status == 200
```
#### Full uvicorn instance
`assertical.fixtures.fastapi.start_uvicorn_server` will behave similar to the above `start_app_with_client` but it will start a full running instance of uvicorn that will tear down once the context manager is exited.
This can be useful if you need to not just test the ASGI behavior of the app, but also how it interacts with a "real" uvicorn instance. Perhaps your app has middleware playing around with the underlying starlette functionality?
Eg: This fixture will start an app instance (listening on a fixed address) and will return the base URI of that instance
```
@pytest.fixture
async def custom_test_uri():
app: FastApi = generate_app() # This is just a reference to a fully constructed instance of your FastApi app
async with start_uvicorn_server(app) as c:
yield c # c is uri like "http://127.0.0.1:12345"
@pytest.mark.anyio
async def test_thing(custom_test_uri: str):
client = AsyncClient()
response = await client.get(custom_test_uri + "/my_endpoint")
assert response.status == 200
```
### Assertion utilities
#### Generator assertical.asserts.generator.*
This package isn't designed to be a collection of all possible asserts, other packages handle that. What is included are a few useful asserts around typing
`assertical.asserts.generator.assert_class_instance_equality()` will allow the comparison of two objects, property by property using a class/type definition as the source of compared properties. Using the above earlier `Student` example:
```
s1 = generate_class_instance(Student, seed=1)
s1_dup = generate_class_instance(Student, seed=1)
s2 = generate_class_instance(Student, seed=2)
# This will raise an assertion error saying that certain Student properties don't match
assert_class_instance_equality(Student, s1, s2)
# This will NOT raise an assertion as each property will be the same value/type
assert_class_instance_equality(Student, s1, s1_dup)
# This will compare on all Student properties EXCEPT 'student_id'
assert_class_instance_equality(Student, s1, s1_dup, ignored_properties=set(['student_id]))
```
#### Time assertical.asserts.time.*
contains some utilities for comparing times in different forms (eg timestamps, datetimes etc)
For example, the following asserts that a timestamp or datetime is "roughly now"
```
dt1 = datetime(2023, 11, 10, 1, 2, 0)
ts2 = datetime(2023, 11, 10, 1, 2, 3).timestamp() # 3 seconds difference
ts2 = datetime(2023, 11, 10, 1, 2, 3).timestamp() # 3 seconds difference
assert_fuzzy_datetime_match(dt1, ts2, fuzziness_seconds=5) # This will pass (difference is <5 seconds)
assert_fuzzy_datetime_match(dt1, ts2, fuzziness_seconds=2) # This will raise (difference is >2 seconds)
```
#### Type collections assertical.asserts.type.*
`assertical.asserts.type` contains some utilities for asserting collections of types are properly formed.
For example, the following asserts that an instance is a list type, that only contains Student elements and that there are 5 total items.
```
my_custom_list = []
assert_list_type(Student, my_custom_list, count=5)
```
#### Pandas assertical.asserts.pandas.*
Contains a number of simple assertions for a dataframe for ensuring certain columns/rows exist
## Installation (for use)
`pip install assertical[all]`
## Installation (for dev)
`pip install -e .[all]`
## Modular Components
| **module** | **requires** |
| ---------- | ------------ |
| `asserts/generator` | `None`+ |
| `asserts/pandas` | `assertical[pandas]` |
| `fake/generator` | `None`+ |
| `fake/sqlalchemy` | `assertical[postgres]` |
| `fixtures/fastapi` | `assertical[fastapi]` |
| `fixtures/postgres` | `assertical[postgres]` |
+ No requirements are mandatory but additional types will be supported if `assertical[pydantic]`, `assertical[postgres]`, `assertical[xml]` are installed
All other types just require just the base `pip install assertical`
## Editors
### vscode
The file `vscode/settings.json` is an example configuration for vscode. To use these setting copy this file to `.vscode/settings,json`
The main features of this settings file are:
- Enabling flake8 and disabling pylint
- Autoformat on save (using the black and isort formatters)
Settings that you may want to change:
- Set the python path to your python in your venv with `python.defaultInterpreterPath`.
- Enable mypy by setting `python.linting.mypyEnabled` to true in settings.json.
Raw data
{
"_id": null,
"home_page": null,
"name": "assertical",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": null,
"keywords": "test, fastapi, postgres, sqlalchemy",
"author": "Battery Storage and Grid Integration Program",
"author_email": null,
"download_url": "https://files.pythonhosted.org/packages/50/60/10edaf4a401ba8451d95efa94aa50dbb3c3c49c4f37d2d74add636d22280/assertical-0.2.1.tar.gz",
"platform": null,
"description": "# Assertical (assertical)\n\nAssertical is a library for helping write (async) integration/unit tests for fastapi/postgres/other projects. It has been developed by the Battery Storage and Grid Integration Program (BSGIP) at the Australian National University (https://bsgip.com/) for use with a variety of our internal libraries/packages.\n\nIt's attempting to be lightweight and modular, if you're not using `pandas` then just don't import the pandas asserts.\n\nContributions/PR's are welcome\n\n## Example Usage\n\n### Generating Class Instances\n\nSay you have an SQLAlchemy model (the below also supports dataclasses, pydantic models and any type that expose its properties/types at runtime) \n```\nclass Student(DeclarativeBase):\n student_id: Mapped[int] = mapped_column(INTEGER, primary_key=True)\n date_of_birth: Mapped[datetime] = mapped_column(DateTime)\n name_full: Mapped[str] = mapped_column(VARCHAR(128))\n name_preferred: Mapped[Optional[str]] = mapped_column(VARCHAR(128), nullable=True)\n height: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(7, 2), nullable=True)\n weight: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(7, 2), nullable=True)\n```\nInstead of writing the following boilerplate in your tests:\n\n```\ndef test_my_insert():\n # Arrange\n s1 = Student(student_id=1, date_of_birth=datetime(2014, 1, 25), name_full=\"Bobby Tables\", name_preferred=\"Bob\", height=Decimal(\"185.5\"), weight=Decimal(\"85.2\"))\n s2 = Student(student_id=2, date_of_birth=datetime(2015, 9, 23), name_full=\"Carly Chairs\", name_preferred=\"CC\", height=Decimal(\"175.5\"), weight=Decimal(\"65\"))\n # Act ... \n```\n\nIt can be simplified to:\n\n```\ndef test_my_insert():\n # Arrange\n s1 = generate_class_instance(Student, seed=1)\n s2 = generate_class_instance(Student, seed=2)\n # Act ... \n```\n\nWhich will generate two instances of Student with every property being set with appropriately typed values and unique values. Eg s1/s2 will be proper `Student` instances with values like:\n\n| field | s1 | s2 |\n| ----- | -- | -- |\n| student_id | 5 (int) | 6 (int) |\n| date_of_birth | '2010-01-02T00:00:01Z' (datetime) | '2010-01-03T00:00:02Z' (datetime) |\n| name_full | '3-str' (str) | '4-str' (str) |\n| name_preferred | '4-str' (Decimal) | '5-str' (Decimal) |\n| height | 2 (Decimal) | 3 (Decimal) |\n| weight | 6 (Decimal) | 7 (Decimal) |\n\nPassing property name/values via kwargs is also supported :\n\n`generate_class_instance(Student, seed=1, height=Decimal(\"12.34\"))` will generate a `Student` instance similar to `s1` above but where `height` is `Decimal(\"12.34\")`\n\nYou can also control the behaviour of `Optional` properties - by default they will populate with the full type but using `generate_class_instance(Student, optional_is_none=True)` will generate a `Student` instance where `height`, `weight` and `name_preferred` are `None`.\n\nFinally, say we add the following \"child\" class `TestResult`:\n\n```\nclass TestResult(DeclarativeBase):\n test_result_id = mapped_column(INTEGER, primary_key=True)\n student_id: Mapped[int] = mapped_column(INTEGER)\n class: Mapped[str] = mapped_column(VARCHAR(128))\n grade: Mapped[str] = mapped_column(VARCHAR(8))\n```\n\nAnd assuming `Student` has a property `all_results: Mapped[list[TestResult]]`. `generate_class_instance(Student)` will NOT supply a value for `all_results`. But by setting `generate_class_instance(Student, generate_relationships=True)` the generation will recurse into any generatable / list of generatable type instances.\n\n\n#### Registering New Types\n\nBy default a number of common types / base classes will be registered but these can be extended with:\n\n`assertical.fake.generator.register_value_generator(t, gen)` allows you to register a function that can generate an instance of type t given an integer seed value. The following example registers `MyType` so that other classes can have a property `my_type: Optional[MyType]` and have the values generated according to the supplied generator function:\n\n```\nclass MyType:\n val: int\n def __init__(self, val):\n self.val = val\n\nregister_value_generator(MyType, lambda seed: MyType(seed))\n```\n\n`assertical.fake.generator.register_base_type(base_t, generate_instance, list_members)` allows you to register a base type so that instances of subclasses of this base type can be generated using `generate_class_instance`. For example, the following registers a more complex type:\n\n```\nclass MyBaseType:\n def __init__(self):\n pass\n\nclass MyComplexType(MyBaseType):\n id: int\n name: str\n def __init__(self, id, name):\n super.__init__()\n self.id = id\n self.name = name\n\n\nregister_base_type(MyBaseType, DEFAULT_CLASS_INSTANCE_GENERATOR, DEFAULT_MEMBER_FETCHER)\n```\n\n**Note:** All registrations apply globally. If you plan on using tests that modify the registry in different ways, there is a fixture `assertical.fixtures.generator.generator_registry_snapshot` that provides a context manager that will preserve and reset the global registry between tests.\n\neg:\n\n```\ndef test_function()\n with generator_registry_snapshot():\n register_value_generator(MyPrimitiveType, lambda seed: MyPrimitiveType(seed))\n register_base_type(\n MyBaseType,\n DEFAULT_CLASS_INSTANCE_GENERATOR,\n DEFAULT_MEMBER_FETCHER,\n )\n\n # Do test body\n```\n\n### Mocking HTTP AsyncClient\n\n`MockedAsyncClient` is a duck typed equivalent to `from httpx import AsyncClient` that can be useful fo injecting into classes that depend on a AsyncClient implementation. \n\nExample usage that injects a MockedAsyncClient that will always return a `HTTPStatus.NO_CONTENT` for all requests:\n```\nmock_async_client = MockedAsyncClient(Response(status_code=HTTPStatus.NO_CONTENT))\nwith mock.patch(\"my_package.my_module.AsyncClient\") as mock_client:\n # test body here\n assert mock_client.call_count_by_method[HTTPMethod.GET] > 0\n```\nThe constructor for `MockedAsyncClient` allows you to setup either constant or varying responses. Eg: by supplying a list of responses you can mock behaviour that changes over multiple requests. \n\nEg: This instance will raise an Exception, then return a HTTP 500 then a HTTP 200\n```\nMockedAsyncClient([\n Exception(\"My mocked error that will be raised\"),\n Response(status_code=HTTPStatus.NO_CONTENT),\n Response(status_code=HTTPStatus.OK),\n])\n```\nResponse behavior can also be also be specified per remote uri:\n\n```\nMockedAsyncClient({\n \"http://first.example.com/\": [\n Exception(\"My mocked error that will be raised\"),\n Response(status_code=HTTPStatus.NO_CONTENT),\n Response(status_code=HTTPStatus.OK),\n ],\n \"http://second.example.com/\": Response(status_code=HTTPStatus.NO_CONTENT),\n})\n```\n\n### Environment Management\n\nIf you have tests that depend on environment variables, the `assertical.fixtures.environment` module has utilities to aid in snapshotting/restoring the state of the operating system environment variables.\n\nEg: This `environment_snapshot` context manager will snapshot the environment allowing a test to freely modify it and then reset everything to before the test run\n```\nimport os\nfrom assertical.fixtures.environment import environment_snapshot\n\ndef test_my_custom_test():\n with environment_snapshot():\n os.environ[\"MY_ENV\"] = new_value\n # Do test body\n```\n\nThis can also be simplified by using a fixture:\n```\n@pytest.fixture\ndef preserved_environment():\n with environment_snapshot():\n yield\n\ndef test_my_custom_test_2(preserved_environment):\n os.environ[\"MY_ENV\"] = new_value\n # Do test body\n```\n\n### Running Testing FastAPI Apps\n\nFastAPI (or ASGI apps) can be loaded for integration testing in two ways with Assertical:\n1. Creating a lightweight httpx.AsyncClient wrapper around the app instance\n1. Running a full uvicorn instance\n\n#### AsyncClient Wrapper\n\n`assertical.fixtures.fastapi.start_app_with_client` will act as an async context manager that can wrap an ASGI app instance and yield a `httpx.AsyncClient` that will communicate directly with that app instance.\n\nEg: This fixture will start an app instance and tests can depend on it to start up a fresh app instance for every test\n```\n@pytest.fixture\nasync def custom_test_client():\n app: FastApi = generate_app() # This is just a reference to a fully constructed instance of your FastApi app \n async with start_app_with_client(app) as c:\n yield c # c is an instance of httpx.AsyncClient\n\n\n@pytest.mark.anyio\nasync def test_thing(custom_test_client: AsyncClient):\n response = await custom_test_client.get(\"/my_endpoint\")\n assert response.status == 200\n```\n\n#### Full uvicorn instance\n\n`assertical.fixtures.fastapi.start_uvicorn_server` will behave similar to the above `start_app_with_client` but it will start a full running instance of uvicorn that will tear down once the context manager is exited.\n\nThis can be useful if you need to not just test the ASGI behavior of the app, but also how it interacts with a \"real\" uvicorn instance. Perhaps your app has middleware playing around with the underlying starlette functionality?\n\nEg: This fixture will start an app instance (listening on a fixed address) and will return the base URI of that instance\n```\n@pytest.fixture\nasync def custom_test_uri():\n app: FastApi = generate_app() # This is just a reference to a fully constructed instance of your FastApi app \n async with start_uvicorn_server(app) as c:\n yield c # c is uri like \"http://127.0.0.1:12345\"\n\n\n@pytest.mark.anyio\nasync def test_thing(custom_test_uri: str):\n client = AsyncClient()\n response = await client.get(custom_test_uri + \"/my_endpoint\")\n assert response.status == 200\n```\n\n\n### Assertion utilities\n\n#### Generator assertical.asserts.generator.*\n\nThis package isn't designed to be a collection of all possible asserts, other packages handle that. What is included are a few useful asserts around typing\n\n`assertical.asserts.generator.assert_class_instance_equality()` will allow the comparison of two objects, property by property using a class/type definition as the source of compared properties. Using the above earlier `Student` example:\n\n```\ns1 = generate_class_instance(Student, seed=1)\ns1_dup = generate_class_instance(Student, seed=1)\ns2 = generate_class_instance(Student, seed=2)\n\n# This will raise an assertion error saying that certain Student properties don't match\nassert_class_instance_equality(Student, s1, s2)\n\n# This will NOT raise an assertion as each property will be the same value/type\nassert_class_instance_equality(Student, s1, s1_dup)\n\n\n# This will compare on all Student properties EXCEPT 'student_id'\nassert_class_instance_equality(Student, s1, s1_dup, ignored_properties=set(['student_id]))\n```\n\n#### Time assertical.asserts.time.* \n\ncontains some utilities for comparing times in different forms (eg timestamps, datetimes etc)\n\nFor example, the following asserts that a timestamp or datetime is \"roughly now\"\n```\ndt1 = datetime(2023, 11, 10, 1, 2, 0)\nts2 = datetime(2023, 11, 10, 1, 2, 3).timestamp() # 3 seconds difference\nts2 = datetime(2023, 11, 10, 1, 2, 3).timestamp() # 3 seconds difference\nassert_fuzzy_datetime_match(dt1, ts2, fuzziness_seconds=5) # This will pass (difference is <5 seconds)\nassert_fuzzy_datetime_match(dt1, ts2, fuzziness_seconds=2) # This will raise (difference is >2 seconds)\n```\n\n#### Type collections assertical.asserts.type.*\n\n`assertical.asserts.type` contains some utilities for asserting collections of types are properly formed. \n\nFor example, the following asserts that an instance is a list type, that only contains Student elements and that there are 5 total items.\n```\nmy_custom_list = []\nassert_list_type(Student, my_custom_list, count=5)\n```\n\n#### Pandas assertical.asserts.pandas.*\n\nContains a number of simple assertions for a dataframe for ensuring certain columns/rows exist\n\n## Installation (for use)\n\n`pip install assertical[all]`\n\n## Installation (for dev)\n\n`pip install -e .[all]`\n\n## Modular Components\n\n| **module** | **requires** |\n| ---------- | ------------ |\n| `asserts/generator` | `None`+ |\n| `asserts/pandas` | `assertical[pandas]` |\n| `fake/generator` | `None`+ |\n| `fake/sqlalchemy` | `assertical[postgres]` |\n| `fixtures/fastapi` | `assertical[fastapi]` |\n| `fixtures/postgres` | `assertical[postgres]` |\n\n+ No requirements are mandatory but additional types will be supported if `assertical[pydantic]`, `assertical[postgres]`, `assertical[xml]` are installed\n\nAll other types just require just the base `pip install assertical`\n\n## Editors\n\n\n### vscode\n\nThe file `vscode/settings.json` is an example configuration for vscode. To use these setting copy this file to `.vscode/settings,json`\n\nThe main features of this settings file are:\n - Enabling flake8 and disabling pylint\n - Autoformat on save (using the black and isort formatters)\n\nSettings that you may want to change:\n- Set the python path to your python in your venv with `python.defaultInterpreterPath`.\n- Enable mypy by setting `python.linting.mypyEnabled` to true in settings.json.\n\n\n",
"bugtrack_url": null,
"license": null,
"summary": "Assertical - a modular library for helping write (async) integration/unit tests for fastapi/sqlalchemy/postgres projects",
"version": "0.2.1",
"project_urls": {
"Homepage": "https://github.com/bsgip/assertical"
},
"split_keywords": [
"test",
" fastapi",
" postgres",
" sqlalchemy"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "6f1a89a2dc978eaf61e92953b1f2aad73a2773b2f46c8df82e6e2c4242d3515f",
"md5": "46f19d640f41a65807a7f32373fcfe31",
"sha256": "bfa4d4647067de3f0b957cd45b0f3fee04d128575736a8b53efdf066d0f597bd"
},
"downloads": -1,
"filename": "assertical-0.2.1-py3-none-any.whl",
"has_sig": false,
"md5_digest": "46f19d640f41a65807a7f32373fcfe31",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 26664,
"upload_time": "2024-12-03T05:34:28",
"upload_time_iso_8601": "2024-12-03T05:34:28.608152Z",
"url": "https://files.pythonhosted.org/packages/6f/1a/89a2dc978eaf61e92953b1f2aad73a2773b2f46c8df82e6e2c4242d3515f/assertical-0.2.1-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "506010edaf4a401ba8451d95efa94aa50dbb3c3c49c4f37d2d74add636d22280",
"md5": "1ff15084c0503ac4dfa757fee2fc981b",
"sha256": "80bb40e8d04b913b9e84b1ace5aa8460cfc77155a69a76f8c30db6b7e1180c3d"
},
"downloads": -1,
"filename": "assertical-0.2.1.tar.gz",
"has_sig": false,
"md5_digest": "1ff15084c0503ac4dfa757fee2fc981b",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 28785,
"upload_time": "2024-12-03T05:34:31",
"upload_time_iso_8601": "2024-12-03T05:34:31.315161Z",
"url": "https://files.pythonhosted.org/packages/50/60/10edaf4a401ba8451d95efa94aa50dbb3c3c49c4f37d2d74add636d22280/assertical-0.2.1.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-12-03 05:34:31",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "bsgip",
"github_project": "assertical",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"lcname": "assertical"
}