2024年5月29日 星期三

WSL 安裝 ssh 並更改 port 設置自動啟動

WSL 安裝 ssh 並更改 port 設置自動啟動

預設狀態是不會自動啟動的而且 port 可能與主機端有衝突,視情況可以自行改成 2222 口這樣可以避開 Windows 本身如果也有 ssh 的話。



安裝並配置 OpenSSH

首先,確保已安裝 OpenSSH 伺服器:

sudo apt update
sudo apt install openssh-server


1. 設定 SSH 端口與密碼登入

編輯 SSH 設定檔:

sudo nano /etc/ssh/sshd_config

  1. 找到 Port 行,並修改為你所要的或保持預設就 22
  2. 找到 PasswordAuthentication  行,並修改為 yes

Port 22
PasswordAuthentication yes

儲存並退出 (Ctrl+S 然後 Ctrl+X)。

這裡預設值其實本來就是 22 ,只是為了示範如何修改而特地寫出來


2. 設定 SSH 伺服器開機自動啟動

設置 systemd 服務管理器:

sudo systemctl enable ssh

必須開著 WSL 視窗才會生效(這服務 systemctl 其實是由 wsl.conf 文件間接啟動的)


如果是舊版的 wsl 不支持 systemctl 請這裡編輯 /etc/wsl.conf 文件:

sudo nano /etc/wsl.conf

在文件末尾添加以下內容:

command="service ssh start"

保存文件(按 Ctrl + O),然後退出編輯器(按 Ctrl + X)。


4. 重啟 WSL

在 Windows 命令提示字元或 PowerShell 中執行:

wsl --shutdown

然後再次打開 WSL 後,SSH 伺服器應該會自動啟動並使用設定的端口。


在 WSL 中執行以下命令確認 SSH 伺服器是否正在運行:
sudo service ssh status



從主機端以 SSH 連接 WSL

打開主機端的 PowerShell 嘗試從主機端連接 WSL:
ssh -p 22 your_username@localhost

這裡 -p 的選項預設值就是 22,如果沒有改 port 可以省略不打





[番外篇] 在 WSL 中添加允許的公鑰

在 WSL 中創建 .ssh 目錄並設置適當的權限:

mkdir -p ~/.ssh
chmod 700 ~/.ssh

已經存在則無需建立


將你想要使用的公鑰添加到 authorized_keys 文件中:

nano ~/.ssh/authorized_keys

已經存在則無需建立


將公鑰貼上到此文件中,然後保存並退出。設置 authorized_keys 文件的權限:

chmod 600 ~/.ssh/authorized_keys



參考

2024年5月26日 星期日

Python 如何使用 boxsdk 獲取 Box Accese Token

Python 如何使用 boxsdk 獲取 Box Accese Token

繼上一篇
CHG: Python 如何使用 Json 通過 JWT 請求獲取 Box Accese Token (不使用boxsdk)

詳細介紹了如何手動請求,這裡介紹 boxsdk 的使用方法



安裝 boxsdk[jwt]

參考這裡官方安裝的教學
box/box-python-sdk: Box SDK for Python (github.com)

他有分兩個版本記得後面要括號 jwt 會多裝一些基本套件

pip install "boxsdk[jwt]"

單純的執行就可以安裝好了



方法1 直接直接導入 json 檔案

參考自官方的這篇文章
JWT with SDKs - Box Developer Documentation

以及這裡的教學
BOX API(JWT認証)をPythonで使ったときの備忘録 #Python - Qiita

from boxsdk import JWTAuth
from boxsdk import Client

# JSON 檔案的路徑
CONFIG_FILE = 'config.json'

# 準備使用 BOX API
auth = JWTAuth.from_settings_file(CONFIG_FILE)
client = Client(auth)

# 獲取並顯示用戶資訊(API 測試)
user = client.user().get()
print(f'用戶名稱: {user.name} (ID: {user.id}), 電子郵件: {user.login}')

如此就能輕鬆導入了



方法2 手動導入 JWT物件

這裡是手動從 json 中抽取出數據,並導入 JWT 物件的方法

import json
from boxsdk import Client, JWTAuth

# 加載官方 JSON 檔案
with open('config.json') as json_file:
    config = json.load(json_file)

