瀏覽代碼

Initial Commit

Oscar Alfredo Leiva Salomón 6 年之前
當前提交
d61b338a4b

+ 104 - 0
.gitignore

@@ -0,0 +1,104 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/

+ 6 - 0
.vscode/settings.json

@@ -0,0 +1,6 @@
+{
+    "python.pythonPath": "c:\\Users\\oleiva\\Documents\\Development\\ems-api\\.venv\\Scripts\\python.exe",
+    "python.linting.pylintEnabled": false,
+    "python.linting.pep8Enabled": true,
+    "python.linting.enabled": true
+}

+ 0 - 0
README.md


+ 13 - 0
app/__init__.py

@@ -0,0 +1,13 @@
+from flask_restful import Api
+from flask import Blueprint
+
+from app.main.resources.energy import TotalEnergyByDayApi, HourlyEnergyApi
+from app.main.resources.meters import MetersApi, MeterApi
+
+api_bp = Blueprint('api', __name__)
+api = Api(api_bp)
+
+api.add_resource(TotalEnergyByDayApi, '/energy/byDay')
+api.add_resource(HourlyEnergyApi, '/energy/hourly')
+api.add_resource(MetersApi, '/meters')
+api.add_resource(MeterApi, '/meter', '/meter/<meter_id>')

+ 15 - 0
app/main/__init__.py

@@ -0,0 +1,15 @@
+from flask import Flask
+from app.main.config import config_by_name
+from app.main import extensions
+
+
+def create_app(config_name):
+    app = Flask(__name__)
+    app.config.from_object(config_by_name[config_name])
+    register_extensions(app)
+    return app
+
+
+def register_extensions(app):
+    extensions.db.init_app(app)
+    extensions.cors.init_app(app)

+ 0 - 0
app/main/common/__init__.py


+ 56 - 0
app/main/common/decorators.py

@@ -0,0 +1,56 @@
+import functools
+import collections
+
+
+def standardize_api_response(function):
+    """ Creates a standardized response. This function should be used as a deco
+    rator.
+    :function: The function decorated should return a dict with one of
+    the keys  bellow:
+        success -> GET, 200
+        error -> Bad Request, 400
+        created -> POST, 201
+        updated -> PUT, 200
+        deleted -> DELETE, 200
+        no-data -> No Content, 204
+    :returns: json.dumps(response), staus code
+    """
+
+    available_result_keys = [
+        'success', 'error', 'created', 'updated', 'deleted', 'no-data']
+
+    status_code_and_descriptions = {
+        'success': (200, 'Successful Operation'),
+        'error': (400, 'Bad Request'),
+        'created': (201, 'Successfully created'),
+        'updated': (200, 'Successfully updated'),
+        'deleted': (200, 'Successfully deleted'),
+        'no-data': (204, '')
+    }
+
+    @functools.wraps(function)
+    def make_response(*args, **kwargs):
+
+        result = function(*args, **kwargs)
+
+        if not set(available_result_keys) & set(result):
+            raise ValueError('Invalid result key.')
+
+        status_code, description = status_code_and_descriptions[
+            next(iter(result.keys()))
+        ]
+
+        status_code = ('status_code', status_code)
+        description = (
+            ('description', description) if status_code[1] != 400 else
+            ('error', description)
+        )
+        data = (
+            ('data', next(iter(result.values()))) if status_code[1] != 204 else
+            ('data', '')
+        )
+
+        return collections.OrderedDict([
+            status_code, description, data]), status_code[-1]
+
+    return make_response

+ 9 - 0
app/main/common/utils.py

@@ -0,0 +1,9 @@
+import bson
+
+
+def is_a_valid_object_id(object_id):
+    """Verify if the value is valid as an object id.
+    :object_id: a string object
+    :returns: True or False
+    """
+    return bson.objectid.ObjectId.is_valid(object_id)

+ 51 - 0
app/main/config.py

@@ -0,0 +1,51 @@
+import os
+from dotenv import load_dotenv
+from apscheduler.jobstores.mongodb import MongoDBJobStore
+from pymongo import MongoClient
+
+load_dotenv()
+
+BASE_DIR = os.path.abspath(os.path.dirname(__file__))
+
+
+class Config:
+    SECRET_KEY = os.getenv('SECRET_KEY', 'my_precious_secret_key')
+    DEBUG = False
+
+
+class DevelopmentConfig(Config):
+
+    DEBUG = True
+    MONGODB_SETTINGS = {
+        "db": "Medidores",
+        "username": "mailScheduler",
+        "password": "pqowieuryt",
+        "host": "192.168.100.5",
+        "port": 27117
+    }
+
+
+class TestingConfig(Config):
+    DEBUG = True
+    TESTING = True
+    PRESERVE_CONTEXT_ON_EXCEPTION = False
+
+
+class ProductionConfig(Config):
+    DEBUG = False
+    MONGODB_SETTINGS = {
+        "db": os.getenv('MONGODB_DB'),
+        "username": os.getenv('MONGODB_USERNAME'),
+        "password": os.getenv('MONGODB_PASSWORD'),
+        "host": os.getenv('MONGODB_HOST'),
+        "port": int(os.getenv('MONGODB_PORT'))
+    }
+
+
+config_by_name = dict(
+    dev=DevelopmentConfig,
+    test=TestingConfig,
+    prod=ProductionConfig
+)
+
+key = Config.SECRET_KEY

