From f80cb45abeaa95b21050ab0c16276b6620b6d14c Mon Sep 17 00:00:00 2001 From: Lilith Wittmann Date: Sat, 27 Jan 2024 11:40:49 +0100 Subject: [PATCH 1/4] feat(ui): initial setup to run the webserver and launch the ui --- .gitignore | 2 + causy/cli.py | 17 +- causy/common_pipeline_steps/calculation.py | 4 +- causy/common_pipeline_steps/placeholder.py | 2 +- causy/graph_model.py | 64 ++--- causy/independence_tests/common.py | 22 +- causy/interfaces.py | 16 +- causy/orientation_rules/fci.py | 32 +-- causy/orientation_rules/pc.py | 68 +++--- causy/ui.py | 95 ++++++++ poetry.lock | 258 ++++++++++++++++++++- pyproject.toml | 2 + tests/test_independence_tests.py | 4 +- tests/test_orientation_rules.py | 4 +- tests/test_orientation_tests.py | 8 +- 15 files changed, 478 insertions(+), 120 deletions(-) create mode 100644 causy/ui.py diff --git a/.gitignore b/.gitignore index eff65a2..96c625e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __pycache__/ .DS_Store .coverage + +causy/static/* diff --git a/causy/cli.py b/causy/cli.py index 9bf1fc9..8d52ba7 100644 --- a/causy/cli.py +++ b/causy/cli.py @@ -5,6 +5,7 @@ import logging import typer +import uvicorn from causy.graph_model import graph_model_factory from causy.serialization import serialize_model @@ -12,6 +13,7 @@ load_pipeline_steps_by_definition, retrieve_edges, ) +from causy.ui import server app = typer.Typer() @@ -49,6 +51,16 @@ def eject(algorithm: str, output_file: str): file.write(json.dumps(result, indent=4)) +@app.command() +def ui(result_file: str): + result = load_json(result_file) + + server_config, server_runner = server(result) + typer.launch(f"http://{server_config.host}:{server_config.port}") + typer.echo(f"🚀 Starting server at http://{server_config.host}:{server_config.port}") + server_runner.run() + + @app.command() def execute( data_file: str, @@ -128,10 +140,5 @@ def execute( fig.savefig(render_save_file) -@app.command() -def visualize(output: str): - raise NotImplementedError() - - if __name__ == "__main__": app() diff --git a/causy/common_pipeline_steps/calculation.py b/causy/common_pipeline_steps/calculation.py index 12ab4a2..7176440 100644 --- a/causy/common_pipeline_steps/calculation.py +++ b/causy/common_pipeline_steps/calculation.py @@ -40,8 +40,8 @@ def test(self, nodes: Tuple[str], graph: BaseGraphInterface) -> TestResult: edge_value["correlation"] = pearson_correlation.item() return TestResult( - x=x, - y=y, + u=x, + v=y, action=TestResultAction.UPDATE_EDGE, data=edge_value, ) diff --git a/causy/common_pipeline_steps/placeholder.py b/causy/common_pipeline_steps/placeholder.py index 5e59f21..fba0716 100644 --- a/causy/common_pipeline_steps/placeholder.py +++ b/causy/common_pipeline_steps/placeholder.py @@ -26,4 +26,4 @@ def test( :return: """ logger.debug(f"PlaceholderTest {nodes}") - return TestResult(x=None, y=None, action=TestResultAction.DO_NOTHING, data={}) + return TestResult(u=None, v=None, action=TestResultAction.DO_NOTHING, data={}) diff --git a/causy/graph_model.py b/causy/graph_model.py index 0411e2c..1439beb 100644 --- a/causy/graph_model.py +++ b/causy/graph_model.py @@ -131,75 +131,75 @@ def _take_action(self, results): result_items = [result_items] for i in result_items: - if i.x is not None and i.y is not None: - logger.info(f"Action: {i.action} on {i.x.name} and {i.y.name}") + if i.u is not None and i.v is not None: + logger.info(f"Action: {i.action} on {i.u.name} and {i.v.name}") # execute the action returned by the test if i.action == TestResultAction.REMOVE_EDGE_UNDIRECTED: - if not self.graph.undirected_edge_exists(i.x, i.y): + if not self.graph.undirected_edge_exists(i.u, i.v): logger.debug( - f"Tried to remove undirected edge {i.x.name} <-> {i.y.name}. But it does not exist." + f"Tried to remove undirected edge {i.u.name} <-> {i.v.name}. But it does not exist." ) continue - self.graph.remove_edge(i.x, i.y) - self.graph.add_edge_history(i.x, i.y, i) - self.graph.add_edge_history(i.y, i.x, i) + self.graph.remove_edge(i.u, i.v) + self.graph.add_edge_history(i.u, i.v, i) + self.graph.add_edge_history(i.v, i.u, i) elif i.action == TestResultAction.UPDATE_EDGE: - if not self.graph.edge_exists(i.x, i.y): + if not self.graph.edge_exists(i.u, i.v): logger.debug( - f"Tried to update edge {i.x.name} -> {i.y.name}. But it does not exist." + f"Tried to update edge {i.u.name} -> {i.v.name}. But it does not exist." ) continue - self.graph.update_edge(i.x, i.y, metadata=i.data) - self.graph.add_edge_history(i.x, i.y, i) - self.graph.add_edge_history(i.y, i.x, i) + self.graph.update_edge(i.u, i.v, metadata=i.data) + self.graph.add_edge_history(i.u, i.v, i) + self.graph.add_edge_history(i.v, i.u, i) elif i.action == TestResultAction.UPDATE_EDGE_DIRECTED: - if not self.graph.directed_edge_exists(i.x, i.y): + if not self.graph.directed_edge_exists(i.u, i.v): logger.debug( - f"Tried to update directed edge {i.x.name} -> {i.y.name}. But it does not exist." + f"Tried to update directed edge {i.u.name} -> {i.v.name}. But it does not exist." ) continue - self.graph.update_directed_edge(i.x, i.y, i.data) - self.graph.add_edge_history(i.x, i.y, i) + self.graph.update_directed_edge(i.u, i.v, i.data) + self.graph.add_edge_history(i.u, i.v, i) elif i.action == TestResultAction.DO_NOTHING: continue elif i.action == TestResultAction.REMOVE_EDGE_DIRECTED: if not self.graph.directed_edge_exists( - i.x, i.y - ) and not self.graph.edge_exists(i.x, i.y): + i.u, i.v + ) and not self.graph.edge_exists(i.u, i.v): logger.debug( - f"Tried to remove directed edge {i.x.name} -> {i.y.name}. But it does not exist." + f"Tried to remove directed edge {i.u.name} -> {i.v.name}. But it does not exist." ) continue - self.graph.remove_directed_edge(i.x, i.y) + self.graph.remove_directed_edge(i.u, i.v) # TODO: move this to pre/post update hooks if self.graph.edge_exists( - i.y, i.x + i.v, i.u ): # if the edge is undirected, make it directed self.graph.update_directed_edge( - i.y, i.x, edge_type=EdgeType.DIRECTED + i.v, i.u, edge_type=EdgeType.DIRECTED ) - self.graph.add_edge_history(i.x, i.y, i) + self.graph.add_edge_history(i.u, i.v, i) elif i.action == TestResultAction.UPDATE_EDGE_TYPE: - if not self.graph.edge_exists(i.x, i.y): + if not self.graph.edge_exists(i.u, i.v): logger.debug( - f"Tried to update edge type {i.x.name} <-> {i.y.name}. But it does not exist." + f"Tried to update edge type {i.u.name} <-> {i.v.name}. But it does not exist." ) continue - self.graph.update_edge(i.x, i.y, edge_type=i.edge_type) - self.graph.add_edge_history(i.x, i.y, i) - self.graph.add_edge_history(i.y, i.x, i) + self.graph.update_edge(i.u, i.v, edge_type=i.edge_type) + self.graph.add_edge_history(i.u, i.v, i) + self.graph.add_edge_history(i.v, i.u, i) elif i.action == TestResultAction.UPDATE_EDGE_TYPE_DIRECTED: - if not self.graph.directed_edge_exists(i.x, i.y): + if not self.graph.directed_edge_exists(i.u, i.v): logger.debug( - f"Tried to update edge type {i.x.name} -> {i.y.name}. But it does not exist." + f"Tried to update edge type {i.u.name} -> {i.v.name}. But it does not exist." ) continue - self.graph.update_directed_edge(i.x, i.y, edge_type=i.edge_type) - self.graph.add_edge_history(i.x, i.y, i) + self.graph.update_directed_edge(i.u, i.v, edge_type=i.edge_type) + self.graph.add_edge_history(i.u, i.v, i) # add the action to the actions history actions_taken.append(i) diff --git a/causy/independence_tests/common.py b/causy/independence_tests/common.py index 48d7d46..4e6c8f1 100644 --- a/causy/independence_tests/common.py +++ b/causy/independence_tests/common.py @@ -28,14 +28,14 @@ class CorrelationCoefficientTest(PipelineStepInterface): def test(self, nodes: List[str], graph: BaseGraphInterface) -> Optional[TestResult]: """ - Test if x and y are independent and delete edge in graph if they are. + Test if u and v are independent and delete edge in graph if they are. :param nodes: list of nodes :return: A TestResult with the action to take """ x = graph.nodes[nodes[0]] y = graph.nodes[nodes[1]] - # make t test for independency of x and y + # make t test for independency of u and v sample_size = len(x.values) nb_of_control_vars = 0 corr = graph.edge_value(x, y)["correlation"] @@ -45,8 +45,8 @@ def test(self, nodes: List[str], graph: BaseGraphInterface) -> Optional[TestResu if abs(t) < critical_t: logger.debug(f"Nodes {x.name} and {y.name} are uncorrelated") return TestResult( - x=x, - y=y, + u=x, + v=y, action=TestResultAction.REMOVE_EDGE_UNDIRECTED, data={}, ) @@ -63,7 +63,7 @@ def test( self, nodes: Tuple[str], graph: BaseGraphInterface ) -> Optional[List[TestResult]]: """ - Test if nodes x,y are independent given node z based on a partial correlation test. + Test if nodes u,v are independent given node z based on a partial correlation test. We use this test for all combinations of 3 nodes because it is faster than the extended test (which supports combinations of n nodes). We can use it to remove edges between nodes which are not independent given another node and so reduce the number of combinations for the extended test. :param nodes: the nodes to test @@ -101,7 +101,7 @@ def test( par_corr = numerator / denominator - # make t test for independency of x and y given z + # make t test for independency of u and v given z sample_size = len(x.values) nb_of_control_vars = len(nodes) - 2 t, critical_t = get_t_and_critical_t( @@ -115,8 +115,8 @@ def test( results.append( TestResult( - x=x, - y=y, + u=x, + v=y, action=TestResultAction.REMOVE_EDGE_UNDIRECTED, data={"separatedBy": [z]}, ) @@ -134,7 +134,7 @@ class ExtendedPartialCorrelationTestMatrix(PipelineStepInterface): def test(self, nodes: List[str], graph: BaseGraphInterface) -> Optional[TestResult]: """ - Test if nodes x,y are independent given Z (set of nodes) based on partial correlation using the inverted covariance matrix (precision matrix). + Test if nodes u,v are independent given Z (set of nodes) based on partial correlation using the inverted covariance matrix (precision matrix). https://en.wikipedia.org/wiki/Partial_correlation#Using_matrix_inversion We use this test for all combinations of more than 3 nodes because it is slower. :param nodes: the nodes to test @@ -181,8 +181,8 @@ def test(self, nodes: List[str], graph: BaseGraphInterface) -> Optional[TestResu ) nodes_set = set([graph.nodes[n] for n in nodes]) return TestResult( - x=graph.nodes[nodes[0]], - y=graph.nodes[nodes[1]], + u=graph.nodes[nodes[0]], + v=graph.nodes[nodes[1]], action=TestResultAction.REMOVE_EDGE_UNDIRECTED, data={ "separatedBy": list( diff --git a/causy/interfaces.py b/causy/interfaces.py index 7816368..af83804 100644 --- a/causy/interfaces.py +++ b/causy/interfaces.py @@ -53,8 +53,6 @@ class EdgeInterface(SerializeMixin): def serialize(self): return { - "u": self.u.serialize(), - "v": self.v.serialize(), "edge_type": self.edge_type, "metadata": self.metadata, } @@ -76,15 +74,15 @@ class TestResultAction(enum.StrEnum): @dataclass class TestResult(SerializeMixin): - x: NodeInterface - y: NodeInterface + u: NodeInterface + v: NodeInterface action: TestResultAction data: Optional[Dict] = None def serialize(self): return { - "x": self.x.serialize(), - "y": self.y.serialize(), + "from": self.u.serialize(), + "to": self.v.serialize(), "action": self.action.name, } @@ -219,9 +217,9 @@ def __init__( @abstractmethod def test(self, nodes: List[str], graph: BaseGraphInterface) -> Optional[TestResult]: """ - Test if x and y are independent - :param x: x values - :param y: y values + Test if u and v are independent + :param u: u values + :param v: v values :return: True if independent, False otherwise """ pass diff --git a/causy/orientation_rules/fci.py b/causy/orientation_rules/fci.py index c6362cf..3d0e2b6 100644 --- a/causy/orientation_rules/fci.py +++ b/causy/orientation_rules/fci.py @@ -24,18 +24,18 @@ def test( Some notes on how we implment FCI: After the independence tests, we have a graph with undirected edges which are implemented as two directed edges, one in each direction. We initialize the graph by adding values to all these edges, in the beginning, they get the value "either directed or undirected". Then we perform the collider test. Unlike in PC, - we do not delete directed edges from z to x and from z to y in order to obtain the structure (x -> z <- y). Instead, we - delete the information "either directed or undirected" from the directed edges from x to z and from y to z. That means, - the directed edges from x to z and from y to z are now truly directed edges. The edges from z to x and from z to y can + we do not delete directed edges from z to u and from z to v in order to obtain the structure (u -> z <- v). Instead, we + delete the information "either directed or undirected" from the directed edges from u to z and from v to z. That means, + the directed edges from u to z and from v to z are now truly directed edges. The edges from z to u and from z to v can still stand for a directed edge or no directed edge. In the literature, this is portrayed by the meta symbol * and we - obtain x *-> z <-* y. There might be ways to implement these similar but still subtly different orientation rules more consistently. + obtain u *-> z <-* v. There might be ways to implement these similar but still subtly different orientation rules more consistently. TODO: write tests - We call triples x, y, z of nodes v structures if x and y that are NOT adjacent but share an adjacent node z. - V structures looks like this in the undirected skeleton: (x - z - y). - We now check if z is in the separating set. If so, the edges must be oriented from x to z and from y to z: - (x *-> z <-* y), where * indicated that there can be an arrowhead or none, we do not know, at least until + We call triples u, v, z of nodes v structures if u and v that are NOT adjacent but share an adjacent node z. + V structures looks like this in the undirected skeleton: (u - z - v). + We now check if z is in the separating set. If so, the edges must be oriented from u to z and from v to z: + (u *-> z <-* v), where * indicated that there can be an arrowhead or none, we do not know, at least until applying further rules. :param nodes: list of nodes :param graph: the current graph @@ -45,11 +45,11 @@ def test( x = graph.nodes[nodes[0]] y = graph.nodes[nodes[1]] - # if x and y are adjacent, do nothing + # if u and v are adjacent, do nothing if graph.undirected_edge_exists(x, y): - return TestResult(x=x, y=y, action=TestResultAction.DO_NOTHING, data={}) + return TestResult(u=x, v=y, action=TestResultAction.DO_NOTHING, data={}) - # if x and y are NOT adjacent, store all shared adjacent nodes + # if u and v are NOT adjacent, store all shared adjacent nodes potential_zs = set(graph.edges[x.id].keys()).intersection( set(graph.edges[y.id].keys()) ) @@ -58,7 +58,7 @@ def test( x, y, TestResultAction.REMOVE_EDGE_UNDIRECTED ) - # if x and y are not independent given z, safe action: make z a collider + # if u and v are not independent given z, safe action: make z a collider results = [] for z in potential_zs: z = graph.nodes[z] @@ -71,14 +71,14 @@ def test( if z.id not in separators: results += [ TestResult( - x=x, - y=z, + u=x, + v=z, action=TestResultAction.UPDATE_EDGE_DIRECTED, data={"edge_type": None}, ), TestResult( - x=y, - y=z, + u=y, + v=z, action=TestResultAction.UPDATE_EDGE_DIRECTED, data={"edge_type": None}, ), diff --git a/causy/orientation_rules/pc.py b/causy/orientation_rules/pc.py index 00e5cd2..ef7a0ea 100644 --- a/causy/orientation_rules/pc.py +++ b/causy/orientation_rules/pc.py @@ -27,11 +27,11 @@ def test( self, nodes: Tuple[str], graph: BaseGraphInterface ) -> Optional[List[TestResult] | TestResult]: """ - We call triples x, y, z of nodes v structures if x and y that are NOT adjacent but share an adjacent node z. - V structures looks like this in the undirected skeleton: (x - z - y). + We call triples u, v, z of nodes v structures if u and v that are NOT adjacent but share an adjacent node z. + V structures looks like this in the undirected skeleton: (u - z - v). We now check if z is in the separating set. - If z is not in the separating set, we know that x and y are uncorrelated given z. - So, the edges must be oriented from x to z and from y to z (x -> z <- y). + If z is not in the separating set, we know that u and v are uncorrelated given z. + So, the edges must be oriented from u to z and from v to z (u -> z <- v). :param nodes: list of nodes :param graph: the current graph :returns: list of actions that will be executed on graph @@ -41,11 +41,11 @@ def test( x = graph.nodes[nodes[0]] y = graph.nodes[nodes[1]] - # if x and y are adjacent, do nothing + # if u and v are adjacent, do nothing if graph.undirected_edge_exists(x, y): - return TestResult(x=x, y=y, action=TestResultAction.DO_NOTHING, data={}) + return TestResult(u=x, v=y, action=TestResultAction.DO_NOTHING, data={}) - # if x and y are NOT adjacent, store all shared adjacent nodes + # if u and v are NOT adjacent, store all shared adjacent nodes potential_zs = set(graph.edges[x.id].keys()).intersection( set(graph.edges[y.id].keys()) ) @@ -54,7 +54,7 @@ def test( x, y, TestResultAction.REMOVE_EDGE_UNDIRECTED ) - # if x and y are not independent given z, safe action: make z a collider + # if u and v are not independent given z, safe action: make z a collider results = [] for z in potential_zs: z = graph.nodes[z] @@ -67,14 +67,14 @@ def test( if z.id not in separators: results += [ TestResult( - x=z, - y=x, + u=z, + v=x, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ), TestResult( - x=z, - y=y, + u=z, + v=y, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ), @@ -103,11 +103,11 @@ def test( x = graph.nodes[nodes[0]] y = graph.nodes[nodes[1]] - # if x and y are adjacent, do nothing + # if u and v are adjacent, do nothing if graph.edge_exists(x, y): return - # if x and y are NOT adjacent, store all shared adjacent nodes + # if u and v are NOT adjacent, store all shared adjacent nodes potential_zs = set(graph.edges[x.id].keys()).intersection( set(graph.edges[y.id].keys()) ) @@ -126,8 +126,8 @@ def test( if breakflag is True: continue return TestResult( - x=y, - y=z, + u=y, + v=z, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -139,8 +139,8 @@ def test( if graph.only_directed_edge_exists(graph.nodes[node], x): continue return TestResult( - x=x, - y=z, + u=x, + v=z, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -180,8 +180,8 @@ def test( ): results.append( TestResult( - x=y, - y=x, + u=y, + v=x, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -193,8 +193,8 @@ def test( ): results.append( TestResult( - x=x, - y=y, + u=x, + v=y, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -243,8 +243,8 @@ def test( ): results.append( TestResult( - x=z, - y=x, + u=z, + v=x, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -259,8 +259,8 @@ def test( ): results.append( TestResult( - x=x, - y=z, + u=x, + v=z, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -309,8 +309,8 @@ def test( ): results.append( TestResult( - x=z, - y=x, + u=z, + v=x, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -325,8 +325,8 @@ def test( ): results.append( TestResult( - x=y, - y=x, + u=y, + v=x, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -341,8 +341,8 @@ def test( ): results.append( TestResult( - x=z, - y=w, + u=z, + v=w, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) @@ -357,8 +357,8 @@ def test( ): results.append( TestResult( - x=y, - y=w, + u=y, + v=w, action=TestResultAction.REMOVE_EDGE_DIRECTED, data={}, ) diff --git a/causy/ui.py b/causy/ui.py new file mode 100644 index 0000000..27aa4c2 --- /dev/null +++ b/causy/ui.py @@ -0,0 +1,95 @@ +import os +from datetime import datetime + +import fastapi +import uvicorn +from typing import Any, Dict, List + +from fastapi import APIRouter +from pydantic import BaseModel, Json, UUID4, Field +from starlette.staticfiles import StaticFiles + +import logging + +logger = logging.getLogger(__name__) + +API_ROUTES = APIRouter() + +MODEL = None + + +class CausyAlgorithm(BaseModel): + type: str + reference: str + + +class CausyNode(BaseModel): + id: UUID4 + name: str + + +class CausyEdgeValue(BaseModel): + metadata: Dict[str, Any] = None + edge_type: str = None + + +class CausyEdge(BaseModel): + from_field: CausyNode = Field(alias="from") + to: CausyNode + value: CausyEdgeValue + + +class CausyModel(BaseModel): + name: str + created_at: datetime + algorithm: CausyAlgorithm + steps: List[Dict[str, Any]] + nodes: Dict[UUID4, CausyNode] + edges: List[CausyEdge] + + +@API_ROUTES.get("/status", response_model=Dict[str, Any]) +async def get_status(): + """Get the current status of the API.""" + return {"status": "ok"} + + +@API_ROUTES.get("/model", response_model=CausyModel) +async def get_model(): + """Get the current model.""" + return MODEL + + +def server(result: Dict[str, Any]): + """Create the FastAPI server.""" + app = fastapi.FastAPI( + title="causy-api", + version="0.0.1", + description="causys internal api to serve data from the result files", + ) + global MODEL + MODEL = CausyModel(**result) + + app.include_router(API_ROUTES, prefix="/api/v1", tags=["api"]) + app.mount("", StaticFiles(directory="./causy/static", html=True), name="static") + + host = os.getenv("HOST", "localhost") + port = int(os.getenv("PORT", "8000")) + cors_enabled = os.getenv("CORS_ENABLED", "false").lower() == "true" + + # cors e.g. for development of separate frontend + if cors_enabled: + logger.warning("🌐 CORS enabled") + from fastapi.middleware.cors import CORSMiddleware + + app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + server_config = uvicorn.Config(app, host=host, port=port, log_level="error") + server = uvicorn.Server(server_config) + return server_config, server diff --git a/poetry.lock b/poetry.lock index 8e08fa5..146416a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,35 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.2.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "black" @@ -233,6 +264,25 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "fastapi" +version = "0.109.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.109.0-py3-none-any.whl", hash = "sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093"}, + {file = "fastapi-0.109.0.tar.gz", hash = "sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.35.0,<0.36.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "filelock" version = "3.13.1" @@ -349,6 +399,17 @@ smb = ["smbprotocol"] ssh = ["paramiko"] tqdm = ["tqdm"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + [[package]] name = "identify" version = "2.5.33" @@ -363,6 +424,17 @@ files = [ [package.extras] license = ["ukkonen"] +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + [[package]] name = "jinja2" version = "3.1.2" @@ -1061,6 +1133,142 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pydantic" +version = "2.5.3" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.14.6" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.14.6" +description = "" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pygments" version = "2.17.2" @@ -1308,6 +1516,34 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "starlette" +version = "0.35.1" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.35.1-py3-none-any.whl", hash = "sha256:50bbbda9baa098e361f398fda0928062abbaf1f54f4fadcbe17c092a01eb9a25"}, + {file = "starlette-0.35.1.tar.gz", hash = "sha256:3e2639dac3520e4f58734ed22553f950d3f3cb1001cd2eaac4d57e8cdc5f66bc"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] + [[package]] name = "sympy" version = "1.12" @@ -1432,6 +1668,24 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "uvicorn" +version = "0.27.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.27.0-py3-none-any.whl", hash = "sha256:890b00f6c537d58695d3bb1f28e23db9d9e7a17cbcc76d7457c499935f933e24"}, + {file = "uvicorn-0.27.0.tar.gz", hash = "sha256:c855578045d45625fd027367f7653d249f7c49f9361ba15cf9624186b26b8eb6"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + [[package]] name = "virtualenv" version = "20.25.0" @@ -1455,4 +1709,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.12" -content-hash = "2cdde9e9f219c134c272f6309a54cc8758a1613ed77e2a272796e21f127ef50c" +content-hash = "88a8d0206b271d2328c5d4dd74f70a69cab000eb729593ab162fd55c4ad4ff02" diff --git a/pyproject.toml b/pyproject.toml index 46adecb..2eeabad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,8 @@ typer = "^0.9.0" networkx = "^3.1" matplotlib = "^3.8.0" torch = "^2.1.0" +fastapi = "^0.109.0" +uvicorn = "^0.27.0" [tool.poetry.group.dev.dependencies] diff --git a/tests/test_independence_tests.py b/tests/test_independence_tests.py index dc2945a..8aba0c9 100644 --- a/tests/test_independence_tests.py +++ b/tests/test_independence_tests.py @@ -19,8 +19,8 @@ def test_correlation_coefficient_standard_model(self): x = [random.normalvariate(mu=0.0, sigma=1.0) for _ in range(n)] noise_y = [random.normalvariate(mu=0.0, sigma=1.0) for _ in range(n)] y = sum_lists([5 * x_val for x_val in x], noise_y) - samples["x"] = x - samples["y"] = y + samples["u"] = x + samples["v"] = y for i in range(n): entry = {} for key in samples.keys(): diff --git a/tests/test_orientation_rules.py b/tests/test_orientation_rules.py index 6198504..2f284b3 100644 --- a/tests/test_orientation_rules.py +++ b/tests/test_orientation_rules.py @@ -20,8 +20,8 @@ def test_collider_rule_fci(self): x, y, TestResult( - x=x, - y=z, + u=x, + v=z, action=TestResultAction.REMOVE_EDGE_UNDIRECTED, data={"separatedBy": []}, ), diff --git a/tests/test_orientation_tests.py b/tests/test_orientation_tests.py index add394e..e7fd8fe 100644 --- a/tests/test_orientation_tests.py +++ b/tests/test_orientation_tests.py @@ -26,8 +26,8 @@ def test_collider_test(self): x, y, TestResult( - x=x, - y=z, + u=x, + v=z, action=TestResultAction.REMOVE_EDGE_UNDIRECTED, data={"separatedBy": []}, ), @@ -49,8 +49,8 @@ def test_collider_test_with_nonempty_separation_set(self): x, y, TestResult( - x=x, - y=z, + u=x, + v=z, action=TestResultAction.REMOVE_EDGE_UNDIRECTED, data={"separatedBy": [y]}, ), From af0093ba7c6ce4a45f78622bf523d28b060e5a34 Mon Sep 17 00:00:00 2001 From: Lilith Wittmann Date: Sat, 27 Jan 2024 14:58:14 +0100 Subject: [PATCH 2/4] cleanup(cli): remove old rendering feature --- causy/cli.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/causy/cli.py b/causy/cli.py index 8d52ba7..0274332 100644 --- a/causy/cli.py +++ b/causy/cli.py @@ -5,7 +5,6 @@ import logging import typer -import uvicorn from causy.graph_model import graph_model_factory from causy.serialization import serialize_model @@ -67,7 +66,6 @@ def execute( pipeline: str = None, algorithm: str = None, output_file: str = None, - render_save_file: str = None, log_level: str = "ERROR", ): logging.basicConfig(level=log_level) @@ -125,20 +123,6 @@ def execute( } file.write(json.dumps(export, cls=MyJSONEncoder, indent=4)) - if render_save_file: - # I'm just a hacky rendering function, pls replace me with causy ui 🙄 - typer.echo(f"💾 Saving graph to {render_save_file}") - import networkx as nx - import matplotlib.pyplot as plt - - n_graph = nx.DiGraph() - for u in model.graph.edges: - for v in model.graph.edges[u]: - n_graph.add_edge(model.graph.nodes[u].name, model.graph.nodes[v].name) - fig = plt.figure(figsize=(10, 10)) - nx.draw(n_graph, with_labels=True, ax=fig.add_subplot(111)) - fig.savefig(render_save_file) - if __name__ == "__main__": app() From 5071b0c510542e3d6b419bc824481f5a57bf899a Mon Sep 17 00:00:00 2001 From: Lilith Wittmann Date: Mon, 29 Jan 2024 18:45:16 +0100 Subject: [PATCH 3/4] add node positions to ui models --- causy/ui.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/causy/ui.py b/causy/ui.py index 27aa4c2..7b86aa6 100644 --- a/causy/ui.py +++ b/causy/ui.py @@ -3,7 +3,7 @@ import fastapi import uvicorn -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from fastapi import APIRouter from pydantic import BaseModel, Json, UUID4, Field @@ -23,9 +23,15 @@ class CausyAlgorithm(BaseModel): reference: str +class CausyNodePosition(BaseModel): + x: float + y: float + + class CausyNode(BaseModel): id: UUID4 name: str + position: Optional[CausyNodePosition] = None class CausyEdgeValue(BaseModel): From 174647e308779d57a613d0ed60d29c8fbd6b1232 Mon Sep 17 00:00:00 2001 From: Lilith Wittmann Date: Mon, 29 Jan 2024 18:58:40 +0100 Subject: [PATCH 4/4] chore(build): add __version__ in __init__.py --- causy/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/causy/__init__.py b/causy/__init__.py index 8b13789..502f49e 100644 --- a/causy/__init__.py +++ b/causy/__init__.py @@ -1 +1,11 @@ +"""causy +Causal discovery made easy. Causy allows you to use and implement causal discovery algorithms with easy to use, extend and maintain pipelines. It is built based on pytorch which allows you to run the algorithms on CPUs as well as GPUs seamlessly. + +Learn more at https://causy.dev. + +""" + +import importlib.metadata + +__version__ = importlib.metadata.version("causy")