# 創建 JWTAuth 認證對象
auth = JWTAuth(
    client_id=config['boxAppSettings']['clientID'],
    client_secret=config['boxAppSettings']['clientSecret'],
    enterprise_id=config['enterpriseID'],
    jwt_key_id=config['boxAppSettings']['appAuth']['publicKeyID'],
    rsa_private_key_data=config['boxAppSettings']['appAuth']['privateKey'].replace('\\n', '\n'),
    rsa_private_key_passphrase=config['boxAppSettings']['appAuth']['passphrase']
)

# 獲取訪問令牌
access_token = auth.authenticate_instance()
print(f"訪問令牌: {access_token}")

# 使用訪問令牌創建 Box 客戶端
client = Client(auth)

# 認證並獲取當前用戶資訊
user = client.user().get()
print(f'用戶名稱: {user.name} (ID: {user.id}), 電子郵件: {user.login}')


本質上跟 方法1 跑的是一樣的代碼只是拉出來手動做而已。


2024年5月25日 星期六

Python 如何使用 Json 通過 JWT 請求獲取 Box Accese Token (不使用boxsdk)

Python 如何使用 Json 通過 JWT 請求獲取 Box Accese Token (不使用boxsdk)

方法是參考自這篇的步驟說明

SDKを使用しないJWT - Box Developerドキュメント

以及這個的官方示例後修改而來
samples-docs-authenticate-with-jwt-api/sample.3.py


如果需要在 PwerShell 上的版本可以參考這個 PsJwt 專案中的範例
PsJwt/Example/Get-BoxToken at main · hunandy14/PsJwt (github.com)


下面讓我們開始正文吧如何一步一步的獲取到令牌



讀取 config.json 文件

這份文件可以通過這個教學一步步取得
SDKを使用しないJWT - Box Developerドキュメント

總之就是需要你在個人的 Box 帳號中註冊一個應用程式,並且在註冊後必須由經由該網域的Admin帳號同意權限請求才能使用。如果該帳號就是 Admin 則可以自己審核自己。

import json
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key

# 讀取 config.json 文件
with open('config.json') as json_file:
    config = json.load(json_file)

# 從配置中加載 PEM 私鑰
key = load_pem_private_key(
    data=config["boxAppSettings"]["appAuth"]["privateKey"].encode('utf8'),
    password=config["boxAppSettings"]["appAuth"]["passphrase"].encode('utf8'),
    backend=default_backend(),
)

這個很簡單就是把東西讀取出來而已,並把加密的私鑰先解密出來。



建立 JWT claims 聲明

建立 JWT claims 聲明

import secrets
import time

# 設定認證 URL
authentication_url = 'https://api.box.com/oauth2/token'

# 建立 JWT claims 用於認證
claims = {
    'iss': config['boxAppSettings']['clientID'],  # 客戶端 ID
    'sub': config['enterpriseID'],                # 企業 ID
    'box_sub_type': 'enterprise',                 # 認證類型
    'aud': authentication_url,                    # 受眾 URL
    'jti': secrets.token_hex(64),                 # JWT ID,用於唯一性
    'exp': round(time.time()) + 45                # 過期時間 (45 秒後)
}

這個聲明最終會變成發送給 Box 伺服器的資訊



建立 JWT assertions

有了 claims 之後,我們需要用這些聲明來生成一個 JWT token。這個 token 將會用來向 Box 的認證服務請求 Access Token。

import jwt

# 建立 JWT assertion
assertion = jwt.encode(
    claims, 
    key, 
    algorithm='RS512',
    headers={
        'kid': config['boxAppSettings']['appAuth']['publicKeyID']
    }
)

這段程式碼使用 pyjwt 庫來對 claims 進行簽名。key 是我們之前解密出的私鑰,而 algorithm 指定了我們使用 RS512 來進行簽名。

JWT token 驗證可以參考這個網站 JSON Web Tokens - jwt.io ,他可以動態驗證你的 JWT 是否正確,如果輸入私鑰也可以動態生成



發送請求以獲取 Access Token

我們現在需要將生成的 JWT token 發送到 Box 的認證伺服器,以換取一個 Access Token。

import requests