+ 5 - 0
app/main/extensions.py

@@ -0,0 +1,5 @@
+from flask_mongoengine import MongoEngine
+from flask_cors import CORS
+
+db = MongoEngine()
+cors = CORS()

+ 0 - 0
app/main/model/__init__.py


+ 19 - 0
app/main/model/logs.py

@@ -0,0 +1,19 @@
+from app.main.extensions import db
+
+
+class Medicion(db.EmbeddedDocument):
+    """Embeded Document: Medicion"""
+    descripcion = db.StringField()
+    registro = db.IntField()
+    lectura = db.StringField()
+
+
+class Logs(db.Document):
+    """Log Model"""
+
+    meta = {'collection': 'meters_logs'}
+    serialNumber = db.StringField()
+    meterLogNumber = db.IntField()
+    logDate = db.StringField()
+    saveDate = db.StringField()
+    mediciones = db.ListField(db.EmbeddedDocumentField(Medicion))

+ 32 - 0
app/main/model/meters.py

@@ -0,0 +1,32 @@
+from app.main.extensions import db
+
+
+class Meter(db.Document):
+    """Meter Model"""
+
+    meta = {'collection': 'meters_installed'}
+    serialNumber = db.StringField()
+    name = db.StringField()
+    brand = db.StringField()
+    model = db.StringField()
+    phases = db.IntField()
+    active = db.BooleanField()
+    address = db.StringField()
+    gpsLat = db.FloatField()
+    gpsLong = db.FloatField()
+    installedDate = db.StringField()
+
+    def to_json(self):
+        return {
+            'id': str(self.id),
+            'serialNumber': self.serialNumber,
+            'name': self.name,
+            'brand': self.brand,
+            'model': self.model,
+            'phases': self.phases,
+            'active:': self.active,
+            'address': self.address,
+            'gpsLat': self.gpsLat,
+            'gpsLong': self.gpsLong,
+            'installedDate': self.installedDate
+        }

+ 0 - 0
app/main/resources/__init__.py


+ 53 - 0
app/main/resources/energy.py

@@ -0,0 +1,53 @@
+from flask_restful import Resource, reqparse
+from app.main.service.energy_service import (
+    get_hourly_wh_by_day, get_total_wh_by_day)
+
+from app.main.common.decorators import standardize_api_response
+
+
+class TotalEnergyByDayApi(Resource):
+
+    @standardize_api_response
+    def get(self):
+
+        parser = reqparse.RequestParser(bundle_errors=True)
+        parser.add_argument(
+            'serial_number', help='Numero serie del medidor', required=True)
+        parser.add_argument('log_number', help='Número de Log', required=True)
+        parser.add_argument(
+            'start_date', help='Fecha de inicio la consulta', required=True)
+        parser.add_argument('end_date', help='Fecha de fin de la consulta')
+        args = parser.parse_args()
+        if not args:
+            return {'no-data': ''}
+        serial_number = args['serial_number']
+        log_number = args['log_number']
+        start_date = args['start_date']
+        end_date = args['end_date']
+
+        return get_total_wh_by_day(
+            serial_number, log_number, start_date, end_date)
+
+
+class HourlyEnergyApi(Resource):
+
+    @standardize_api_response
+    def get(self):
+
+        parser = reqparse.RequestParser(bundle_errors=True)
+        parser.add_argument(
+            'serial_number', help='Numero serie del medidor', required=True)
+        parser.add_argument('log_number', help='Número de Log', required=True)
+        parser.add_argument(
+            'start_date', help='Fecha de inicio la consulta', required=True)
+        parser.add_argument('end_date', help='Fecha de fin de la consulta')
+        args = parser.parse_args()
+        if not args:
+            return {'no-data': ''}
+        serial_number = args['serial_number']
+        log_number = args['log_number']
+        start_date = args['start_date']
+        end_date = args['end_date']
+
+        return get_hourly_wh_by_day(
+            serial_number, log_number, start_date, end_date)

+ 94 - 0
app/main/resources/meters.py

