Added dockerfile and yaml file 95/4195/1
authorkhemendra kumar <khemendra.kumar@huawei.com>
Tue, 30 Mar 2021 14:04:07 +0000 (19:34 +0530)
committerkhemendra kumar <khemendra.kumar@huawei.com>
Tue, 30 Mar 2021 14:04:07 +0000 (19:34 +0530)
Signed-off-by: khemendra kumar <khemendra.kumar@huawei.com>
Change-Id: I48b30f0e1a0c73f2dcb9180862502c148200382d

example-apps/ROBO/retail_app/Dockerfile
example-apps/ROBO/retail_app/Inventory-be-k8s.yaml [new file with mode: 0644]
example-apps/ROBO/retail_app/config.py
example-apps/ROBO/retail_app/configs/start.sh
example-apps/ROBO/retail_app/inventry/retail_app.py
example-apps/ROBO/retail_app/requirements.txt

index 3d76af3..cad2cf5 100644 (file)
 
 # Prepare stage for multistage image build
 ## START OF STAGE0 ##
-FROM python:3.6-slim-stretch
-
+# FROM python:3.6-slim-stretch
+# FROM python:3.9
+# FROM python:3.9-slim-buster
 # # CREATE APP USER ##
+
+FROM python:2.7
+
 # Set umask
 RUN sed -i "s|umask 022|umask 027|g" /etc/profile
 
@@ -27,7 +31,7 @@ RUN mkdir -p /usr/app
 RUN mkdir -p /usr/app/bin
 RUN mkdir -p /usr/app/inventry
 RUN mkdir -p /usr/app/test/resources
-RUN mkdir -p /usr/app/images
+RUN mkdir -p /usr/app/images_result
 
 # Set the home directory to our app user's home.
 ENV APP_HOME=/usr/app
@@ -36,6 +40,7 @@ ENV GID=166
 ENV USER_NAME=eguser
 ENV GROUP_NAME=eggroup
 ENV ENV="/etc/profile"
+ENV PYTHONUNBUFFERED=0
 
 # Create an app user so our program doesn't run as root.
 RUN apt-get -y update &&\
@@ -51,7 +56,6 @@ RUN apt-get install -y --fix-missing \
     curl \
     graphicsmagick \
     libgraphicsmagick1-dev \
-    libatlas-dev \
     libavcodec-dev \
     libavformat-dev \
     libgtk2.0-dev \
@@ -59,10 +63,9 @@ RUN apt-get install -y --fix-missing \
     liblapack-dev \
     libswscale-dev \
     pkg-config \
-    python3-dev \
-    python3-numpy \
     software-properties-common \
     zip \