# 發送請求以獲取存取權杖
response = requests.post(
    authentication_url, {
        'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
        'assertion': assertion,
        'client_id': config['boxAppSettings']['clientID'],
        'client_secret': config['boxAppSettings']['clientSecret']
    }
)
# 解析回應並獲取存取權杖
access_token = response.json()['access_token']
print(f"AccessToken: {access_token}")

這裡使用 requests 庫發送 HTTP POST 請求到 Box 的認證伺服器。回應中包含了我們需要的 Access Token。

這個就是最終的成品了,可以拿來直接對 Box API 伺服器請求操作你的帳號了。



驗證 Access Token 

接下來,我們將使用獲取的 Access Token 來驗證身份,並且可以進一步操作 Box API。這邊就做一個簡單的測試查詢當前帳號是誰。

這部分就直接引用 boxsdk 測試比較快,目的只是測試獲取的令牌是否可用,也可以通過 response 的方式手動獲取請求。

from boxsdk import OAuth2, Client

# 使用 OAuth2 認證
auth = OAuth2(
    client_id=config['boxAppSettings']['clientID'],
    client_secret=config['boxAppSettings']['clientSecret'],
    access_token=access_token,
)

# 創建 Box 客戶端
client = Client(auth)
# 獲取用戶資訊
user = client.user().get()
print('Authenticated as:', user.name)

這段程式碼使用 boxsdk 庫來創建一個 Box 客戶端,並使用 Access Token 進行身份驗證。最後我們打印出已驗證用戶的名稱。



驗證 Access Token 2

這是直接透過 requests 跟 Box 伺服器交互的方法,會稍微麻煩一點,還得自己解構回傳的信息字串。

在這個方法中,我們不使用 boxsdk 庫,而是直接使用 requests 庫來發送 HTTP 請求,並手動處理回應。這樣可以更靈活地控制 API 請求和回應。

import requests

# 發送請求到 Box API 以獲取當前用戶資訊
response = requests.get(
    'https://api.box.com/2.0/users/me',
    headers={
        'Authorization': f'Bearer {access_token}',
    }
)

# 檢查請求是否成功
if response.status_code == 200:
    user_info = response.json()
    print('Authenticated as:', user_info['name'])
else:
    print('Failed to authenticate')
    print('Response:', response.text)

這段程式碼做了以下幾件事:

  1. 設置請求頭:使用獲取的 Access Token 設置請求頭中的 Authorization 字段。
  2. 發送 GET 請求:向 Box API 發送 GET 請求,以獲取當前用戶的資訊。請求的 URL 是 https://api.box.com/2.0/users/me
  3. 處理回應:檢查回應的狀態碼。如果狀態碼是 200,表示請求成功,然後解析回應的 JSON 數據並打印出用戶名稱。如果請求失敗,打印回應的錯誤信息。

通過這種方式,你可以手動處理 Box API 的所有回應,這在需要進行更複雜的錯誤處理或自定義 API 請求時特別有用。




2024年5月22日 星期三

Python 專案如何在提交時觸發 pytest 自動測試

Python 專案如何在提交時觸發 pytest 自動測試

使用的工具是 pre-commit 和 pytest 這兩個庫

pip install pre-commit pytest pytest-html pytest-metadata



用法其實很容易大概看一下說明就能理解了這邊寫一個範例可以自跑測試

run_tests.py

import os
import subprocess
import pytest
import argparse
from datetime import datetime

# Define the report directory
report_dir = 'reports'

def main():
    parser = argparse.ArgumentParser(description="Run pytest and generate HTML report.")
    parser.add_argument('--hook', action='store_true', help="Run as a pre-commit hook with commit hash in report filename.")
    args = parser.parse_args()

    if not os.path.exists(report_dir):
        os.makedirs(report_dir)

    # Get the current timestamp
    timestamp = datetime.now().strftime('%Y%m%d-%H%M%S')

    if args.hook:
        # Get the current commit hash
        try:
            commit_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).strip().decode('utf-8')
        except subprocess.CalledProcessError as e:
            print(f"Error getting commit hash: {e}")
            return

        # Define the report filename with timestamp and commit hash
        report_file = os.path.join(report_dir, f'test_report_{timestamp}_aftercommit-{commit_hash[:5]}.html')

    else:
        # Define the report filename with timestamp
        report_file = os.path.join(report_dir, f'test_report_{timestamp}.html')

    # Run pytest with the HTML report option
    pytest_args = ['tests/', '--html', report_file]
    print(f"Running pytest with arguments: {pytest_args}")
    pytest.main(pytest_args)

