Repository: mlco2/codecarbon (language: python)

Issue:
CarbonBoard refactor: Data Persistence Pattern
Use SQLAlchemy ORM for a db-agnostic persistence layer. See [Repository Pattern](https://www.cosmicpython.com/book/chapter_02_repository.html)


Discussion / hints:

Fix patch:
diff --git a/.isort.cfg b/.isort.cfg
index ff03f0ea6..a55d9354c 100644
--- a/.isort.cfg
+++ b/.isort.cfg
@@ -4,6 +4,6 @@ include_trailing_comma=True
 force_grid_wrap=0
 use_parentheses=True
 line_length=88
-known_third_party = apscheduler,comet_ml,cpuinfo,dash,dash_bootstrap_components,dash_core_components,dash_html_components,dash_table,database,dependencies,domain,fastapi,fire,infra,kerastuner,numpy,pandas,pkg_resources,plotly,pydantic,pynvml,pytest,requests,responses,routers,setuptools,sklearn,sqlalchemy,tensorflow
+known_third_party = apscheduler,comet_ml,cpuinfo,dash,dash_bootstrap_components,dash_core_components,dash_html_components,dash_table,fastapi,fire,kerastuner,numpy,pandas,pkg_resources,plotly,pydantic,pynvml,pytest,requests,responses,setuptools,sklearn,sqlalchemy,tensorflow
 skip_glob = /*
 not_skip = setup.py
diff --git a/api/domain/models.py b/api/domain/models.py
deleted file mode 100644
index fb8ebcb15..000000000
--- a/api/domain/models.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# read https://fastapi.tiangolo.com/tutorial/sql-databases/
-
-
-from database.database import Base
-
-# Put here the structure of the database
-from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String
-from sqlalchemy.orm import relationship
-
-
-class Emission(Base):
-    __tablename__ = "emissions"
-
-    id = Column(Integer, primary_key=True, index=True)
-    timestamp = Column(DateTime)
-    # experiment_id = Column(Integer)
-    duration = Column(Float)
-    emissions = Column(Float)
-    energy_consumed = Column(Float)
-    country_name = Column(String)
-    country_iso_code = Column(String)
-    region = Column(String)
-    on_cloud = Column(Boolean, default=False)
-    cloud_provider = Column(String)
-    cloud_region = Column(String)
-    experiment_id = Column(Integer, ForeignKey("experiments.id"))
-    experiment = relationship("Experiment", back_populates="emissions")
-
-
-# Un experiment = un run
-class Experiment(Base):
-    __tablename__ = "experiments"
-
-    id = Column(Integer, primary_key=True, index=True)
-    timestamp = Column(DateTime)
-    name = Column(String)
-    description = Column(String)
-    is_active = Column(Boolean, default=True)
-    project_id = Column(Integer, ForeignKey("projects.id"))
-    emissions = relationship("Emission", back_populates="experiment")
-    project = relationship("Project", back_populates="experiments")
-
-
-class Project(Base):
-    __tablename__ = "projects"
-
-    id = Column(Integer, primary_key=True, index=True)
-    name = Column(String)
-    description = Column(String)
-    team_id = Column(Integer, ForeignKey("teams.id"))
-    experiments = relationship("Experiment", back_populates="project")
-    team = relationship("Team", back_populates="projects")
-
-
-class Team(Base):
-    __tablename__ = "teams"
-
-    id = Column(Integer, primary_key=True, index=True)
-    name = Column(String)
-    description = Column(String)
-    organization_id = Column(Integer, ForeignKey("organizations.id"))
-    projects = relationship("Project", back_populates="team")
-    organization = relationship("Organization", back_populates="teams")
-
-
-# Organization
-class Organization(Base):
-    __tablename__ = "organizations"
-
-    id = Column(Integer, primary_key=True, index=True)
-    name = Column(String)
-    description = Column(String)
-    teams = relationship("Team", back_populates="organization")
-
-
-class User(Base):
-    __tablename__ = "users"
-
-    id = Column(Integer, primary_key=True, index=True)
-    name = Column(String)
-    email = Column(String, unique=True, index=True)
-    hashed_password = Column(String)
-    is_active = Column(Boolean, default=True)
-    # TODO: Associate user with his entities
diff --git a/api/domain/schemas.py b/api/domain/schemas.py
deleted file mode 100644
index 9bca3d255..000000000
--- a/api/domain/schemas.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""
-https://fastapi.tiangolo.com/tutorial/sql-databases/
-
-To avoid confusion between the SQLAlchemy models and the Pydantic models, we will have the file models.py with the SQLAlchemy models, and the file schemas.py with the Pydantic models.
-
-These Pydantic models define more or less a "schema" (a valid data shape).
-
-So this will help us avoiding confusion while using both.
-"""
-
-# TODO : Move this file in codecarbon package
-
-from datetime import datetime
-from typing import List, Optional
-
-from pydantic import BaseModel, Field
-
-
-class EmissionBase(BaseModel):
-    timestamp: datetime
-    experiment_id: str
-    duration: int = Field(
-        ..., gt=0, description="The duration must be greater than zero"
-    )
-    emissions: float = Field(
-        ..., gt=0, description="The emissions must be greater than zero"
-    )
-    energy_consumed: float = Field(
-        ..., gt=0, description="The energy_consumed must be greater than zero"
-    )
-    country_name: Optional[str] = None
-    country_iso_code: Optional[str] = None
-    region: Optional[str] = None
-    on_cloud: bool
-    cloud_provider: Optional[str] = None
-    cloud_region: Optional[str] = None
-
-    class Config:
-        # orm_mode = True
-        schema_extra = {
-            "example": {
-                "timestamp": "2021-04-04T08:43:00+02:00",
-                "experiment_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
-                "duration": 98745,
-                "emissions": 1.548444,
-                "energy_consumed": 57.21874,
-                "country_name": "France",
-                "country_iso_code": "FRA",
-                "region": "france",
-                "on_cloud": True,
-                "cloud_provider": "aws",
-                "cloud_region": "eu-west-1a",
-            }
-        }
-
-
-class EmissionCreate(EmissionBase):
-    pass
-
-
-class Emission(EmissionBase):
-    id: int
-
-
-# Experiment
-class ExperimentBase(BaseModel):
-    timestamp: datetime
-    name: str
-    description: str
-    is_active: bool
-    project_id: int
-
-
-class ExperimentCreate(ExperimentBase):
-    pass
-
-
-class Experiment(ExperimentBase):
-    id: int
-    emissions: List[Emission] = []
-
-
-# Project
-class ProjectBase(BaseModel):
-    name: str
-    description: str
-    team_id: int
-
-
-class ProjectCreate(ProjectBase):
-    pass
-
-
-class Project(ProjectBase):
-    id: int
-    experiments: List[Experiment] = []
-
-
-# Team
-class TeamBase(BaseModel):
-    name: str
-    description: str
-    organization_id: int
-
-
-class TeamCreate(TeamBase):
-    pass
-
-
-class Team(TeamBase):
-    id: int
-    projects: List[Project] = []
-
-
-# Organization
-class OrganizationBase(BaseModel):
-    name: str
-    description: str
-
-
-class OrganizationCreate(OrganizationBase):
-    pass
-
-
-class Organization(OrganizationBase):
-    id: int
-    teams: List[Team] = []
-
-
-# User
-class UserBase(BaseModel):
-    email: str
-
-
-class UserCreate(UserBase):
-    password: str
-
-
-class User(UserBase):
-    id: int
-    is_active: bool
-
-    class Config:
-        orm_mode = True
diff --git a/api/infra/repository/repository_emissions.py b/api/infra/repository/repository_emissions.py
deleted file mode 100644
index 7d29f504c..000000000
--- a/api/infra/repository/repository_emissions.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# from uuid import uuid4 as uuid
-import abc
-from typing import List
-
-from domain import models, schemas
-from sqlalchemy.orm import Session
-
-
-"""
-The emissions are stored in the database by this repository class.
-The emission repository is implemented to facilitate tests & the switch of database backend.
-It relies on an abstract repository which exposes an interface of signatures shared by all repository implementations.
-
-"""
-
-
-class AbstractRepository(abc.ABC):
-    @abc.abstractmethod
-    def add_save_emission(self, db: Session, emission: schemas.EmissionCreate):
-        raise NotImplementedError
-
-    @abc.abstractmethod
-    def get_db_to_class(self, emission: models.Emission) -> schemas.Emission:
-        raise NotImplementedError
-
-    @abc.abstractmethod
-    def get_one_emission(self, db: Session, emission_id) -> schemas.Emission:
-        raise NotImplementedError
-
-    @abc.abstractmethod
-    def get_emissions_from_experiment(
-        self, db: Session, experiment_id
-    ) -> List[schemas.Emission]:
-        raise NotImplementedError
-
-
-class SqlAlchemyRepository(AbstractRepository):
-    def __init__(self, session):
-        self.session = session
-
-    def get_db_to_class(self, emission: models.Emission) -> schemas.Emission:
-        """Convert a models.Emission to a schemas.Emission
-
-        :emission: An Emission in SQLAlchemy format.
-        :returns: An Emission in pyDantic BaseModel format.
-        :rtype: schemas.Emission
-        """
-        return schemas.Emission(
-            id=emission.id,
-            timestamp=emission.timestamp,
-            duration=emission.duration,
-            emissions=emission.emissions,
-            energy_consumed=emission.energy_consumed,
-            country_name=emission.country_name,
-            country_iso_code=emission.country_iso_code,
-            region=emission.region,
-            on_cloud=emission.on_cloud,
-            cloud_provider=emission.cloud_provider,
-            cloud_region=emission.cloud_region,
-            experiment_id=emission.experiment_id,
-        )
-
-    def add_save_emission(self, db: Session, emission: schemas.EmissionCreate):
-        """Save an emission to the database.
-
-        :db: : A SQLAlchemy session.
-        :emission: An Emission in pyDantic BaseModel format.
-        """
-        db_emission = models.Emission(
-            timestamp=emission.timestamp,
-            duration=emission.duration,
-            emissions=emission.emissions,
-            energy_consumed=emission.energy_consumed,
-            country_name=emission.country_name,
-            country_iso_code=emission.country_iso_code,
-            region=emission.region,
-            on_cloud=emission.on_cloud,
-            cloud_provider=emission.cloud_provider,
-            cloud_region=emission.cloud_region,
-            experiment_id=emission.experiment_id,
-        )
-        db.add(db_emission)
-        db.commit()
-
-    def get_one_emission(self, db: Session, emission_id) -> schemas.Emission:
-        """Find the emission in database and return it
-
-        :db: : A SQLAlchemy session.
-        :emission_id: The id of the emission to retreive.
-        :returns: An Emission in pyDantic BaseModel format.
-        :rtype: schemas.Emission
-        """
-        e = db.query(models.Emission).filter(models.Emission.id == emission_id).first()
-        if e is None:
-            return None
-        else:
-            return self.get_db_to_class(e)
-
-    def get_emissions_from_experiment(
-        self, db: Session, experiment_id
-    ) -> List[schemas.Emission]:
-        """Find the emissions from an experiment in database and return it
-
-        :db: : A SQLAlchemy session.
-        :experiment_id: The id of the experiment to retreive emissions from.
-        :returns: An Emission in pyDantic BaseModel format.
-        :rtype: List[schemas.Emission]
-        """
-        res = db.query(models.Emission).filter(
-            models.Emission.experiment_id == experiment_id
-        )
-        if res.first() is None:
-            return None
-        else:
-            # Convert the table of models.Emission to a table of schemas.Emission
-            emissions = []
-            for e in res:
-                emission = self.get_db_to_class(e)
-                emissions.append(emission)
-            return emissions
diff --git a/carbonserver/Images/CodecarbonDB.jpg b/carbonserver/Images/CodecarbonDB.jpg
new file mode 100644
index 000000000..a5f21b0ba
Binary files /dev/null and b/carbonserver/Images/CodecarbonDB.jpg differ
diff --git a/api/README.md b/carbonserver/README.md
similarity index 100%
rename from api/README.md
rename to carbonserver/README.md
diff --git a/api/__init__.py b/carbonserver/__init__.py
similarity index 100%
rename from api/__init__.py
rename to carbonserver/__init__.py
diff --git a/api/database/__init__.py b/carbonserver/carbonserver/__init__.py
similarity index 100%
rename from api/database/__init__.py
rename to carbonserver/carbonserver/__init__.py
diff --git a/api/routers/__init__.py b/carbonserver/carbonserver/api/__init__.py
similarity index 100%
rename from api/routers/__init__.py
rename to carbonserver/carbonserver/api/__init__.py
diff --git a/api/dependencies.py b/carbonserver/carbonserver/api/dependencies.py
similarity index 92%
rename from api/dependencies.py
rename to carbonserver/carbonserver/api/dependencies.py
index 9f37df7b0..09150ed1d 100644
--- a/api/dependencies.py
+++ b/carbonserver/carbonserver/api/dependencies.py
@@ -1,7 +1,6 @@
-from typing import Optional
-
-from database.database import SessionLocal
 from fastapi import Header, HTTPException
+from typing import Optional
+from carbonserver.database.database import SessionLocal
 
 # TODO : https://fastapi.tiangolo.com/tutorial/security/
 
diff --git a/carbonserver/carbonserver/api/domain/__init__.py b/carbonserver/carbonserver/api/domain/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/api/database/models.py b/carbonserver/carbonserver/api/domain/models.py
similarity index 98%
rename from api/database/models.py
rename to carbonserver/carbonserver/api/domain/models.py
index a2e3a2a92..22ef2d2bd 100644
--- a/api/database/models.py
+++ b/carbonserver/carbonserver/api/domain/models.py
@@ -1,12 +1,12 @@
 # read https://fastapi.tiangolo.com/tutorial/sql-databases/
 
 
+from carbonserver.database.database import Base
+
 # Put here the structure of the database
 from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Integer, String
 from sqlalchemy.orm import relationship
 
-from .database import Base
-
 
 class Emission(Base):
     __tablename__ = "emissions"
diff --git a/carbonserver/carbonserver/api/infra/repository/__init__.py b/carbonserver/carbonserver/api/infra/repository/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/carbonserver/carbonserver/api/infra/repository/repository_emissions.py b/carbonserver/carbonserver/api/infra/repository/repository_emissions.py
new file mode 100644
index 000000000..531291ed6
--- /dev/null
+++ b/carbonserver/carbonserver/api/infra/repository/repository_emissions.py
@@ -0,0 +1,189 @@
+# from uuid import uuid4 as uuid
+import abc
+from typing import List
+
+from carbonserver.api.domain import models
+from carbonserver.database import schemas
+from sqlalchemy.orm import Session
+
+"""
+The emissions are stored in the database by this repository class.
+The emission repository is implemented to facilitate tests & the switch of database backend.
+It relies on an abstract repository which exposes an interface of signatures shared by all repository implementations.
+
+"""
+
+
+class AbstractRepository(abc.ABC):
+    @abc.abstractmethod
+    def add_save_emission(self, emission: schemas.EmissionCreate):
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_db_to_class(self, emission: models.Emission) -> schemas.Emission:
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_one_emission(self, emission_id) -> schemas.Emission:
+        raise NotImplementedError
+
+    @abc.abstractmethod
+    def get_emissions_from_experiment(self, experiment_id) -> List[schemas.Emission]:
+        raise NotImplementedError
+
+
+class SqlAlchemyRepository(AbstractRepository):
+    def __init__(self, db: Session):
+        self.db = db
+
+    def get_db_to_class(self, emission: models.Emission) -> schemas.Emission:
+        """Convert a models.Emission to a schemas.Emission
+
+        :emission: An Emission in SQLAlchemy format.
+        :returns: An Emission in pyDantic BaseModel format.
+        :rtype: schemas.Emission
+        """
+        return schemas.Emission(
+            id=emission.id,
+            timestamp=emission.timestamp,
+            duration=emission.duration,
+            emissions=emission.emissions,
+            energy_consumed=emission.energy_consumed,
+            country_name=emission.country_name,
+            country_iso_code=emission.country_iso_code,
+            region=emission.region,
+            on_cloud=emission.on_cloud,
+            cloud_provider=emission.cloud_provider,
+            cloud_region=emission.cloud_region,
+            experiment_id=emission.experiment_id,
+        )
+
+    def add_save_emission(self, emission: schemas.EmissionCreate):
+        """Save an emission to the database.
+
+        :db: : A SQLAlchemy session.
+        :emission: An Emission in pyDantic BaseModel format.
+        """
+        db_emission = models.Emission(
+            timestamp=emission.timestamp,
+            duration=emission.duration,
+            emissions=emission.emissions,
+            energy_consumed=emission.energy_consumed,
+            country_name=emission.country_name,
+            country_iso_code=emission.country_iso_code,
+            region=emission.region,
+            on_cloud=emission.on_cloud,
+            cloud_provider=emission.cloud_provider,
+            cloud_region=emission.cloud_region,
+            experiment_id=emission.experiment_id,
+        )
+        self.db.add(db_emission)
+        self.db.commit()
+
+    def get_one_emission(self, emission_id) -> schemas.Emission:
+        """Find the emission in database and return it
+
+        :db: : A SQLAlchemy session.
+        :emission_id: The id of the emission to retreive.
+        :returns: An Emission in pyDantic BaseModel format.
+        :rtype: schemas.Emission
+        """
+        e = (
+            self.db.query(models.Emission)
+            .filter(models.Emission.id == emission_id)
+            .first()
+        )
+        if e is None:
+            return None
+        else:
+            return self.get_db_to_class(e)
+
+    def get_emissions_from_experiment(self, experiment_id) -> List[schemas.Emission]:
+        """Find the emissions from an experiment in database and return it
+
+        :db: : A SQLAlchemy session.
+        :experiment_id: The id of the experiment to retreive emissions from.
+        :returns: An Emission in pyDantic BaseModel format.
+        :rtype: List[schemas.Emission]
+        """
+        res = self.db.query(models.Emission).filter(
+            models.Emission.experiment_id == experiment_id
+        )
+        if res.first() is None:
+            return []
+        else:
+            # Convert the table of models.Emission to a table of schemas.Emission
+            emissions = []
+            for e in res:
+                emission = self.get_db_to_class(e)
+                emissions.append(emission)
+            return emissions
+
+
+class InMemoryRepository(AbstractRepository):
+    def __init__(self):
+        self.emissions: List = []
+        self.id: int = 0
+
+    def add_save_emission(self, emission: schemas.EmissionCreate):
+        self.emissions.append(
+            models.Emission(
+                id=self.id + 1,
+                timestamp=emission.timestamp,
+                duration=emission.duration,
+                emissions=emission.emissions,
+                energy_consumed=emission.energy_consumed,
+                country_name=emission.country_name,
+                country_iso_code=emission.country_iso_code,
+                region=emission.region,
+                on_cloud=emission.on_cloud,
+                cloud_provider=emission.cloud_provider,
+                cloud_region=emission.cloud_region,
+                experiment_id=emission.experiment_id,
+            )
+        )
+
+    def get_db_to_class(self, emission: models.Emission) -> schemas.Emission:
+        return schemas.Emission(
+            id=emission.id,
+            timestamp=emission.timestamp,
+            duration=emission.duration,
+            emissions=emission.emissions,
+            energy_consumed=emission.energy_consumed,
+            country_name=emission.country_name,
+            country_iso_code=emission.country_iso_code,
+            region=emission.region,
+            on_cloud=emission.on_cloud,
+            cloud_provider=emission.cloud_provider,
+            cloud_region=emission.cloud_region,
+            experiment_id=emission.experiment_id,
+        )
+
+    def get_one_emission(self, emission_id) -> schemas.Emission:
+        first_emission = self.emissions[0]
+        return schemas.Emission(
+            id=first_emission.id,
+            timestamp=first_emission.timestamp,
+            duration=first_emission.duration,
+            emissions=first_emission.emissions,
+            energy_consumed=first_emission.energy_consumed,
+            country_name=first_emission.country_name,
+            country_iso_code=first_emission.country_iso_code,
+            region=first_emission.region,
+            on_cloud=first_emission.on_cloud,
+            cloud_provider=first_emission.cloud_provider,
+            cloud_region=first_emission.cloud_region,
+            experiment_id=first_emission.experiment_id,
+        )
+
+    def get_emissions_from_experiment(self, experiment_id) -> List[schemas.Emission]:
+        print(len(self.emissions))
+        stored_emissions = [
+            stored_emission
+            for stored_emission in self.emissions
+            if stored_emission.experiment_id == experiment_id
+        ]
+        emissions: List[schemas.Emission] = []
+        for emission in stored_emissions:
+            emissions.append(self.get_db_to_class(emission))
+        return emissions
diff --git a/api/infra/repository/repository_experiments.py b/carbonserver/carbonserver/api/infra/repository/repository_experiment.py
similarity index 91%
rename from api/infra/repository/repository_experiments.py
rename to carbonserver/carbonserver/api/infra/repository/repository_experiment.py
index 2dffb62dc..821ecf3b0 100644
--- a/api/infra/repository/repository_experiments.py
+++ b/carbonserver/carbonserver/api/infra/repository/repository_experiment.py
@@ -1,12 +1,13 @@
 # from uuid import uuid4 as uuid
-from domain import models, schemas
+from carbonserver.api.domain import models
+from carbonserver.database import schemas
+
 from sqlalchemy.orm import Session
 
 # TODO : read https://fastapi.tiangolo.com/tutorial/sql-databases/
 
 """
 Here there is all the method to manipulate the experiment data
-
 """
 
 
diff --git a/api/infra/repository/repository_organizations.py b/carbonserver/carbonserver/api/infra/repository/repository_organization.py
similarity index 83%
rename from api/infra/repository/repository_organizations.py
rename to carbonserver/carbonserver/api/infra/repository/repository_organization.py
index b03a9849d..cc17179df 100644
--- a/api/infra/repository/repository_organizations.py
+++ b/carbonserver/carbonserver/api/infra/repository/repository_organization.py
@@ -1,12 +1,12 @@
-# from uuid import uuid4 as uuid
-from domain import models, schemas
+from carbonserver.api.domain import models
+from carbonserver.database import schemas
+
 from sqlalchemy.orm import Session
 
 # TODO : read https://fastapi.tiangolo.com/tutorial/sql-databases/
 
 """
 Here there is all the method to manipulate the project data
-
 """
 
 
@@ -25,10 +25,9 @@ def save_organization(db: Session, organization: schemas.OrganizationCreate):
 
 def get_one_organization(organization_id):
     # TODO : find the Organization in database and return it
-
-    return
+    pass
 
 
-def get_Team_from_Organizations(organization_id):
+def get_team_from_organizations(organization_id):
     # TODO : get Organization from team id in database
-    return
+    pass
diff --git a/api/infra/repository/repository_projects.py b/carbonserver/carbonserver/api/infra/repository/repository_project.py
similarity index 87%
rename from api/infra/repository/repository_projects.py
rename to carbonserver/carbonserver/api/infra/repository/repository_project.py
index 92e80b3aa..1a0e4bf72 100644
--- a/api/infra/repository/repository_projects.py
+++ b/carbonserver/carbonserver/api/infra/repository/repository_project.py
@@ -1,12 +1,12 @@
-# from uuid import uuid4 as uuid
-from domain import models, schemas
+from carbonserver.api.domain import models
+from carbonserver.database import schemas
+
 from sqlalchemy.orm import Session
 
 # TODO : read https://fastapi.tiangolo.com/tutorial/sql-databases/
 
 """
 Here there is all the method to manipulate the project data
-
 """
 
 
@@ -25,10 +25,9 @@ def save_project(db: Session, project: schemas.ProjectCreate):
 
 def get_one_Project(project_id):
     # TODO : find the Project in database and return it
-
-    return
+    pass
 
 
 def get_Projects_from_Project(project_id):
     # TODO : get Projects from Project id in database
-    return
+    pass
diff --git a/api/infra/repository/repository_teams.py b/carbonserver/carbonserver/api/infra/repository/repository_teams.py
similarity index 87%
rename from api/infra/repository/repository_teams.py
rename to carbonserver/carbonserver/api/infra/repository/repository_teams.py
index bc2afe7af..eb56fccba 100644
--- a/api/infra/repository/repository_teams.py
+++ b/carbonserver/carbonserver/api/infra/repository/repository_teams.py
@@ -1,12 +1,13 @@
 # from uuid import uuid4 as uuid
-from domain import models, schemas
+from carbonserver.api.domain import models
+from carbonserver.database import schemas
+
 from sqlalchemy.orm import Session
 
 # TODO : read https://fastapi.tiangolo.com/tutorial/sql-databases/
 
 """
 Here there is all the method to manipulate the project data
-
 """
 
 
@@ -25,10 +26,9 @@ def save_team(db: Session, team: schemas.TeamCreate):
 
 def get_one_Team(team_id):
     # TODO : find the Team in database and return it
-
-    return
+    pass
 
 
 def get_Project_from_Teams(team_id):
     # TODO : get Projects from Project id in database
-    return
+    pass
diff --git a/carbonserver/carbonserver/api/routers/__init__.py b/carbonserver/carbonserver/api/routers/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/api/routers/emissions.py b/carbonserver/carbonserver/api/routers/emissions.py
similarity index 78%
rename from api/routers/emissions.py
rename to carbonserver/carbonserver/api/routers/emissions.py
index 1193fa162..1ced135f4 100644
--- a/api/routers/emissions.py
+++ b/carbonserver/carbonserver/api/routers/emissions.py
@@ -1,7 +1,8 @@
-from dependencies import get_db, get_token_header
-from domain.schemas import EmissionCreate
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.database.schemas import EmissionCreate
+from carbonserver.api.infra.repository.repository_emissions import SqlAlchemyRepository
+
 from fastapi import APIRouter, Depends, HTTPException, Path
-from infra.repository.repository_emissions import SqlAlchemyRepository
 from sqlalchemy.orm import Session
 
 router = APIRouter(
@@ -12,7 +13,7 @@
 @router.put("/emission", tags=["emissions"], status_code=201)
 def add_emission(emission: EmissionCreate, db: Session = Depends(get_db)):
     repository_emissions = SqlAlchemyRepository(db)
-    repository_emissions.add_save_emission(db, emission)
+    repository_emissions.add_save_emission(emission)
 
 
 @router.get("/emission/{emission_id}", tags=["emissions"])
@@ -21,7 +22,7 @@ async def read_emission(
     db: Session = Depends(get_db),
 ):
     repository_emissions = SqlAlchemyRepository(db)
-    emission = repository_emissions.get_one_emission(db, emission_id)
+    emission = repository_emissions.get_one_emission(emission_id)
     if emission is None:
         raise HTTPException(status_code=404, detail="Emission not found")
     return emission
@@ -34,7 +35,7 @@ async def read_experiment_emissions(
 ):
     repository_emissions = SqlAlchemyRepository(db)
     experiment_emissions = repository_emissions.get_emissions_from_experiment(
-        db, experiment_id
+        experiment_id
     )
     if experiment_emissions is None:
         raise HTTPException(
diff --git a/api/routers/experiments.py b/carbonserver/carbonserver/api/routers/experiments.py
similarity index 88%
rename from api/routers/experiments.py
rename to carbonserver/carbonserver/api/routers/experiments.py
index 579c630c8..fd98dd9e1 100644
--- a/api/routers/experiments.py
+++ b/carbonserver/carbonserver/api/routers/experiments.py
@@ -1,6 +1,5 @@
-# from infra.repository.repository_experiments import *
-from database.domain.schemas import ExperimentCreate
-from dependencies import get_db, get_token_header
+from carbonserver.database.schemas import ExperimentCreate
+from carbonserver.api.dependencies import get_db, get_token_header
 from fastapi import APIRouter, Depends, HTTPException, Path
 from sqlalchemy.orm import Session
 
diff --git a/api/routers/organizations.py b/carbonserver/carbonserver/api/routers/organizations.py
similarity index 91%
rename from api/routers/organizations.py
rename to carbonserver/carbonserver/api/routers/organizations.py
index e59442eff..f721ad63b 100644
--- a/api/routers/organizations.py
+++ b/carbonserver/carbonserver/api/routers/organizations.py
@@ -1,6 +1,6 @@
 # from database.Infra.SqlAlchemy import repository_organizations
-from database.Infra.Domain.schemas import OrganizationCreate
-from dependencies import get_db, get_token_header
+from carbonserver.database.schemas import OrganizationCreate
+from carbonserver.api.dependencies import get_db, get_token_header
 from fastapi import APIRouter, Depends, HTTPException, Path
 from sqlalchemy.orm import Session
 
diff --git a/api/routers/projects.py b/carbonserver/carbonserver/api/routers/projects.py
similarity index 77%
rename from api/routers/projects.py
rename to carbonserver/carbonserver/api/routers/projects.py
index e22117291..f868435e5 100644
--- a/api/routers/projects.py
+++ b/carbonserver/carbonserver/api/routers/projects.py
@@ -1,7 +1,6 @@
-from dependencies import get_db, get_token_header
-from domain.schemas import ProjectCreate
+from carbonserver.api.dependencies import get_db, get_token_header
+from carbonserver.database.schemas import ProjectCreate
 from fastapi import APIRouter, Depends, HTTPException, Path
-from infra.repository.repository_projects import SqlAlchemyRepository
 from sqlalchemy.orm import Session
 
 router = APIRouter(
@@ -15,9 +14,10 @@
 @router.put("/project", tags=["projects"])
 def add_project(project: ProjectCreate, db: Session = Depends(get_db)):
     # Remove next line when DB work
-    projects_temp_db.append(project.dict())
-    repository_projects = SqlAlchemyRepository(db)
-    repository_projects.add_save_emission(db, project)
+    # projects_temp_db.append(project.dict())
+    # repository_projects = SqlAlchemyRepository(db)
+    # repository_projects.add_save_emission(db, project)
+    raise HTTPException(status_code=501, detail="Not Implemented")
 
 
 @router.get("/project/{project_id}", tags=["projects"])
diff --git a/api/routers/teams.py b/carbonserver/carbonserver/api/routers/teams.py
similarity index 88%
rename from api/routers/teams.py
rename to carbonserver/carbonserver/api/routers/teams.py
index 5ddfd1d02..1b1ec487f 100644
--- a/api/routers/teams.py
+++ b/carbonserver/carbonserver/api/routers/teams.py
@@ -1,7 +1,7 @@
 from fastapi import APIRouter, Path, Depends, HTTPException
 from sqlalchemy.orm import Session
-from dependencies import get_token_header, get_db
-from domain.schemas import TeamCreate
+from carbonserver.api.dependencies import get_token_header, get_db
+from carbonserver.database.schemas import TeamCreate
 
 
 router = APIRouter(
@@ -34,7 +34,7 @@ async def read_project_teams(
     project_id: str = Path(..., title="The ID of the project to get")
 ):
     # project_teams = crud_teams.get_Project_from_Teams(team_id)
-    # # Remove next line when DB work
+    # Remove next line when DB work
     # project_teams = teams_temp_db
     # return project_teams
     raise HTTPException(status_code=501, detail="Not Implemented")
diff --git a/carbonserver/carbonserver/database/__init__.py b/carbonserver/carbonserver/database/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/api/database/database.py b/carbonserver/carbonserver/database/database.py
similarity index 100%
rename from api/database/database.py
rename to carbonserver/carbonserver/database/database.py
diff --git a/api/database/schemas.py b/carbonserver/carbonserver/database/schemas.py
similarity index 100%
rename from api/database/schemas.py
rename to carbonserver/carbonserver/database/schemas.py
diff --git a/carbonserver/code_carbon.db b/carbonserver/code_carbon.db
new file mode 100644
index 000000000..6702ad8f4
Binary files /dev/null and b/carbonserver/code_carbon.db differ
diff --git a/api/main.py b/carbonserver/main.py
similarity index 50%
rename from api/main.py
rename to carbonserver/main.py
index 475dca71d..e0e347ba5 100644
--- a/api/main.py
+++ b/carbonserver/main.py
@@ -1,8 +1,15 @@
-from database.database import engine
-from dependencies import get_query_token
-from domain import models
 from fastapi import Depends, FastAPI
-from routers import emissions  # , experiments, projects , organizations, teams
+from carbonserver.api.domain import models
+from carbonserver.api.routers import (
+    emissions,
+    experiments,
+    projects,
+    organizations,
+    teams,
+)
+from carbonserver.api.dependencies import get_query_token
+from carbonserver.database.database import engine
+
 
 # TODO : read https://fastapi.tiangolo.com/tutorial/bigger-applications/
 
@@ -14,10 +21,10 @@
 
 
 app.include_router(emissions.router)
-# app.include_router(experiments.router)
-# app.include_router(projects.router)
-# app.include_router(teams.router)
-# app.include_router(organisations.router)
+app.include_router(experiments.router)
+app.include_router(projects.router)
+app.include_router(teams.router)
+app.include_router(organizations.router)
 
 
 @app.get("/")
diff --git a/api/requirements.txt b/carbonserver/requirements-dev.txt
similarity index 100%
rename from api/requirements.txt
rename to carbonserver/requirements-dev.txt
diff --git a/carbonserver/setup.py b/carbonserver/setup.py
new file mode 100644
index 000000000..439438eb5
--- /dev/null
+++ b/carbonserver/setup.py
@@ -0,0 +1,32 @@
+import setuptools
+
+with open("README.md", "r") as f:
+    long_description = f.read()
+
+DEPENDENCIES = [
+    "uvicorn[standard]==0.13.4",
+    "fastapi==0.63.0",
+    "sqlalchemy",
+]
+
+TEST_DEPENDENCIES = ["mock", "pytest", "responses", "tox"]
+
+
+setuptools.setup(
+    name="carbonserver",
+    version="0.1.0",
+    author="BCG GAMMA, Comet.ml, Haverford College, MILA",
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    packages=setuptools.find_packages(
+        exclude=["*.tests", "*.tests.*", "tests.*", "tests"]
+    ),
+    install_requires=DEPENDENCIES,
+    tests_require=TEST_DEPENDENCIES,
+    classifiers=[
+        "Natural Language :: English",
+        "Programming Language :: Python :: 3.7",
+    ],
+    python_requires=">=3.7",
+    entry_points={"console_scripts": ["carbonserver = carbonserver:main"]},
+)
diff --git a/carbonserver/tox.ini b/carbonserver/tox.ini
new file mode 100644
index 000000000..68be0b46e
--- /dev/null
+++ b/carbonserver/tox.ini
@@ -0,0 +1,19 @@
+[tox]
+envlist = py37
+
+[testenv:api]
+deps =
+    pytest
+    -rrequirements-test.txt
+
+commands =
+    pip install -e .
+    python -m pytest
+
+
+recreate =
+    false
+
+[pytest]
+testpaths =
+    tests/infra


Test patch:
diff --git a/carbonserver/requirements-test.txt b/carbonserver/requirements-test.txt
new file mode 100644
index 000000000..ae4316484
--- /dev/null
+++ b/carbonserver/requirements-test.txt
@@ -0,0 +1,4 @@
+uvicorn[standard]
+fastapi
+sqlalchemy
+requests
diff --git a/carbonserver/tests/infra/repository/test_in_memory_emissions_repository.py b/carbonserver/tests/infra/repository/test_in_memory_emissions_repository.py
new file mode 100644
index 000000000..a06d1162c
--- /dev/null
+++ b/carbonserver/tests/infra/repository/test_in_memory_emissions_repository.py
@@ -0,0 +1,159 @@
+import pytest
+
+from carbonserver.database.schemas import EmissionCreate, Emission as SchemaEmission
+from carbonserver.api.domain.models import Emission as ModelEmission
+
+from carbonserver.api.infra.repository.repository_emissions import InMemoryRepository
+
+
+@pytest.fixture()
+def emissions_repository():
+    repo = InMemoryRepository()
+    return repo
+
+
+@pytest.fixture()
+def emission_fixture() -> EmissionCreate:
+    emission = EmissionCreate.parse_obj(
+        {
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "experiment_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "france",
+            "on_cloud": True,
+            "cloud_provider": "aws",
+            "cloud_region": "eu-west-1a",
+        }
+    )
+    return emission
+
+
+@pytest.fixture()
+def model_emission() -> ModelEmission:
+    model_emission = ModelEmission(
+        **{
+            "id": 1,
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "experiment_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "france",
+            "on_cloud": True,
+            "cloud_provider": "aws",
+            "cloud_region": "eu-west-1a",
+        }
+    )
+    return model_emission
+
+
+def test_emissions_repository_saves_correct_emission(
+    emissions_repository, model_emission
+):
+    emission = EmissionCreate.parse_obj(
+        {
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "experiment_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "france",
+            "on_cloud": True,
+            "cloud_provider": "aws",
+            "cloud_region": "eu-west-1a",
+        }
+    )
+    emissions_repository.add_save_emission(emission)
+    saved_emissions = emissions_repository.emissions
+
+    assert len(saved_emissions) == 1
+    assert saved_emissions[0].id == model_emission.id
+
+
+def test_emissions_repository_get_db_to_class_returns_correct_object_type(
+    emissions_repository, model_emission
+):
+    expected_schema_emission = SchemaEmission.parse_obj(
+        {
+            "id": 1,
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "experiment_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "france",
+            "on_cloud": True,
+            "cloud_provider": "aws",
+            "cloud_region": "eu-west-1a",
+        }
+    )
+
+    actual_schema_emission = emissions_repository.get_db_to_class(model_emission)
+
+    assert expected_schema_emission == actual_schema_emission
+
+
+def test_get_one_emission_returns_the_correct_emission_from_emission_id(
+    emissions_repository, emission_fixture
+):
+    emission_id = 1
+    expected_emission = SchemaEmission.parse_obj(
+        {
+            "id": 1,
+            "timestamp": "2021-04-04T08:43:00+02:00",
+            "experiment_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+            "duration": 98745,
+            "emissions": 1.548444,
+            "energy_consumed": 57.21874,
+            "country_name": "France",
+            "country_iso_code": "FRA",
+            "region": "france",
+            "on_cloud": True,
+            "cloud_provider": "aws",
+            "cloud_region": "eu-west-1a",
+        }
+    )
+    emissions_repository.add_save_emission(emission_fixture)
+
+    actual_emission = emissions_repository.get_one_emission(emission_id)
+
+    assert expected_emission == actual_emission
+
+
+def test_get_one_emission_returns_the_correct_emission_list_from_experiment_id(
+    emissions_repository, emission_fixture
+):
+    experiment_id = "40088f1a-d28e-4980-8d80-bf5600056a14"
+    expected_emissions = [
+        SchemaEmission.parse_obj(
+            {
+                "id": 1,
+                "timestamp": "2021-04-04T08:43:00+02:00",
+                "experiment_id": "40088f1a-d28e-4980-8d80-bf5600056a14",
+                "duration": 98745,
+                "emissions": 1.548444,
+                "energy_consumed": 57.21874,
+                "country_name": "France",
+                "country_iso_code": "FRA",
+                "region": "france",
+                "on_cloud": True,
+                "cloud_provider": "aws",
+                "cloud_region": "eu-west-1a",
+            }
+        )
+    ]
+    emissions_repository.add_save_emission(emission_fixture)
+
+    actual_emissions = emissions_repository.get_emissions_from_experiment(experiment_id)
+
+    assert expected_emissions == actual_emissions
diff --git a/carbonserver/tests/routers/test_emissions.py b/carbonserver/tests/routers/test_emissions.py
new file mode 100644
index 000000000..851df4b87
--- /dev/null
+++ b/carbonserver/tests/routers/test_emissions.py
@@ -0,0 +1,49 @@
+import pytest
+from fastapi import Depends, FastAPI
+from fastapi.testclient import TestClient
+
+from carbonserver.api.dependencies import get_query_token
+from carbonserver.api.routers import emissions
+
+"""
+Setting up a test client & a test server in specific states would be interesting.
+This setup may be extracted on a separate test container file to let developers ingest multiple / custom configurations.
+# Current state:
+ - No actual database is plugged on unit tests
+ - Client is emitting from same network as server
+ - This test class tests only API handling (logic is mocked)
+"""
+
+
+@pytest.fixture()
+def emissions_router():
+    app = FastAPI(dependencies=[Depends(get_query_token)])
+    app.include_router(emissions.router)
+    client = TestClient(app)
+    return app, client
+
+
+def test_post_emission_returns_success_with_correct_object(emissions_router):
+    app, client = emissions_router
+
+    response = client.post("/emission")
+
+    assert response.status_code == 405
+
+
+def test_read_emission_by_emission_id_returns_not_found_error(emissions_router):
+    app, client = emissions_router
+    emission_id = "1"
+    response = client.get("/emission/{emission_id}".format(emission_id=emission_id))
+
+    assert response.status_code == 404
+
+
+def test_read_emission_returns_experiment_id_returns_not_found_error(emissions_router):
+    app, client = emissions_router
+    experiment_id = "1"
+    response = client.get(
+        "/emissions/{experitment_id}".format(experitment_id=experiment_id)
+    )
+
+    assert response.status_code == 404