2025-05-20

Mac 上使用 python 工具 uv

Mac 上使用 python 工具 uv

uv 是一個新的 Python 工具鏈,透過 效能優化、功能整合與使用者友善設計,簡化了 Python 專案的開發、依賴管理和發布流程。

MacOS 安裝 uv

最簡單的方式是使用 homebrew 安裝 uv

brew install uv

透過 uv 特定版本的 python

安裝好後可以查看已安裝的 python 版本

# 查看已安裝的 python 版本 uv python list
cpython-3.14.0a7-macos-aarch64-none <download available> cpython-3.14.0a7+freethreaded-macos-aarch64-none <download available> cpython-3.13.3-macos-aarch64-none <download available> cpython-3.13.3+freethreaded-macos-aarch64-none <download available> cpython-3.13.2-macos-aarch64-none /opt/homebrew/bin/python3.13 -> ../Cellar/python@3.13/3.13.2/bin/python3.13 cpython-3.13.2-macos-aarch64-none /opt/homebrew/bin/python3 -> ../Cellar/python@3.13/3.13.2/bin/python3 cpython-3.12.10-macos-aarch64-none <download available> cpython-3.12.7-macos-aarch64-none /opt/homebrew/bin/python3.12 -> ../Cellar/python@3.12/3.12.7_1/bin/python3.12 cpython-3.11.12-macos-aarch64-none .local/share/uv/python/cpython-3.11.12-macos-aarch64-none/bin/python3.11 cpython-3.10.17-macos-aarch64-none <download available> cpython-3.10.15-macos-aarch64-none /opt/homebrew/bin/python3.10 -> ../Cellar/python@3.10/3.10.15/bin/python3.10 cpython-3.9.22-macos-aarch64-none <download available> cpython-3.9.6-macos-aarch64-none /usr/bin/python3

安裝特定版本的 python

uv python install 3.11

透過 uv 建立專案有兩種方式: - 和原有的 pip 和 venv 相容的方式 - 直接使用 uv 管理

搭配 pip 和 venv 建立專案

# 指定 python 版本,建立專案語法 uv venv --python=<version> <env-name> # 範例 uv venv --python=3.12 .venv

這種方式的缺點是沒有記錄 python 版本,但實際上在 .venv/pyvenv.cfg 可以查看相關設定,但沒有記錄到版控。

# 安裝套件 uv pip install requests

直接使用 uv 管理

# 初始化專案 uv init --python 3.11

會直接建立專案的 git 設定,同時建立專案設定檔 pyproject.toml。

# 加入套件 uv add requests

加入任一套件後會自動建立 .venv 資料夾,並把虛擬環境建立起來。同時把安裝的套件記錄在 pyproject.toml。 pyproject.toml 的角色類似 nodejs 專案的 package.json。其他常用的指令如下:

# 查看 uv 用法 uv -h # 移除套件 uv remove <套件> uv remove requests # 執行指令或程式 uv run <程式或指令> uv run main.py # 更新特定套件 uv lock --upgrade-package requests # 更新所有套件 uv lock --upgrade

2025-04-25

nginx 反向代理 fastapi 和 ollama 服務

nginx 反向代理 fastapi 和 ollama 服務

uvicorn 和 fastapi

使用 Uvicorn 要注意的大概就是 proxy_set_header 那邊的設定。其他 https 的部份,請使用 Certbot 幫忙搞定。