@@ -0,0 +1,94 @@
+from flask_restful import Resource, reqparse
+from app.main.service.meters_service import (
+    get_meters, create_or_update_meters, delete_meter)
+from app.main.common.decorators import standardize_api_response
+from app.main.common.utils import is_a_valid_object_id
+
+
+def post_put_parser():
+    """Request parser for HTTP POST or PUT.
+    :returns: flask.ext.restful.reqparse.RequestParser object
+    """
+
+    parse = reqparse.RequestParser()
+    parse.add_argument(
+        'serialNumber', type=str, location='json', required=True)
+    parse.add_argument(
+        'name', type=str, location='json', required=True)
+    parse.add_argument(
+        'brand', type=str, location='json', required=True)
+    parse.add_argument(
+        'model', type=str, location='json', required=True)
+    parse.add_argument(
+        'phases', type=int, location='json')
+    parse.add_argument(
+        'active', type=bool, location='json')
+    parse.add_argument(
+        'address', type=str, location='json')
+    parse.add_argument(
+        'gpsLat', type=float, location='json')
+    parse.add_argument(
+        'gpsLong', type=float, location='json')
+    parse.add_argument(
+        'installedDate', type=str, location='json')
+
+    return parse
+
+
+class MetersApi(Resource):
+
+    @standardize_api_response
+    def get(self):
+        return get_meters()
+
+    @standardize_api_response
+    def post(self):
+
+        parse = post_put_parser()
+        args = parse.parse_args()
+
+        serialNumber = args['serialNumber']
+        name = args['name']
+        brand = args['brand']
+        model = args['model']
+        phases = args['phases']
+        active = args['active']
+        address = args['address']
+        gpsLat = args['gpsLat']
+        gpsLong = args['gpsLong']
+        installedDate = args['installedDate']
+
+        return create_or_update_meters(
+            serialNumber, name, brand, model, phases,
+            active, address, gpsLat, gpsLong, installedDate)
+
+
+class MeterApi(Resource):
+
+    @standardize_api_response
+    def put(self):
+        parse = post_put_parser()
+        args = parse.parse_args()
+
+        serialNumber = args['serialNumber']
+        name = args['name']
+        brand = args['brand']
+        model = args['model']
+        phases = args['phases']
+        active = args['active']
+        address = args['address']
+        gpsLat = args['gpsLat']
+        gpsLong = args['gpsLong']
+        installedDate = args['installedDate']
+
+        return create_or_update_meters(
+            serialNumber, name, brand, model, phases,
+            active, address, gpsLat, gpsLong, installedDate)
+
+    @standardize_api_response
+    def delete(self, meter_id):
+
+        if not is_a_valid_object_id(meter_id):
+            return {'error': 'Invalid meter id.'}
+
+        return delete_meter(meter_id)

+ 0 - 0
app/main/service/__init__.py


+ 126 - 0
app/main/service/energy_service.py

@@ -0,0 +1,126 @@
+from app.main.model.logs import Logs
+from datetime import datetime, timedelta
+
+
+def _make_pipeline(serial_number, log_number,
+                   register, start_date, end_date, aggregate_by):
+    pipeline = [
+        {'$project':
+            {
+                'serialNumber': 1,
+                'meterLogNumber': 1,
+                'mediciones': 1,
+                'logDate': 1,
+                'hour': {
+                    '$hour': {
+                        '$dateFromString':  {'dateString': '$logDate'}
+                    }
+                },
+                'day': {
+                    '$dayOfMonth': {
+                        '$dateFromString':  {'dateString': '$logDate'}
+                    }
+                },
+                'month': {
+                    '$month': {
+                        '$dateFromString':  {'dateString': '$logDate'}
+                    }
+                },
+                'year': {
+                    '$year': {
+                        '$dateFromString':  {'dateString': '$logDate'}
+                    }
+                },
+            }
+         },
+        {'$unwind': '$mediciones'},
+        {'$match': {
+            'serialNumber': serial_number,
+            'meterLogNumber': int(log_number),
+            "mediciones.registro": register,
+            'logDate': {'$gte': start_date, '$lt': end_date}}},
+        {'$group':
+            {
+                '_id': aggregate_by,
+                'date': {'$min': '$logDate'},
+                'total_energy': {'$sum': {'$toInt': "$mediciones.lectura"}}
+            }
+         },
+        {'$sort': {'date': -1}},
+        {'$project': {'date': 1, 'total_energy': 1, '_id': 0}}
+    ]
+
+    return pipeline
+
+
+def get_total_wh_by_day(serial_number, log_number, start_date, end_date=None):
+    """Get total energy Wh by day
+
+    :serial_number: string object
+    :log_number: string object
+    :date: string object
+
+    """
+    log = Logs.objects(serialNumber=serial_number,
+                       meterLogNumber=log_number,
+                       logDate=start_date + " 00:00:00")
+
+    if not log:
+        return {
+            'error': 'No existe informacion para el medidor, log o fecha'
+        }
+
+    register = 4999
+
+    aggregate_by = {
+        'hour': None,
+        'day': '$day',
+        'month': None,
+        'year': None
+    }
+    if end_date is None:
+        date_dt = datetime.strptime(start_date, "%Y-%m-%d")
+        delta = timedelta(days=1)
+        end_date_dt = date_dt + delta
+        end_date = end_date_dt.strftime("%Y-%m-%d")
+
+    pipeline = _make_pipeline(
+        serial_number, log_number, register, start_date,
+        end_date, aggregate_by)
+    return {'success': [log for log in Logs.objects.aggregate(*pipeline)]}
+
+
+def get_hourly_wh_by_day(serial_number, log_number, start_date, end_date):
+    """Get hourly energy Wh by day
+
+    :serial_number: string object
+    :log_number: string object
+    :date: string object
+
+    """
+    log = Logs.objects(serialNumber=serial_number,
+                       meterLogNumber=log_number,
+                       logDate=start_date + " 00:00:00")
+
+    if not log:
+        return {'error': 'No existe información para el medidor, log o fecha'}
+
+    register = 4999
+
+    aggregate_by = {
+        'hour': '$hour',
+        'day': '$day',
+        'month': None,
+        'year': None
+    }
+
+    if end_date is None:
+        date_dt = datetime.strptime(start_date, "%Y-%m-%d")
+        delta = timedelta(days=1)
+        end_date_dt = date_dt + delta
+        end_date = end_date_dt.strftime("%Y-%m-%d")
+
+    pipeline = _make_pipeline(
+        serial_number, log_number, register, start_date,
+        end_date, aggregate_by)
+    return {'success': [log for log in Logs.objects.aggregate(*pipeline)]}

