---
# System prepended metadata

title: 2024/12/28 API

---


# 2024/12/28 API



## Q & A
1. 完整 API 專案已寄出，若有問題再跟我說！

## 大綱
- Python 語法說明(decorator)
- 基本API設計(User All)
- 進階API設計
    - Swagger文件
    - JWT token

## 推薦課程
資料結構與演算法
- CS61B: https://sp23.datastructur.es/ (網路上搜尋一下會加上中文字幕的版本)
- 台大線上課程：https://www.youtube.com/playlist?list=PLMHSr8fseBzV7CHWQUXDGRPFkhKlbJjvj




## 需預先下載的軟體
- [Vscode](https://code.visualstudio.com/download)
- [Postman](https://www.postman.com/downloads/)
- Python3.10
```
aniso8601==9.0.1
apispec==5.2.2
certifi==2023.5.7
charset-normalizer==3.2.0
click==8.1.3
Flask==2.1.2
flask-apispec==0.11.4
Flask-JWT-Extended==4.4.1
Flask-RESTful==0.3.9
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
marshmallow==3.18.0
marshmallow-enum==1.5.1
packaging==24.2
pip==24.3.1
PyJWT==2.6.0
python-dotenv==1.0.1
pytz==2022.1
requests==2.31.0
setuptools==75.6.0
six==1.16.0
urllib3==1.26.16
webargs==8.2.0
Werkzeug==2.2.2

```
- ~~MySQLDB server, 可以用自己習慣的方式下載, 也可以使用 [MAMP](https://www.mamp.info/en/downloads/) 來驅動~~

## 上課會用到網頁
- [ChatGPT](https://chat.openai.com/)
- [Jwt解析網頁](https://jwt.io/)
- [Marshmello官網](https://marshmallow.readthedocs.io/en/stable/)


## Mac 的 virtualenv 的方式
```
pip3 install virtualenv
virtualenv venv
source venv/bin/activate
deactivate
```
![image](https://hackmd.io/_uploads/rJ_TA1aS1x.png)



## 上半堂

### vscode 快捷鍵
- 連續按 crtl + D: 持續選取相同的字元, 直到沒有符合條件為止，
好處是可以一次修改相同的字元。
![image](https://hackmd.io/_uploads/SJREuC2Skl.png)

- alt + shift + 下 / 上： 複製鼠標所在的行（上：往上新增，下：往下新增）
- ctrl + P: 透過檔案名稱，搜尋檔案。
![image](https://hackmd.io/_uploads/HJZI_CnrJg.png)

- ctrl + F: 該檔案全域搜尋符合條件的字元。
![image](https://hackmd.io/_uploads/HycUuCnHke.png)

- alt + 上 / 下：移動鼠標所在的行（上：往上移動，下：往下移動）



### 裝飾器(decorator)
```
# Introduction
# function的加工 (紀錄資訊)


def execute():
    print("nice")


def print_func_name(func):
    print(f"The function name is {func.__name__}")
    func()


if __name__ == "__main__":
    print_func_name(execute)
```

```
## function的加工2


def execute():
    print("nice")


def print_func_name(func):
    def warp():
        print(f"The function name is {func.__name__}")
        func()

    return warp


if __name__ == "__main__":
    print_func_name(execute)()
```

```
# Basic decorator


def print_func_name(func):
    def warp():
        print(f"The function name is {func.__name__}")
        func()

    return warp


@print_func_name
def execute():
    print("nice")


if __name__ == "__main__":
    execute()
```

```
# Multi decorator


def print_func_hash_code(func):
    def warp():
        print(f"The function hash code is {func.__hash__()}")
        func()

    return warp


def print_func_name(func):
    def warp():
        print(f"The function name is {func.__name__}")
        func()

    return warp


@print_func_hash_code
@print_func_name
def execute():
    print("nice")


if __name__ == "__main__":
    execute()
```

```
# Decorator with parameter


def print_func_with_param(param):
    def decorator(func):
        def wrap():
            print(f"The function name is {func.__name__}, parameter is {param}")
            func()

        return wrap

    return decorator


@print_func_with_param("nice")
def execute():
    print("Main execute")


if __name__ == "__main__":
    execute()
```

練習題
```
"""
在 execute 的 func 中加入驗證的裝飾器, 驗證內容為輸入正確的帳號密碼(input)
帳號密碼自行定義, 要驗證通過才能取response值
"""


def login(func):
    def warp():
        username, password = input("您的帳號為："), input("您的密碼為：")
        if username == "john" and password == "john":
            return func()
        else:
            print("帳號密碼登入失敗")

    return warp


@login
def execute():
    print("nice")


if __name__ == "__main__":
    execute()
```
結果呈現如下
![image](https://hackmd.io/_uploads/SyK3Zy6SJx.png)
```
# 解答2：

account = input("帳號：")
password = input("密碼：")

def login(account, password):
    def decorator(func):  
        def acps():
            if account == "XXX" and password == "123456":
                func()
            else:
                print("error")
        return acps
    return decorator
 
@login(account, password)
def execute():
    print("nice")


if __name__ == "__main__":
    execute()
```

## User API

==使用習慣==
建議檔案結構
![image](https://hackmd.io/_uploads/S1AFRXTHkl.png)

resource(folder): 放所有的 API route，route 的檔名以 route 的名稱為主 e.g., /user -> user.py
- `__init__.py`: 一定要加！！！不然會無法被其他檔案 import
- `user.py`: route 的邏輯檔案
- `user_route_model.py`: route 的序列化 (serializer) 檔案，給 swagger 使用

`api.py`: API 專案的進入點，同時是執行的檔案 `python3 api.py`
`untl.py`: 整個專案會用到的共用 function e.g., timestamp 轉換，格式同一 func...


POSTMAN 結果
![image](https://hackmd.io/_uploads/BJTHSG6Hkg.png)


解析帶入參數的方式: https://flask-restful.readthedocs.io/en/latest/quickstart.html#a-minimal-api
![image](https://hackmd.io/_uploads/BJbs-JnBJg.png)


定義從哪裡取得參數: https://flask-restful.readthedocs.io/en/latest/reqparse.html
![image](https://hackmd.io/_uploads/HJGc-y3Skx.png)


resource/user_route_model.py
```
from marshmallow import Schema, fields

# Request


class UserPutSchema(Schema):
    name = fields.Str(doc="name", example="string", required=True)
    birth = fields.Str(doc="birth", example="string", required=True)
    note = fields.Str(doc="note", example="string", required=True)


class UserPostSchema(Schema):
    name = fields.Str(doc="name", example="string", required=True)
    birth = fields.Str(doc="birth", example="string", required=True)
    note = fields.Str(doc="note", example="string", required=True)


# Response
class UserGetResponse(Schema):
    message = fields.Str(example="success")
    datatime = fields.Str(example="1970-01-01T00:00:00.000000")
    data = fields.List(fields.Dict())


class UserSingleGetResponse(Schema):
    message = fields.Str(example="success")
    datatime = fields.Str(example="1970-01-01T00:00:00.000000")
    data = fields.Dict()


class UserPostResponse(Schema):
    message = fields.Str(example="success")


class UserPutResponse(Schema):
    message = fields.Str(example="success")


class UserSingleDeleteResponse(Schema):
    message = fields.Str(example="success")

```

resource/user.py
```
import json
from flask_restful import Resource, reqparse
from flask_apispec import MethodResource, marshal_with, doc, use_kwargs
from . import user_route_model


import util


class Users(MethodResource):
    # GET_ALL
    @doc(description="Get Users info.", tags=["User"])
    @marshal_with(user_route_model.UserGetResponse, code=200)
    def get(self):
        with open("user.json", "r", encoding="utf-8") as f:
            data = json.load(f)
        return util.success(data)

    # Create User
    @doc(description="Get Users info.", tags=["User"])
    @use_kwargs(user_route_model.UserPostSchema, location="form")
    @marshal_with(user_route_model.UserPostResponse, code=201)
    def post(self, name, birth, note):
        user = {
            "name": name,
            "birth": birth,
            "note": note,
        }
        with open("user.json", "r", encoding="utf-8") as f:
            data = json.load(f)

        data.append(user)

        with open("user.json", "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=4)

        return util.success(status_code=201)


class User(MethodResource):
    # Get single by id
    @doc(description="Get Single Users info.", tags=["User"])
    @marshal_with(user_route_model.UserSingleGetResponse, code=200)
    def get(self, id):
        with open("user.json", "r", encoding="utf-8") as f:
            data = json.load(f)

        if id > len(data):
            return util.failure(f"User id {id} not found.", status_code=400)
        else:
            return util.success(data[id - 1])

    @doc(description="Update Single Users info.", tags=["User"])
    @use_kwargs(user_route_model.UserPutSchema, location="form")
    @marshal_with(user_route_model.UserPutResponse, code=200)
    def put(self, id, name, birth, note):
        with open("user.json", "r", encoding="utf-8") as f:
            data = json.load(f)

        if id > len(data):
            return util.failure(f"User id {id} not found.", status_code=400)

        user = {
            "name": name,
            "birth": birth,
            "note": note,
        }
        data[id - 1] = user
        with open("user.json", "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=4)

        return util.success()

    @doc(description="Delete Single Users info.", tags=["User"])
    @marshal_with(user_route_model.UserSingleDeleteResponse, code=204)
    def delete(self, id):
        with open("user.json", "r", encoding="utf-8") as f:
            data = json.load(f)

        if id <= len(data): # 這裡要加 =，才能刪除最後一筆資料
            data[id - 1] = {}

        with open("user.json", "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=4)

        return util.success(status_code=204)

```

api.py
```
from flask import Flask
from flask_restful import Api
from apispec import APISpec
from flask_apispec.extension import FlaskApiSpec
from apispec.ext.marshmallow import MarshmallowPlugin
from flask_jwt_extended import JWTManager

from resource.user import Users, User
from resource.login import Login

# Flask init
app = Flask(__name__)

# FlaskRestFul init
api = Api(app)


# Swagger

## JWT swagger setting
### JWT Secret Key 的設定
app.config["JWT_SECRET_KEY"] = "secret_key"
### 為了讓 swagger 能長出鎖頭 （如附件1）
security_definitions = {
    "bearer": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
    }
}

app.config.update(
    {
        "APISPEC_SPEC": APISpec(
            title="Awesome Projectdfdfssdd",
            version="v1",
            ### 實際上帶入 swagger 的位置
            securityDefinitions=security_definitions,
            plugins=[MarshmallowPlugin()],
            openapi_version="2.0.0",
        ),
        "APISPEC_SWAGGER_URL": "/swagger/",  # URI to access API Doc JSON
        "APISPEC_SWAGGER_UI_URL": "/swagger-ui/",  # URI to access UI of API Doc
    }
)

# Swagger init
docs = FlaskApiSpec(app)

# Route
api.add_resource(Users, "/users")
docs.register(Users)
api.add_resource(User, "/user/<int:id>")
docs.register(User)
api.add_resource(Login, "/login")
docs.register(Login)


if __name__ == "__main__":
    ### JWT 初始化，讓 JWT 可以在 flask 中被使用
    jwt = JWTManager().init_app(app)
    app.run(host="0.0.0.0", port="10009", debug=True, use_reloader=True)

```


util.py
```
from datetime import datetime


def success(data=None, status_code=200):
    if data is None:
        return {"message": "success"}, status_code

    return {
        "message": "success",
        "data": data,
        "datatime": datetime.utcnow().isoformat(),
    }, status_code


def failure(data=None, status_code=500):
    if data is None:
        return {"message": "failure"}, status_code

    return {
        "message": "failure",
        "data": data,
        "datatime": datetime.utcnow().isoformat(),
    }, status_code
```




#### JWT 說明

API 加入驗證保護
```
@doc(
        description="Delete Single Users info.",
        tags=["User"],
        security=[{"bearer": []}],
    )
    @marshal_with(user_route_model.UserSingleDeleteResponse, code=204)
    @jwt_required()
    def delete(self, id):
        with open("user.json", "r", encoding="utf-8") as f:
            data = json.load(f)

        if id < len(data):
            data[id - 1] = {}

        with open("user.json", "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=4)

        return util.success(status_code=204)
```
`security=[{"bearer": []}],`
讓 API 在 Swagger 介面上能自動帶入鎖頭(token)
![image](https://hackmd.io/_uploads/SyOHoZII1g.png)


`@jwt_required()` 
加在 API 的 function，讓這 API 被 call 時，需要帶入 JWT token

產出JWT token
```
import json
from flask_jwt_extended import create_access_token
from flask_apispec import MethodResource, marshal_with, doc, use_kwargs
import util
from resource import login_router_model
from datetime import timedelta


class Login(MethodResource):
    @doc(description="User Login", tags=["Login"])
    @use_kwargs(login_router_model.LoginSchema, location="form")
    @marshal_with(login_router_model.LoginResponse, code=200)
    def post(self, account, password):
        if account == "john" and password == "john":
            token = create_access_token(
                identity={"account": account}, expires_delta=timedelta(days=1)
            )
            return util.success(token)
        else:
            return util.failure("Account or password is wrong")
```

`create_access_token`
透過這個 func 產出 jwt token，裡面需要帶入以下兩個參數
1. identity: 要產出的資訊，此資訊可以被解析出來 e.g., 下圖中的 sub: {account: john}(解析網站如附件2) ![image](https://hackmd.io/_uploads/Bk5RjW8LJl.png)

2. expires_delta: 這個 token 多久後過期


### 驗證方式
Swagger 界面驗證
記得最前面要加上 “Bearer”
![image](https://hackmd.io/_uploads/HkPdErAH1e.png)

Postman 驗證
Authorization -> Auth Type -> Bearer Token -> 直接貼上(不須要加上 Bearer，Postman 會自動幫忙帶入)
![image](https://hackmd.io/_uploads/H1-nErRHJx.png)




附件1: 鎖頭
![image](https://hackmd.io/_uploads/HJWf5ZIUkg.png)

附件2: jwt解析
前往 https://jwt.io/ 這個網頁，將產出的 token 貼上即可解析
![image](https://hackmd.io/_uploads/SyOu2Z88kx.png)


---
作業1
```
import requests

url = "https://yfapi.net/v8/finance/chart"
querystring = {"symbols": "AAPL", "interval": "1d", "range": "1mo"}
headers = {"x-api-key": ""}
response = requests.request("GET", url, headers=headers, params=querystring)
results = response.json()

"""
透過 https://yfapi.net/v8/finance/chart/{ticker} 的API 
取出AAPL , Tsla, MSFT的 為期5天且間隔為1天的股價資訊(low, high, open, close),
並整理成下放的資料架構

Tips:
1. 把timestamp格式改為datetime並設為key
2. 建立一個dict, 並把股票代號設為key值
3. 取出個股票的特定資料價格, 並把它設為另一個dict的value值
"""

"""
{
    "2021/12/21": {
        "AAPL" : {"open": 100, "close": 120, "high": 150, "low": 99},
        "NFLX" : {"open": 100, "close": 120, "high": 150, "low": 99},
        "GOOGL" : {"open": 100, "close": 120, "high": 150, "low": 99},
    },
    "2021/12/21": {
        "AAPL" : {"open": 100, "close": 120, "high": 150, "low": 99},
        "NFLX" : {"open": 100, "close": 120, "high": 150, "low": 99},
        "GOOGL" : {"open": 100, "close": 120, "high": 150, "low": 99},
    },
    }
}
"""

if __name__ == "__main__":
    print(response.json())
```