if __name__ == "__main__":
    main()

然後直接運行就可以了,會自動測試存放在 tests 資料夾裡面的測試


比如說這個測試檔案 tests/test_sample.py-

import pytest

def divide(a, b):
    return a / b

# 正常測試
def test_divide_normal():
    assert divide(6, 3) == 2, "測試 divide(6, 3) 失敗,期望結果為 2"
    print("測試 divide(6, 3) 成功,期望結果為 2")

# 異常測試
def test_divide_abnormal():
    with pytest.raises(ZeroDivisionError):
        divide(1, 0)
    print("測試 divide(1, 0) 成功,期望拋出 ZeroDivisionError")

# 邊界測試
def test_divide_boundary():
    assert divide(1e10, 1e5) == 1e5, "測試 divide(1e10, 1e5) 失敗,期望結果為 1e5"
    print("測試 divide(1e10, 1e5) 成功,期望結果為 1e5")


也可以直接呼叫 pytest 進行測試,參數是指定資料夾

pytest tests/

會自己抓 test_ 開頭的檔案中 test_ 開頭的函式測試


使用 --html 將報告生成到 html 上

pytest tests/ --html=report.html




再來是寫 hook 這邊用工具產生的所以只要寫在 .pre-commit-config.yaml 這份檔案裏面就可以了

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: run-tests
        name: Run Tests
        entry: python run_tests.py --hook
        language: system
        stages: [commit]
        pass_filenames: false

然後執行命令安裝到 git 中

pre-commit install

這樣就完成了,再來嘗試提交看看就會自動測試了

Python 如何動態轉發到模組中的同名函式

Python 如何動態轉發到模組中的同名函式

這是上一篇文章的延伸,檔案結構請參考這篇
CHG: Python 如何動態載入模組中的所有的檔案 (charlottehong.blogspot.com)

主要是想把實作分開到 impl 檔案中,這裡是自動導入的寫法一樣追加在該包的初始話文件中


utils\__init__.py

import importlib
from functools import wraps

# 動態轉發與檔案名稱相同的函式或類別
def forward(target):
    # 裝飾器應用於函式時
    if callable(target):
        @wraps(target)
        def wrapper(*args, **kwargs):
            # 先執行原始函數(但不使用其返回值)
            target(*args, **kwargs)
            func_name = target.__name__
            try:
                # 動態導入目標模組
                parent_module = importlib.import_module(f'.{func_name}', package=__package__)
                # 獲取同名的函數
                target_func = getattr(parent_module, func_name, None)
                if target_func is None:
                    raise AttributeError(f"'{parent_module.__name__}' module has no attribute '{func_name}'")
                # 執行轉發函數並返回其結果
                return target_func(*args, **kwargs)
            except ImportError as e:
                raise ImportError(f"Failed to import module for function '{func_name}': {e}")
            except AttributeError as e:
                raise AttributeError(f"Function '{func_name}' not found in the module '{parent_module.__name__}': {e}")
        return wrapper
    # 裝飾器應用於類別時
    elif isinstance(target, type):
        for attr_name, attr_value in target.__dict__.items():
            if callable(attr_value) and not attr_name.startswith('__'):
                decorated_method = forward(attr_value)
                setattr(target, attr_name, decorated_method)
        return target
    else:
        raise TypeError("The @forward decorator can only be applied to functions or classes")


然後就可以在 main 中這樣使用

import utils

@utils.forward
def test_func(name):
    pass

@utils.forward
class test_class:
    pass

if __name__ == "__main__":
    test_func("CHG")
    test_class().method()

如此一來就可以自動轉發到同名的函式或類別上了


其中我有保留了原本函式的執行,這是為了把參數前置處理檢查之類的工作可以一同寫在參數定義上,轉發後的函式只需要專注在業務邏輯上

  • 執行順序是先原函式然後再轉發的函式
  • 原函式的返回值會被丟棄,以轉發的函式的返回值為主


新增在 utils 中有兩個檔案

utils\test_func.py

def test_func(name):
    print(f"[name = {name}]")

utils\test_class.py

class test_class:
    def method(self):
        print("This is a method from test_class in utils.test_class")