server { server_name domain.shinder.cc; # managed by Certbot add_header Accept-Ranges bytes; location / { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; proxy_cache off; } listen [::]:443 ssl ipv6only=on; # managed by Certbot listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/domain.shinder.cc/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/domain.shinder.cc/privkey.pem; # managed by Certbot include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = domain.shinder.cc) { return 301 https://$host$request_uri; } # managed by Certbot listen 80 ; listen [::]:80 ; server_name domain.shinder.cc; return 404; # managed by Certbot }

ollama

一般為了安全性 ollama 服務是不對外的。在此的情境是,想讓遠端使用資源,但限定 IP。對外開放的通訊埠為 11435。

server { server_name _; add_header Accept-Ranges bytes; location / { proxy_pass http://127.0.0.1:11434; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; proxy_cache off; allow 192.168.1.100; # 允許 192.168.1.100 訪問 deny all; # 拒絕所有其他 IP 訪問 } listen [::]:11435; listen 11435; }

2025-03-29

Ubuntu 重新開機後以 pm2 重啟行程

Ubuntu 重新開機後以 pm2 重啟行程

一般建議使用 systemd 來設定,這裡記錄一下 pm2 的作法。

# 1. 啟動設定,pm2 會依使用的系統,寫入啟動的 script (例如 systemd) # 這部份可以免除複雜的設定,交給 pm2 處理就好 pm2 startup
# 2. 以 pm2 啟動你要執行的行程,通常是 Nodejs 應用 pm2 start index.js --name mfee62_3000 # 3. 儲存設定 pm2 save # pm2 會依據目前記憶體內正在執行的行程,儲存一個記錄檔以利重開機後重啟這些行程

2025-03-10

使用 homebrew 安裝 apache2 和 php 環境

使用 homebrew 安裝 apache2 和 php 環境

XAMPP 的 mac 版本現在都比較老舊,而且在 macOS 15 有無法啟動 apache 的問題。 為了教學需求,在此記錄 mac 的 php 環境的建立方式。假設 homebrew 的環境已經安裝完成。 如果要使用 ubuntu vm 可以參考上一篇 https://qops.blogspot.com/2025/03/orbstack-ubuntu-224-php-56-apache2.html 的作法。

安裝 apache2

# 先查看是否已安裝 brew info apache2 # 前幾行若出現 Not installed 表示沒有安裝,後面也有設定的說明
# 安裝 brew install apache2 # 安裝後,注意預先的設定 # -- DocumentRoot is /opt/homebrew/var/www # -- 主設定檔位置 /opt/homebrew/etc/httpd/httpd.conf # -- ssl 設定檔位置 /opt/homebrew/etc/httpd/extra/httpd-ssl.conf # -- 預設的 ports 為 8080 和 8443 # 啟動服務 brew services start httpd # 在瀏覽器上查看 http://localhost:8080 # 重新啟動服務 brew services restart httpd # 停止服務 brew services stop httpd

安裝 php

這裡是以 php 8.3 的版本為例說明,當然你可以自行決定要安裝的版本。

# 先查看要安裝的版本資訊 brew info php@8.3 # 安裝 brew install php@8.3
# 在 apache 的設定檔,放入下式 LoadModule php_module /opt/homebrew/opt/php@8.3/lib/httpd/modules/libphp.so <FilesMatch \.php$> SetHandler application/x-httpd-php </FilesMatch> # 設定索引檔 DirectoryIndex index.php index.html

PHP 的設定檔位於 /opt/homebrew/etc/php/8.3/ 裡面。

# 若要使用 php 指令,可以將下兩行放入用戶設定檔 (.zshrc) 裡 export PATH="/opt/homebrew/opt/php@8.3/bin:$PATH" export PATH="/opt/homebrew/opt/php@8.3/sbin:$PATH"

在根目錄 /opt/homebrew/var/www 寫一支 phpinfo() 檔測試看看。

mysql 8.0

# 先查看要安裝的版本資訊 brew info mysql@8.0 # 安裝 brew install mysql@8.0 # 啟動服務 brew services start mysql@8.0

2025-03-03

OrbStack 安裝 Ubuntu 22.4, php 5.6 及 apache2

OrbStack 安裝 Ubuntu 22.4, php 5.6 及 apache2

某個專案還在老舊的 php 5.6 環境,macOS 15 Sequoia 目前已無法正常執行 XAMPP。在不小心升級 macOS 後,測試 php 5.6 程式是讓人困擾的問題。

原本想要使用 OrbStack 的 docker containers 功能,然而 container 需要不少設定,反而是一種麻煩的做法。後來還是以 VM 的方式來處理。OrbStack 的安裝就不贅述。

安裝 ubuntu vm

# *** 安裝 ubuntu vm *** # orb create --help # orb create [flags] DISTRO[:VERSION] [MACHINE_NAME] orb create ubuntu:jammy # 預設的 machine name 為 ubuntu
# *** 從 mac ssh 連線 vm *** # ssh [用戶名]@[虛擬主機名]@orb ssh root@ubuntu@orb
# *** 使用 vm 的 domain *** # ping [虛擬主機名].orb.local ping ubuntu.orb.local # mac host 的 ip 可以直接使用區域網路 ip

在 vm 中可以透過 /mnt/mac 路徑找到 mac 的對應檔案。

安裝 php 5.6

以 ssh 登入 vm 後,依下列步驟安裝 php 5.6

# *** 1. 更新系統 repo 資料 apt update # *** 2. 安裝 software-properties-common 以管理 PPA (Personal Package Archives) apt install software-properties-common # *** 3. php 5.6 已經不是官方維護的版本,需依賴 PPA add-apt-repository ppa:ondrej/php # *** 4. 再更新系統 repo 資料一次 apt update # *** 5. 安裝 php 5.6 apt install php5.6 # *** 6. 安裝 php 常用套件 apt install php5.6-mysql php5.6-curl php5.6-json php5.6-cgi php5.6-xml php5.6-mbstring php5.6-gd # *** 7. 確認 php 版本 php -v # *** 8. 啟用 apache2 模組 # 在安裝 apache2 之後才可以執行 # a2dismod php8.3 # 若有需要先關閉新模組 a2enmod php5.6

安裝 apache2

apt install apache2
# 啟用 rewrite module a2enmod rewrite
# /etc/apache2/mods-enabled 裡啟用的模組參考 access_compat.load -> ../mods-available/access_compat.load alias.conf -> ../mods-available/alias.conf alias.load -> ../mods-available/alias.load auth_basic.load -> ../mods-available/auth_basic.load authn_core.load -> ../mods-available/authn_core.load authn_file.load -> ../mods-available/authn_file.load authz_core.load -> ../mods-available/authz_core.load authz_host.load -> ../mods-available/authz_host.load authz_user.load -> ../mods-available/authz_user.load autoindex.conf -> ../mods-available/autoindex.conf autoindex.load -> ../mods-available/autoindex.load cache.load -> ../mods-available/cache.load cache_disk.conf -> ../mods-available/cache_disk.conf cache_disk.load -> ../mods-available/cache_disk.load deflate.conf -> ../mods-available/deflate.conf deflate.load -> ../mods-available/deflate.load dir.conf -> ../mods-available/dir.conf dir.load -> ../mods-available/dir.load env.load -> ../mods-available/env.load expires.load -> ../mods-available/expires.load file_cache.load -> ../mods-available/file_cache.load filter.load -> ../mods-available/filter.load headers.load -> ../mods-available/headers.load mime.conf -> ../mods-available/mime.conf mime.load -> ../mods-available/mime.load mpm_prefork.conf -> ../mods-available/mpm_prefork.conf mpm_prefork.load -> ../mods-available/mpm_prefork.load negotiation.conf -> ../mods-available/negotiation.conf negotiation.load -> ../mods-available/negotiation.load php5.6.conf -> ../mods-available/php5.6.conf php5.6.load -> ../mods-available/php5.6.load reqtimeout.conf -> ../mods-available/reqtimeout.conf reqtimeout.load -> ../mods-available/reqtimeout.load rewrite.load -> ../mods-available/rewrite.load setenvif.conf -> ../mods-available/setenvif.conf setenvif.load -> ../mods-available/setenvif.load socache_shmcb.load -> ../mods-available/socache_shmcb.load ssl.conf -> ../mods-available/ssl.conf ssl.load -> ../mods-available/ssl.load status.conf -> ../mods-available/status.conf status.load -> ../mods-available/status.load

安裝 Mariadb 10

# *** 1. 更新套件資訊 apt update # *** 2. 查看可以安裝的子版本 apt-cache policy mariadb-server # *** 3. 安裝 apt install mariadb-server
# 以 mysql client 使用 root 登入 mysql -uroot # 變更 root 密碼 ALTER USER 'root'@'localhost' IDENTIFIED BY '你的密碼'; # 建立管理用戶給 phpmyadmin 使用 CREATE USER 'pmauser'@'localhost' IDENTIFIED BY 'pmauser'; GRANT ALL PRIVILEGES ON *.* TO 'pmauser'@'localhost' WITH GRANT OPTION; FLUSH PRIVILEGES; # 查看使用者 SELECT User, Host FROM mysql.user;

2024-08-24

使用前端 JS 接收 Ollama 回應的 Ndjson 格式

使用前端 JS 接收 Ollama 回應的 Ndjson 格式

基本上就是使用 fetch API 來處理串流資料,利用 async/await 就可以使用無窮迴圈來處理。

<input type="text" id="chat" value="台灣位於哪裡?" /> <button onclick="startAsk()">開始</button> <div id="result_div"></div>
const chat = document.querySelector("#chat"); const result_div = document.querySelector("#result_div"); const model = "mistral-nemo"; // ollama 使用的 LLM 模型名稱 const startAsk = async () => { const response = await fetch(`http://localhost:11434/api/generate`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ model, prompt: chat.value }), }); if (!response.ok) { console.log("回應發生錯誤!!!"); return; } // ['content-type', 'application/x-ndjson'] console.log(...response.headers); const reader = response.body.getReader(); // 讀取器 const decoder = new TextDecoder("utf8"); // 解碼器 while (true) { const { value, done } = await reader.read(); const json = decoder.decode(value); // 基本上每一行都是 json console.log(json); try { const obj = JSON.parse(json); result_div.innerText += obj.response; } catch (ex) { console.log(ex); console.log("不是 JSON:", `---${json}---`); } if (done) { result_div.innerText += "\n\n"; break; } } };

2024-07-15

在 M1 macOS上執行 tensorflow 的環境

在 M1 macOS上執行 tensorflow 的環境

目前 M3 的晶片都出好一陣子了,想說 M1 跑 tensorflow 應該沒問題。 結果不管是 python 3.11 或 3.12 都無法正常執行使用到 tensorflow 的專案。

爬文都只能爬到 python 3.8 的環境,3.8 也被評為執行 tensorflow 最為穩定的版本。 後來只好安裝 3.8 再使用 3.8 建立虛擬環境。

參考 Apple 官方文件 https://developer.apple.com/metal/tensorflow-plugin/

# 安裝 python 3.8 brew install python@3.8 mkdir proj-dir cd proj-dir # 建立虛擬環境 python3.8 -m venv venv # 啟動虛擬環境 source ./venv/bin/activate # 安裝 tensorflow 2.12 的版本 pip install tensorflow-macos # 安裝 tensorflow-metal pip install tensorflow-metal

測試官方的範例,有出現 Warning 和 Notice 不過可以正常執行。

2024-02-04

Apache HTTPS 服務間歇停止的問題

Apache HTTPS 服務間歇停止的問題

2024-02-01 開始客戶的網站就發生服務中斷的情況(其實這狀況當時應該發生好幾天了)。

主機架在 DigitalOcean 的 SGP1 上,由於 SGP1 網路設備曾有停擺快 2 天的黑記錄;所以一開始就懷疑是不是網路設備不穩。

以往的經驗,通常停止十分鐘左右就會恢復正常,但這次卻不是想像中單純。

服務大概停擺十分鐘後,就恢復正常,但是每隔一到兩小時就發生一次服務停擺,實著讓人頭疼。

一開始是毫無頭緒到底是什麼問題,也發 ticket 給 DO,DO 回應希望進一步提供資料。

懷疑過:網路設備問題、程式沒寫好、SSL 憑證問題、MySQL 資料庫問題等,大伙搞了兩天也沒離清問題點,只發現 HTTPS 受影響,但 HTTP 似乎沒受影響。可是 HTTPS 中斷服務時, HTTP 也變慢了。

大家的想法都一樣,HTTPS 和 HTTP 都是 Apache 伺服器的服務;沒道理 HTTPS 停擺,而 HTTP 正常。所以焦點又被轉移到 SSL 憑證上。

第三天開始,我終於有比較多的時間,從調 MySQL 開始搞了半天,沒什麼進展。 後來在一堆 Apache error log 中,某個停擺時點找到一條:

[Sat Feb 03 23:54:11.819500 2024] [mpm_prefork:error] [pid 8964] AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting

很少在搞設定的我,只好查一下資料 mpm_prefork 是什麼模組,然後網路上也沒查到上面的錯誤會造成什麼問題。 後來在 stackoverflow 找到這篇 AH00161: server reached MaxRequestWorkers setting, consider raising the MaxRequestWorkers setting

硬著頭皮依照建議設定 mpm_prefork.conf,心想應該就是這個問題。 一覺起來,果然有改善,再依 log 做些調整。

# prefork MPM # StartServers: number of server processes to start # MinSpareServers: minimum number of server processes which are kept spare # MaxSpareServers: maximum number of server processes which are kept spare # MaxRequestWorkers: maximum number of server processes allowed to start # MaxConnectionsPerChild: maximum number of requests a server process serves <IfModule mpm_prefork_module> StartServers 10 MinSpareServers 10 MaxSpareServers 20 ServerLimit 2000 MaxRequestWorkers 1536 MaxConnectionsPerChild 10000 </IfModule>

幾天的心情低落就因為一個設定。

另一個有趣的事情,就是在 stackoverflow 找資料時,看到一篇症狀和我們遇到的差不多狀況。 但傻眼的是,他自己解答自己的問題,答案是「公司找到會設定 Apache 的人才,他弄一下設定就好了」,沒有提到任何的模組或設定...

2023-11-12

RemixJS 使用 Jotai 取代 Context

RemixJS 使用 Jotai 取代 Context

把之前的專案,用 Jotai 取代 Context 做登入狀態的管理。原本的登入狀態是使用 cookie-session 處理的。

基本上,改用 Jotai 程式碼反而變多了,思路比較徧向以前端的方式解決問題。

以下是遇到的狀況和問題:

  1. 要將 loader 取得的資料放到 store,應該要使用 useHydrateAtoms hook。

  2. 使用 store.get()store.set() 時,似乎不會觸發 re-render;而必須使用 useAtom,應該是訂閱功能做在 useAtom 身上吧。

  3. 使用 Context 時,有些功能用後端功能處理就可以了;但 Jotai 必須使用前端的功能解決。例如登出時,使用 Context 只要在 logout 頁面的 loader() 處理即可。使用 Jotai 必須登出後,使用 useAtom 去變更 store 狀態。使用 google 登入時,有相同的問題。

  4. 經常遇到 Warning: Text content did not match 的問題。解決的方式是在使用 useAtom 的 setter 後,再使用 useNavigate 轉到別的頁面,讓頁面重新 render。

RemixJS, Cloudflare Pages 專案使用 Google Sign-in

RemixJS, Cloudflare Pages 專案使用 Google Sign-in

承上篇的環境設定,要使用 Google sign-in 但不使用官方套件 google-auth-library

可以使用 worker-auth-providers,它雖然還在 beta 版,但簡單的 oauth 登入是可行的。

GCP 基本設定

Google Cloud Platform (GCP) 設定並下載憑證資料:

  1. 到「API 程式庫」>「公開」>「社交」啟用 Google People API。

  2. 到「API和服務 > 憑證」,點選「+建立憑證」>「OAuth 用戶 ID」。

  3. 「應用程式類型」選「網頁應用程式」

  4. 「已授權的 JavaScript 來源」加入正式環境及測試環境的 URI。

  5. 「已授權的重新導向 URI」加入接收 Query String 參數的頁面 URI,同樣包含測試及正式環境。

  6. 建立完成後,「下載 OAuth 用戶端」的 JSON 檔。

  7. 到「OAuth 同意畫面」,設定應用程式名稱,及測試帳號等。

登入頁面

按了「Google 登入」的按鈕轉到 google-signin.tsx 頁面

// app/routes/google-signin.tsx import { type LoaderFunctionArgs, redirect } from "@remix-run/cloudflare"; import { google } from "worker-auth-providers"; import { getMySession } from "~/utils/sessions"; export async function loader({ request, context, params }: LoaderFunctionArgs) { const session = await getMySession(request); const userId = session.get("userId"); if (userId) { return redirect("/"); } const env: any = context.env; const googleLoginUrl = await google.redirect({ options: { clientId: env.GOOGLE_CLIENT_ID, redirectTo: env.GOOGLE_REDIRECT_URI, }, }); return redirect(googleLoginUrl); }

用戶登入後轉向的頁面

// import { type LoaderFunctionArgs, redirect } from "@remix-run/cloudflare"; import { getMySession, getHeadersWithSetCookie } from "~/utils/sessions"; import { google } from "worker-auth-providers"; import getSupabase from "~/utils/supabase-client"; export async function loader({ request, context, params }: LoaderFunctionArgs) { const env: any = context.env; const { user, tokens } = await google.users({ options: { clientSecret: env.GOOGLE_CLIENT_SECRET, clientId: env.GOOGLE_CLIENT_ID, redirectUrl: env.GOOGLE_REDIRECT_URI, }, request, }); // "user": { // "id": "111******", // "email": "shinder.lin@gmail.com", // "verified_email": true, // "name": "Shinder Lin", // "given_name": "Shinder", // "family_name": "Lin", // "picture": "...", // "locale": "zh-TW" // }, // console.log({ user, tokens }); // tokens: { // access_token: 'ya29...', // expires_in: 3599, // scope: 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile openid', // token_type: 'Bearer', // id_token: 'eyJh...' // } if (!user?.id) { return redirect("/login"); } const supabase = getSupabase(context); const { data: data1, error: error1 } = await supabase .from("g_users") .select() .eq("gid", user.id); // 是否已經登入過 if (data1.length) { const row: any = data1[0]; if (row.blocked) { return { msg: "您的帳號已被停權" }; } else { await supabase .from("g_users") .update({ updated_at: new Date() }) .eq("gid", row.gid); } } else { const { data: data2, error: error2 } = await supabase .from("g_users") .insert({ gid: user.id, email: user.email, name: user.name }) .select(); if (!data2.length) { return { msg: "DB 無法新增資料" }; } } const session = await getMySession(request); session.set("gid", user.id); session.set("userId", user.email); session.set("nickname", user.name); const headers = await getHeadersWithSetCookie(session); return redirect('/', { headers }); }

FB 留言