+ 52 - 0
app/main/service/meters_service.py

@@ -0,0 +1,52 @@
+from app.main.model.meters import Meter
+
+
+def get_meters(serialNumber=None):
+    query = {} if not serialNumber else {'serialNumber': serialNumber}
+    meters = Meter.objects(**query).all()
+
+    if not meters:
+        return {'no-data': ''}
+
+    return {'success': [meter.to_json() for meter in meters]}
+
+
+def create_or_update_meters(serialNumber, name, brand, model, phases, active,
+                            address, gpsLat, gpsLong, installedDate,
+                            meter_id=None):
+
+    try:
+        query = {'id': 'meter_id'} if meter_id else {
+            'serialNumber': serialNumber}
+        result = Meter.objects(**query).modify(
+            upsert=True,
+            set__serialNumber=serialNumber,
+            set__name=name,
+            set__model=model,
+            set__phases=phases,
+            set__active=active,
+            set__address=address,
+            set__gpsLat=gpsLat,
+            set__gpsLong=gpsLong,
+            set__installedDate=installedDate
+        )
+    except Exception as e:
+        return {'error': 'Error during the operation: {}'.format(e)}
+
+    print(result)
+    if result is None:
+        return {'created': 'Created the meter {!r}.'.format(serialNumber)}
+
+    return {'updated': 'Updated the meter {!r}.'.format(serialNumber)}
+
+
+def delete_meter(meter_id):
+
+    meter = Meter.objects(id=meter_id).first()
+
+    if not meter:
+        return {'error': 'Invalid meter id.'}
+
+    meter.delete()
+
+    return {'deleted': 'Meter deleted.'}

+ 22 - 0
manage.py

@@ -0,0 +1,22 @@
+import os
+from flask_script import Manager
+
+from app import api_bp
+from app.main import create_app
+
+
+app = create_app(os.getenv('BOILERPLATE_ENV') or 'dev')
+app.register_blueprint(api_bp, url_prefix='/api')
+
+app.app_context().push()
+
+manager = Manager(app)
+
+
+@manager.command
+def run():
+    app.run()
+
+
+if __name__ == '__main__':
+    manager.run()

+ 25 - 0
requirements.txt

@@ -0,0 +1,25 @@
+aniso8601==7.0.0
+APScheduler==3.6.1
+autopep8==1.4.4
+Click==7.0
+Flask==1.1.1
+Flask-APScheduler==1.11.0
+Flask-Cors==3.0.8
+flask-mongoengine==0.9.5
+Flask-RESTful==0.3.7
+Flask-Script==2.0.6
+Flask-WTF==0.14.2
+itsdangerous==1.1.0
+Jinja2==2.10.1
+MarkupSafe==1.1.1
+mongoengine==0.18.2
+pep8==1.7.1
+pycodestyle==2.5.0
+pymongo==3.8.0
+python-dateutil==2.8.0
+python-dotenv==0.10.3
+pytz==2019.1
+six==1.12.0
+tzlocal==2.0.0
+Werkzeug==0.15.5
+WTForms==2.2.1