+    imagemagick \
     && apt-get clean && rm -rf /tmp/* /var/tmp/*
 
 # Set the working directory.
@@ -71,9 +74,9 @@ WORKDIR $APP_HOME
 # Copy the application & scripts
 COPY config.py requirements.txt run.py $APP_HOME/
 COPY inventry $APP_HOME/inventry/
+
 COPY test $APP_HOME/test/
 COPY test/resources $APP_HOME/test/resources/
-#COPY configs/*.sh $APP_HOME/bin
 COPY configs/start.sh $APP_HOME/bin
 
 RUN chmod 750 $APP_HOME &&\
@@ -83,7 +86,7 @@ RUN chmod 750 $APP_HOME &&\
     chown -R $USER_NAME:$GROUP_NAME $APP_HOME
 
 # Exposed port
-EXPOSE 9996
+EXPOSE 9995
 
 # Change to the app user.
 USER $USER_NAME
diff --git a/example-apps/ROBO/retail_app/Inventory-be-k8s.yaml b/example-apps/ROBO/retail_app/Inventory-be-k8s.yaml
new file mode 100644 (file)
index 0000000..9c05489
--- /dev/null
@@ -0,0 +1,58 @@
+# Copyright 2020 Huawei Technologies Co., Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: inventory-be
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: inventory
+  template:
+    metadata:
+      labels:
+        app: inventory
+    spec:
+      containers:
+        - name: inventory-be
+          image: ealtedge/inventory-be
+          env:
+            - name: INFLUXDB_IP
+              value: "159.138.33.54"
+            - name: INFLUXDB_PORT
+              value: "30944"
+            - name: OBJ_DET_SER_IP
+              value: "159.138.33.54"
+            - name: OBJ_DET_SER_PORT
+              value: "30093"
+          ports:
+            - containerPort: 9995
+
+
+# backup-be serice
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: inventory-be
+spec:
+  selector:
+    app: inventory
+  type: NodePort
+  ports:
+    - port: 9995
+      name: service-port
+      nodePort: 30092
index 29c2ebb..896e5a0 100644 (file)
@@ -17,8 +17,9 @@
 import os
 
 # [Server Configurations]
-server_port = 9999
-server_address = os.environ.get('LISTEN_IP')
+server_port = 9995
+# server_address = os.environ.get('LISTEN_IP')
+server_address = "0.0.0.0"
 
 # [InfluxDB config]
 IPADDRESS = os.environ.get('INFLUXDB_IP')
@@ -39,7 +40,15 @@ ssl_server_name = os.environ.get('SERVER_NAME', "ealtedge")
 
 # [Service Configurations]
 api_gateway = os.environ.get("API_GATEWAY", "apigw.mep.org")
-Obj_Det = os.environ.get("OBJ_DETECTION", "objdetection")
+# Obj_Det_SER_IP = os.environ.get("OBJ_DET_SER_IP", "localhost")
+
+Obj_Det_SER_IP = os.environ.get("OBJ_DET_SER_IP")
+# Obj_Det_SER_IP = os.environ.get("OBJ_DET_SER_IP")
+Obj_Det_SER_PORT = os.environ.get("OBJ_DET_SER_PORT")
+# Obj_Det_SER_PORT = os.environ.get("OBJ_DET_SER_PORT")
+Obj_Det = os.environ.get("OBJ_DETECTION", "mep/v1/obj_detection")
 
 # [Constants]
-detection_url = "http://" + api_gateway + "/" + Obj_Det
+# detection_url = "http://" + api_gateway + "/" + Obj_Det
+detection_url = "http://" + Obj_Det_SER_IP + ":" + Obj_Det_SER_PORT + "/" + \
+                                           Obj_Det + "/"
index fd38b71..23796c6 100644 (file)
@@ -41,66 +41,7 @@ validate_name() {
   return 0
 }
 
-# validates whether file exist
-validate_file_exists() {
-  file_path="$1"
-
-  # checks variable is unset
-  if [ -z "$file_path" ]; then
-    echo "file path variable is not set"
-    return 1
-  fi
-
-  # checks if file exists
-  if [ ! -f "$file_path" ]; then
-    echo "file does not exist"
-    return 1
-  fi
-
-  return 0
-}
-
-validate_ip "$LISTEN_IP"
-valid_listen_ip="$?"
-if [ ! "$valid_listen_ip" -eq "0" ]; then
-  echo "invalid ip address for listen ip"
-  exit 1
-fi
-
-if [ ! -z "$SERVER_NAME" ]; then
-  validate_name "$SERVER_NAME"
-  valid_name="$?"
-  if [ ! "$valid_name" -eq "0" ]; then
-    echo "invalid ssl server name"
-    exit 1
-  fi
-fi
-
-# ssl parameters validation
-validate_file_exists "/usr/app/ssl/server_tls.crt"
-valid_ssl_server_cert="$?"
-if [ ! "$valid_ssl_server_cert" -eq "0" ]; then
-  echo "invalid ssl server certificate"
-  exit 1
-fi
-
-# ssl parameters validation
-validate_file_exists "/usr/app/ssl/server_tls.key"
-valid_ssl_server_key="$?"
-if [ ! "$valid_ssl_server_key" -eq "0" ]; then
-  echo "invalid ssl server key"
-  exit 1
-fi
-
-# ssl parameters validation
-validate_file_exists "/usr/app/ssl/ca.crt"
-valid_ssl_ca_crt="$?"
-if [ ! "$valid_ssl_ca_crt" -eq "0" ]; then
-  echo "invalid ssl ca cert"
-  exit 1
-fi
-
 echo "Running Retail App"
 umask 0027
 cd /usr/app || exit
-python run.py
\ No newline at end of file
+python -u run.py
\ No newline at end of file
index e3f98d3..22f7db0 100644 (file)
@@ -18,21 +18,23 @@ import config
 from flask_sslify import SSLify
 from flask import Flask, request, jsonify, Response
 from flask_cors import CORS
-# from camera_driver.capture_frame import VideoCamera, VideoFile
-# from capture_frame import VideoCamera, VideoFile
-# from influxdb import InfluxDBClient
+from influxdb import InfluxDBClient
 import json
-import time
 import requests
 import os
 import cv2
-
+import os.path
+from os import path
+import base64
+import time
+import sys
 
 app = Flask(__name__)
 CORS(app)
 sslify = SSLify(app)
 app.config['JSON_AS_ASCII'] = False
-app.config['UPLOAD_PATH'] = '/usr/app/images/'
+app.config['UPLOAD_PATH'] = '/usr/app/images_result/'
+app.config['VIDEO_PATH'] = '/usr/app/test/resources/'
 app.config['supports_credentials'] = True
 app.config['CORS_SUPPORTS_CREDENTIALS'] = True
 app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
@@ -42,31 +44,37 @@ count = 0
 listOfMsgs = []
 listOfCameras = []
 listOfVideos = []
-mock_func = 1
 
 
 class inventory_info:
     """
     Store the data and manage multiple input video feeds
     """
-    def __init__(self, current_count=0, total_count=0, time=0):
+    def __init__(self, status="Needs Filling", time=0):
         self.type = "Shelf_INV1"
         self.labels = "Bottles"
-        self.current_count = current_count
-        self.total_count = total_count
+        self.status = status
+        self.currentCount = 1
+        self.maxCount = 5
         self.time = time
 
-    def setcurrentcount(self, current_count):
-        self.current_count = current_count
+    def setstatus(self, status):
+        self.status = status
 
-    def settotalcount(self, total_count):
-        self.total_count = total_count
+    def getstatus(self):
+        return self.status
 
-    def getcurrentcount(self):
-        return self.current_count
+    def setcurrentCount(self, count):
+        self.currentCount = count
 
-    def gettotalcount(self):
-        return self.total_count
+    def getcurrentCount(self):
+        return self.currentCount
+
+    def setmaxCount(self, count):
+        self.maxCount = count
+
+    def getmaxCount(self):
+        return self.maxCount
 
     def setlabel(self, labels):
         self.labels = labels
@@ -83,7 +91,6 @@ class inventory_info:
 
 # temporary copied capture_frame file to this due to docker issue for module
 # import
-
 class VideoCamera(object):
     """
     opneCV to capture frame from a camera
@@ -107,7 +114,7 @@ class VideoFile(object):
     opneCV to capture frame from a video stream
     """
     def __init__(self, video_name):
-        self.video = cv2.VideoCapture("./test/resources/" + video_name)
+        self.video = cv2.VideoCapture(app.config['VIDEO_PATH'] + video_name)
 
     def delete(self):
         self.video.release()
@@ -120,27 +127,6 @@ class VideoFile(object):
         return success, image
 
 
-def store_data(inventory_info):
-    """
-    store time series data in influx db
-    """
-    # TODO config, schema table, DB, fill data set
-    create_database()
-    store_info_db(inventory_info)
-
-
-def mock_table(inven_info):
-    current_count = 3
-    labels = "Bottles"
-    total_count = 6
-    inven_info.setcurrentcount(current_count)
-    inven_info.settotalcount(total_count)
-    inven_info.setlabel(labels)
-    inven_info.utime = time.time()
-    # store_data(inven_info)
-    local_store(inven_info)
-
-
 def shelf_inventory(video_capture, camera_info, true=None):
     """
     shelf_inventory
@@ -148,48 +134,68 @@ def shelf_inventory(video_capture, camera_info, true=None):
     global count
     global mock_func
 
-    labels = "bottles"
+    labels = "Bottles"
+    count_val = 'ObjCount'
     process_this_frame = 0
-    if mock_func == 1:
-        inven_info = inventory_info()
-        mock_table(inven_info)
-    else:
-        while True:
-            success, frame = video_capture.get_frame()
-            if not success:
-                break
-            if process_this_frame == 0:
-                url = config.detection_url + "/v1/obj_detection/detect"
-                # info1 = cv2.imencode(".jpg", rgb_small_frame)[1].tobytes()
-                data = json.loads(requests.post
-                                  (url, data=frame,
-                                   verify=config.ssl_cacertpath).text)
-        inven_info = inventory_info()
-        current_count = data[count]
-        labels = data[labels]
-        total_count = inven_info.current_count + inven_info.total_count
-        inven_info.setcurrentcount(current_count)
-        inven_info.settotalcount(total_count)
-        inven_info.setlabel(labels)
-        inven_info.utime = time.time()
-        # store_data(inven_info)
-        local_store(inven_info)
-
-
-def local_store(inven_info):
+    i = 0
+    url = config.detection_url + "detect"
+    url_get = config.detection_url + "image"
+
+    while True:
+        success, frame = video_capture.get_frame()
+        if not success:
+            print('read frame from file is failed')
+            break
+
+        i = i+1
+        if i < 10:
+            continue
+
+        i = 0
+
+        if process_this_frame == 0:
+            imencoded = cv2.imencode(".jpg", frame)[1]
+            file = {'file': (
+                'image.jpg', imencoded.tostring(), 'image/jpeg',
+                {'Expires': '0'})}
+            res = requests.post(url, files=file)
+            data = json.loads(res.text)
+
+            # get image
+            response = requests.get(url_get)
+
+            file = open(app.config['UPLOAD_PATH'] + "sample_image.jpg", "wb")
+            file.write(response.content)
+            file.close()
+
+            inven_info = inventory_info()
+            current_count = data[count_val]
+            if (current_count >= 3):
+                status = "Mostly Filled"
+            elif (current_count == 2):
+                status = "Partially Filled"
+            else:
+                status = "Needs Filling"
+
+            inven_info.setlabel(labels)
+            inven_info.setstatus(status)
+            inven_info.setcurrentCount(current_count)
+            time_sec = time.time()
+            local_time = time.ctime(time_sec)
+            inven_info.time = local_time
+            store_info_db(inven_info)
+            time.sleep(0.30)
+
+
+def db_drop_table(inven_info):
     """
-    store "shelf" data to array
+    cleanup measrurment before new trigger
 
-    :param inven_info: Inventry object
+    :param inven_info: inven_info object
     :return: None
     """
-    if len(listOfMsgs) >= 100:
-        listOfMsgs.pop()
-    newdict = {"shelfName": inven_info.type, "ObjType": inven_info.labels,
-               "currentCount": inven_info.current_count,
-               "totalCount": inven_info.total_count,
-               "time": time.time()}
-    listOfMsgs.insert(0, newdict)
+    global db_client
+    db_client.drop_measurement(inven_info.type)
 
 
 def store_info_db(inven_info):
@@ -208,13 +214,45 @@ def store_info_db(inven_info):
             },
             "fields": {
                 "time": inven_info.time,
-                "Current Count": inven_info.current_count,
-                "Total Count": inven_info.total_count,
+                "status": inven_info.status,
+                "currentCount": inven_info.currentCount,
+                "maxCount": inven_info.maxCount,
             }
         }]
     db_client.write_points(json_body)
 
 
+def retrive_info_db():
+    """
+    Send "shelf" data to InfluxDB
+
+    :param inven_info: Inventry object
+    :return: None
+    """
+    global db_client
+
+    # get data last n data points from DB
+    result = db_client.query('select * from Shelf_INV1 order by desc limit '
+                             '1;')
+
+    # Get points and iterate over each record
+    points = result.get_points(tags={"object": "bottles"})
+
+    # clear the msg list
+    # listOfMsgs.clear()
+    del listOfMsgs[:]
+
+    # iterate points and fill the records and insert to list
+    for point in points:
+        print("status: %s,Time: %s" % (point['status'], point['time']))
+        newdict = {"shelfName": 'Shelf_INV1', "ObjType": "bottles",
+                   "status": point['status'],
+                   "currentCount": point['currentCount'],
+                   "maxCount": point['maxCount'],
+                   "time": point['time']}
+        listOfMsgs.insert(0, newdict)
+
+
 def create_database():
     """
     Connect to InfluxDB and create the database
@@ -222,10 +260,10 @@ def create_database():
     :return: None
     """
     global db_client
-#    proxy = {"http": "http://{}:{}".format(config.IPADDRESS, config.PORT)}
-#    db_client = InfluxDBClient(host=config.IPADDRESS, port=config.PORT,
-#    proxies=proxy, database=config.DATABASE_NAME)
-#    db_client.create_database(config.DATABASE_NAME)
+    proxy = {"http": "http://{}:{}".format(config.IPADDRESS, config.PORT)}
+    db_client = InfluxDBClient(host=config.IPADDRESS, port=config.PORT,
+                               proxies=proxy, database=config.DATABASE_NAME)
+    db_client.create_database(config.DATABASE_NAME)
 
 
 @app.route('/v1/inventry/table', methods=['GET'])
@@ -235,17 +273,32 @@ def inventry_table():
 
     :return: inventry table
     """
-    return jsonify(listOfMsgs)
+    retrive_info_db()
+    table = {"InventryData": listOfMsgs}
+    return jsonify(table)
 
 
 @app.route('/v1/inventry/image', methods=['GET'])
 def detected_image():
     """
-    return inventry table
+    detect images with imposed
 
-    :return: inventry table
+    :return: result image
     """
-    return jsonify(listOfMsgs)
+    detected_image = app.config['UPLOAD_PATH'] + 'sample_image.jpg'
+    print('file exits:', str(path.exists(detected_image)))
+    status = str(path.exists(detected_image))
+    if status == 'True':
+        # as base64 string
+        with open(detected_image, "rb") as img_file:
+            jpeg_bin = base64.b64encode(img_file.read())
+
+        response = {'image': jpeg_bin}
+        return jsonify(response)
+    else:
+        response = {'image': 'null'}
+        print('file not exist')
+        return jsonify(response)
 
 
 def allowed_videofile(filename):
@@ -262,50 +315,96 @@ def upload_video():
     app.logger.info("Received message from ClientIP [" + request.remote_addr
                     + "] Operation [" + request.method + "]" +
                     " Resource [" + request.url + "]")
+    print("videpath:" + app.config['VIDEO_PATH'])
     if 'file' in request.files:
         files = request.files.getlist("file")
         for file in files:
             if allowed_videofile(file.filename):
                 file.save(os.path.join(app.config['VIDEO_PATH'],
                                        file.filename))
+                print('file path is:', app.config['VIDEO_PATH']
+                      + file.filename)
             else:
                 raise IOError('video format error')
-    return Response("success")
+                msg = {"responce": "failure"}
+                return jsonify(msg)
+    msg = {"responce": "success"}
+    return jsonify(msg)
+
+
+def hash_func(camera_info):
+    hash_string = camera_info["cameraNumber"] + \
+                  camera_info["cameraLocation"] + \
+                  camera_info["rtspUrl"]
+    # readable_hash = hashlib.sha256(str(hash_string).encode(
+    # 'utf-8')).hexdigest()
+    readable_hash = hash(hash_string)
+    if readable_hash < 0:
+        readable_hash += sys.maxsize
+    print(readable_hash)
+    return readable_hash
 
 
 @app.route('/v1/monitor/cameras', methods=['POST'])
 def add_camera():
-    camera_info = request.json
+    camera_detail = request.json
     app.logger.info("Received message from ClientIP [" + request.remote_addr
                     + "] Operation [" + request.method + "]" +
                     " Resource [" + request.url + "]")
-    camera_info = {"name": camera_info["name"],
-                   "rtspurl": camera_info["rtspurl"],
-                   "location": camera_info["location"]}
+    camera_id = hash_func(camera_detail)
+    camera_id = str(camera_id)
+    for camera_info in listOfCameras:
+        if camera_id == camera_info["cameraID"]:
+            msg = {"responce": "failure"}
+            return jsonify(msg)
+            break
+    camera_info = {"cameraID": camera_id,
+                   "cameraNumber": camera_detail["cameraNumber"],
+                   "rtspUrl": camera_detail["rtspUrl"],
+                   "cameraLocation": camera_detail["cameraLocation"]}
     listOfCameras.append(camera_info)
-    return Response("success")
+    msg = {"responce": "success"}
+    return jsonify(msg)
 
 
-@app.route('/v1/monitor/cameras/<name>/<rtspurl>/<location>', methods=['GET'])
-def get_camera(name, rtspurl, location):
+@app.route('/v1/monitor/cameras/<cameraID>', methods=['GET'])
+def get_camera(cameraID):
     """
     register camera with location
     """
     app.logger.info("Received message from ClientIP [" + request.remote_addr
                     + "] Operation [" + request.method + "]" +
                     " Resource [" + request.url + "]")
-    camera_info = {"name": name, "rtspurl": rtspurl, "location": location}
-    if "mp4" in camera_info["rtspurl"]:
-        video_file = VideoFile(camera_info["rtspurl"])
-        video_dict = {camera_info["name"]: video_file}
+    valid_id = 0
+    for camera_info in listOfCameras:
+        # cameraID = int(cameraID)
+        if cameraID == camera_info["cameraID"]:
+            valid_id = 1
+            break
+
+    if valid_id == 0:
+        app.logger.info("camera ID is not valid")
+        msg = {"responce": "failure"}
+        return jsonify(msg)
+
+    if "mp4" in camera_info["rtspUrl"]:
+        video_file = VideoFile(camera_info["rtspUrl"])
+        video_dict = {camera_info["cameraNumber"]: video_file}
         listOfVideos.append(video_dict)
-        return Response(shelf_inventory(video_file, camera_info["name"]),
-                        mimetype='multipart/x-mixed-replace; boundary=frame')
+        # return Response(shelf_inventory(video_file, camera_info[
+        # "cameraNumber"]),
+        #                mimetype='multipart/x-mixed-replace; boundary=frame')
+        shelf_inventory(video_file, camera_info["cameraNumber"])
+        app.logger.info("get_camera: Added json")
+        msg = {"responce": "success"}
+        return jsonify(msg)
+
     else:
-        video_file = VideoCamera(camera_info["rtspurl"])
-        video_dict = {camera_info["name"]: video_file}
+        video_file = VideoCamera(camera_info["rtspUrl"])
+        video_dict = {camera_info["cameraNumber"]: video_file}
         listOfVideos.append(video_dict)
-        return Response(shelf_inventory(video_file, camera_info["name"]),
+        return Response(shelf_inventory(video_file,
+                        camera_info["cameraNumber"]),
                         mimetype='multipart/x-mixed-replace; boundary=frame')
 
 
@@ -319,11 +418,8 @@ def delete_camera(camera_name):
             video_obj = video1[camera_name]
             video_obj.delete()
     for camera in listOfCameras:
-        if camera_name == camera["name"]:
+        if camera_name == camera["cameraNumber"]:
             listOfCameras.remove(camera)
-    for msg in listOfMsgs:
-        if camera_name in msg["msg"]:
-            listOfMsgs.remove(msg)
     return Response("success")
 
 
@@ -332,7 +428,8 @@ def query_cameras():
     app.logger.info("Received message from ClientIP [" + request.remote_addr
                     + "] Operation [" + request.method + "]" +
                     " Resource [" + request.url + "]")
-    return jsonify(listOfCameras)
+    camera_info = {"roboCamera": listOfCameras}
+    return jsonify(camera_info)
 
 
 @app.route('/', methods=['GET'])
@@ -345,6 +442,7 @@ def hello_world():
 
 def start_server(handler):
     app.logger.addHandler(handler)
+    create_database()
     if config.ssl_enabled:
         context = (config.ssl_certfilepath, config.ssl_keyfilepath)
         app.run(host=config.server_address, debug=True, ssl_context=context,
index dd037ca..00afd2f 100644 (file)
@@ -17,7 +17,8 @@
 flask~=1.1.2
 requests~=2.18.4
 flask_sslify
-opencv-python
+opencv-python~=3.3.1.11
 flask_cors
-
-influxdb~=5.3.1
\ No newline at end of file
+numpy
+influxdb~=5.3.1
+Wand
\ No newline at end of file