diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000..2215bac --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,198 @@ +name: Python Integration Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + repository_dispatch: + types: [verify-new-go-lib-version] + +permissions: + contents: write + +jobs: + # ==================================================== + # 1. BUILD FAKE + # ==================================================== + build-fake: + name: Build fake client + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version-file: "./go/go.mod" + check-latest: true + - run: go version + + - name: Update Go Dependency (If triggered by Go Repo) + if: github.event_name == 'repository_dispatch' + run: | + echo "Triggered by Go Core commit: ${{ github.event.client_payload.go_ref }}" + cd go + go get github.com/pzsp-teams/lib@${{ github.event.client_payload.go_ref }} + go mod tidy + + - name: Compile Fake Binary + run: | + chmod +x ./go/bridge/compileBridges.sh + ./go/bridge/compileBridges.sh fake + + - name: Upload Fake Binary + uses: actions/upload-artifact@v4 + with: + name: fake-binary + path: python/teams_lib_pzsp2_z1/bin/ + retention-days: 1 + + # ==================================================== + # 2. RUN TESTS + # ==================================================== + test-integration: + name: Run Integration Tests + runs-on: ubuntu-latest + needs: build-fake + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download Fake Binary + uses: actions/download-artifact@v4 + with: + name: fake-binary + path: python/teams_lib_pzsp2_z1/bin/ + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: "python/uv.lock" + + - name: Setup Python + run: uv python install 3.12 + + - name: Install dependencies + run: | + cd python + uv sync --dev + + - name: Run Integration Tests + run: | + chmod +x python/teams_lib_pzsp2_z1/bin/* + cd python + uv pip install -e . + uv run pytest -v tests/ + + # ==================================================== + # 3. BUMP VERSION (COMMIT TO MAIN) + # ==================================================== + bump-version: + name: Bump Version on Main + runs-on: ubuntu-latest + needs: test-integration + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.ADMIN_PAT }} + fetch-depth: 0 + + - name: Bump Patch Version + run: | + cd python + python3 -c " + import re + file_path = 'pyproject.toml' + with open(file_path, 'r') as f: + content = f.read() + match = re.search(r'version = \"(\d+\.\d+)\.(\d+)\"', content) + if match: + prefix = match.group(1) + patch = int(match.group(2)) + new_version = f'{prefix}.{patch + 1}' + new_content = re.sub(r'version = \"\d+\.\d+\.\d+\"', f'version = \"{new_version}\"', content) + with open(file_path, 'w') as f: + f.write(new_content) + print(f'Bumped version to {new_version}') + else: + print('Version not found or invalid format') + exit(1) + " + + - name: Commit and Push to Main + run: | + git config --global user.name "GitHub Actions Bot" + git config --global user.email "actions@github.com" + + git add python/pyproject.toml + git commit -m "Bump version [skip ci]" + git push origin main + + # ==================================================== + # 4. PREPARE RELEASE BRANCH + # ==================================================== + update-release-branch: + name: Update Release Branch + runs-on: ubuntu-latest + needs: bump-version + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Pull latest version from main + run: git pull origin main + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: "./go/go.mod" + check-latest: true + + - name: Re-apply Go Dependency Update + if: github.event_name == 'repository_dispatch' + run: | + cd go + go get github.com/pzsp-teams/lib@${{ github.event.client_payload.go_ref }} + go mod tidy + + - name: Compile Real Binaries + run: | + chmod +x ./go/bridge/compileBridges.sh + ./go/bridge/compileBridges.sh real + + - name: Prepare Client Directory + run: | + mkdir python_release + cp -r python/* python_release/ + + grep "version =" python_release/pyproject.toml + + rm -rf python_release/tests + find python_release -type d -name "__pycache__" -exec rm -rf {} + + rm -rf python_release/.venv + + ls -R python_release/teams_lib_pzsp2_z1/bin + + - name: Commit and Push to Release Branch + run: | + git config --global user.name "GitHub Actions Bot" + git config --global user.email "actions@github.com" + git checkout --orphan python-release + + git rm -rf . + cp -r python_release/* . + rm -rf python_release + git add . + + git commit -m "Release build: ${{ github.sha }}" + git push -f origin python-release diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..d60e2ce --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,38 @@ +name: Publish to PyPI + +on: + push: + branches: + - python-release + +jobs: + pypi-publish: + name: Upload release to PyPI + runs-on: ubuntu-latest + + permissions: + contents: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Setup Python + run: uv python install 3.12 + + - name: Install build tools + run: | + uv pip install --system build + + - name: Build the package + run: uv run python -m build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4d4235 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pyc +.egg-info +python/teams_lib_pzsp2_z1.egg-info +python/teams_lib_pzsp2_z1/bin/* +.env +example.py diff --git a/go/.golangci.yml b/go/.golangci.yml new file mode 100644 index 0000000..268a2fe --- /dev/null +++ b/go/.golangci.yml @@ -0,0 +1,43 @@ +version: "2" +run: + tests: true +linters: + enable: + - gocritic + - gocyclo + - misspell + - revive + - unconvert + - unparam + - whitespace + settings: + gocritic: + enabled-tags: + - diagnostic + - performance + - style + gocyclo: + min-complexity: 15 + revive: + rules: + - name: exported + disabled: true + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ +issues: + max-issues-per-linter: 0 + max-same-issues: 0 +formatters: + enable: + - gofmt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ \ No newline at end of file diff --git a/go/bridge/compileBridges.sh b/go/bridge/compileBridges.sh new file mode 100755 index 0000000..b058e5c --- /dev/null +++ b/go/bridge/compileBridges.sh @@ -0,0 +1,42 @@ +#!/bin/bash +set -e + +# 1. Zapamiętajmy gdzie jesteśmy (katalog główny projektu) +PROJECT_ROOT=$(pwd) + +# 2. Definiujemy ścieżkę wyjściową (absolutną, żeby działała po zmianie katalogu) +BIN_DIR="$PROJECT_ROOT/python/teams_lib_pzsp2_z1/bin" +mkdir -p "$BIN_DIR" + +# 3. Wchodzimy do katalogu 'go', gdzie leży plik go.mod +cd go + +# Ścieżka do pakietu mostka (teraz relatywna względem katalogu 'go') +BRIDGE_PKG="./bridge" + +# Read the mode from the first argument +MODE=$1 + +if [[ "$MODE" == "real" ]]; then + echo "=== Building REAL mode (Production) ===" + + echo "Building Linux (real)..." + GOOS=linux GOARCH=amd64 go build -tags real -o "$BIN_DIR/teamsClientLib_linux" "$BRIDGE_PKG" + + echo "Building Windows (real)..." + GOOS=windows GOARCH=amd64 go build -tags real -o "$BIN_DIR/teamsClientLib_windows.exe" "$BRIDGE_PKG" + +elif [[ "$MODE" == "fake" ]]; then + echo "=== Building FAKE mode (Integration Tests) ===" + + echo "Building Linux (fake)..." + GOOS=linux GOARCH=amd64 go build -tags fake -o "$BIN_DIR/teamsClientLib_linux" "$BRIDGE_PKG" + +else + echo "Error: Invalid argument. Usage: $0 [real|fake]" + echo " real - builds for Linux and Windows with production code" + echo " fake - builds for Linux only with mock capability" + exit 1 +fi + +echo "Done! Binaries saved in $BIN_DIR" \ No newline at end of file diff --git a/go/bridge/main.go b/go/bridge/main.go new file mode 100644 index 0000000..ec5052b --- /dev/null +++ b/go/bridge/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + + jsonClientLib "github.com/pzsp-teams/lib-python/internal/json-client" + jsonModel "github.com/pzsp-teams/lib-python/internal/json-model" +) + +var client *jsonClientLib.TeamsJSONClient +var initialized bool + +func main() { + scanner := bufio.NewScanner(os.Stdin) + scanner.Buffer(make([]byte, 0, 1024), 1024*1024) + writer := bufio.NewWriter(os.Stdout) + + for scanner.Scan() { + line := scanner.Text() + + var req jsonModel.Request + err := json.Unmarshal([]byte(line), &req) + if err != nil { + respondError(writer, fmt.Errorf("invalid json: %w", err)) + continue + } + + if req.Type == "init" { + if initialized { + respondError(writer, fmt.Errorf("client already initialized")) + continue + } + + c, err := jsonClientLib.NewJSONClient(req) + if detectFail(writer, err) { + continue + } + + client = c + initialized = true + respondResult(writer, "initialized") + continue + } + + if req.Type == "request" { + if client == nil { + respondError(writer, fmt.Errorf("client not initialized")) + continue + } + + switch req.Method { + case "listChannels": + channels, err := client.ListChannels(req.Params) + if err != nil { + respondError(writer, err) + } else { + respondResult(writer, channels) + } + default: + respondError(writer, fmt.Errorf("unknown method")) + } + continue + } + respondError(writer, fmt.Errorf("unknown request type")) + } +} diff --git a/go/bridge/responses.go b/go/bridge/responses.go new file mode 100644 index 0000000..ff2c4f2 --- /dev/null +++ b/go/bridge/responses.go @@ -0,0 +1,40 @@ +package main + +import ( + "bufio" + "encoding/json" +) + +type response struct { + Result interface{} `json:"result,omitempty"` + Error string `json:"error,omitempty"` +} + +func detectFail(writer *bufio.Writer, err error) bool { + if err != nil { + respondError(writer, err) + return true + } + return false +} + + +func respondError(writer *bufio.Writer, err error) { + response := response{ + Error: err.Error(), + } + respBytes, _ := json.Marshal(response) + writer.Write(respBytes) + writer.WriteString("\n") + writer.Flush() +} + +func respondResult(writer *bufio.Writer, result interface{}) { + response := response{ + Result: result, + } + respBytes, _ := json.Marshal(response) + writer.Write(respBytes) + writer.WriteString("\n") + writer.Flush() +} \ No newline at end of file diff --git a/go/go.mod b/go/go.mod new file mode 100644 index 0000000..75df08b --- /dev/null +++ b/go/go.mod @@ -0,0 +1,42 @@ +module github.com/pzsp-teams/lib-python + +go 1.25.5 + +require ( + github.com/microsoft/kiota-abstractions-go v1.9.3 + github.com/microsoftgraph/msgraph-sdk-go v1.90.0 + github.com/pzsp-teams/lib v0.0.0-20251213104555-e41d3adf599f +) + +require ( + al.essio.dev/pkg/shellescape v1.5.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/danieljoos/wincred v1.2.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/microsoft/kiota-authentication-azure-go v1.3.1 // indirect + github.com/microsoft/kiota-http-go v1.5.4 // indirect + github.com/microsoft/kiota-serialization-form-go v1.1.2 // indirect + github.com/microsoft/kiota-serialization-json-go v1.1.2 // indirect + github.com/microsoft/kiota-serialization-multipart-go v1.1.2 // indirect + github.com/microsoft/kiota-serialization-text-go v1.1.3 // indirect + github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 // indirect + github.com/zalando/go-keyring v0.2.6 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect +) diff --git a/go/go.sum b/go/go.sum new file mode 100644 index 0000000..e345e5c --- /dev/null +++ b/go/go.sum @@ -0,0 +1,88 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= +github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 h1:rG3VnJUnAWyiv7qYmmdOdSapzz6HM+zb9/uRFr0T5EM= +github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/microsoft/kiota-abstractions-go v1.9.3 h1:cqhbqro+VynJ7kObmo7850h3WN2SbvoyhypPn8uJ1SE= +github.com/microsoft/kiota-abstractions-go v1.9.3/go.mod h1:f06pl3qSyvUHEfVNkiRpXPkafx7khZqQEb71hN/pmuU= +github.com/microsoft/kiota-authentication-azure-go v1.3.1 h1:AGta92S6IL1E6ZMDb8YYB7NVNTIFUakbtLKUdY5RTuw= +github.com/microsoft/kiota-authentication-azure-go v1.3.1/go.mod h1:26zylt2/KfKwEWZSnwHaMxaArpbyN/CuzkbotdYXF0g= +github.com/microsoft/kiota-http-go v1.5.4 h1:wSUmL1J+bTQlAWHjbRkSwr+SPAkMVYeYxxB85Zw0KFs= +github.com/microsoft/kiota-http-go v1.5.4/go.mod h1:L+5Ri+SzwELnUcNA0cpbFKp/pBbvypLh3Cd1PR6sjx0= +github.com/microsoft/kiota-serialization-form-go v1.1.2 h1:SD6MATqNw+Dc5beILlsb/D87C36HKC/Zw7l+N9+HY2A= +github.com/microsoft/kiota-serialization-form-go v1.1.2/go.mod h1:m4tY2JT42jAZmgbqFwPy3zGDF+NPJACuyzmjNXeuHio= +github.com/microsoft/kiota-serialization-json-go v1.1.2 h1:eJrPWeQ665nbjO0gsHWJ0Bw6V/ZHHU1OfFPaYfRG39k= +github.com/microsoft/kiota-serialization-json-go v1.1.2/go.mod h1:deaGt7fjZarywyp7TOTiRsjfYiyWxwJJPQZytXwYQn8= +github.com/microsoft/kiota-serialization-multipart-go v1.1.2 h1:1pUyA1QgIeKslQwbk7/ox1TehjlCUUT3r1f8cNlkvn4= +github.com/microsoft/kiota-serialization-multipart-go v1.1.2/go.mod h1:j2K7ZyYErloDu7Kuuk993DsvfoP7LPWvAo7rfDpdPio= +github.com/microsoft/kiota-serialization-text-go v1.1.3 h1:8z7Cebn0YAAr++xswVgfdxZjnAZ4GOB9O7XP4+r5r/M= +github.com/microsoft/kiota-serialization-text-go v1.1.3/go.mod h1:NDSvz4A3QalGMjNboKKQI9wR+8k+ih8UuagNmzIRgTQ= +github.com/microsoftgraph/msgraph-sdk-go v1.90.0 h1:ygVeWfGB8TMO4rTFxtrYueZmj1mLqtDKW5UZ4iJwczU= +github.com/microsoftgraph/msgraph-sdk-go v1.90.0/go.mod h1:UdZWxbZiFvjPug9DYayD90JNiHjXyNRA39lEpcy3Kms= +github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0 h1:0SrIoFl7TQnMRrsi5TFaeNe0q8KO5lRzRp4GSCCL2So= +github.com/microsoftgraph/msgraph-sdk-go-core v1.4.0/go.mod h1:A1iXs+vjsRjzANxF6UeKv2ACExG7fqTwHHbwh1FL+EE= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pzsp-teams/lib v0.0.0-20251208073120-ca1606f2b27d h1:hFdPrkbnUngxrGdMkPVZpvY76T6QgnXo87u9wzXgNWY= +github.com/pzsp-teams/lib v0.0.0-20251208073120-ca1606f2b27d/go.mod h1:uGrgoO+KmBc55Le/AeysA0d24HOCQHO55Uw5Z7naQo4= +github.com/pzsp-teams/lib v0.0.0-20251213104555-e41d3adf599f h1:tmXKJEWYl0f/H4n91wnfW1asgkHbftKgX5nbpafNHmU= +github.com/pzsp-teams/lib v0.0.0-20251213104555-e41d3adf599f/go.mod h1:EPtQVPz+HYZNsSpcoQ3QWLbMEz1O2qeIH5ojlYJLEic= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3 h1:7hth9376EoQEd1hH4lAp3vnaLP2UMyxuMMghLKzDHyU= +github.com/std-uritemplate/std-uritemplate/go/v2 v2.0.3/go.mod h1:Z5KcoM0YLC7INlNhEezeIZ0TZNYf7WSNO0Lvah4DSeQ= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/internal/json-client/fake-client.go b/go/internal/json-client/fake-client.go new file mode 100644 index 0000000..2d731ca --- /dev/null +++ b/go/internal/json-client/fake-client.go @@ -0,0 +1,94 @@ +//go:build fake + +package jsonclient + +import ( + "context" + "crypto/tls" + "fmt" + "net/http" + "net/url" + + azcore "github.com/microsoft/kiota-abstractions-go" + graph "github.com/microsoftgraph/msgraph-sdk-go" + + lib "github.com/pzsp-teams/lib" + jsonModel "github.com/pzsp-teams/lib-python/internal/json-model" +) + +// --- 1. Fake Auth Provider --- +type FakeAuthProvider struct{} + +func (f *FakeAuthProvider) AuthenticateRequest(ctx context.Context, request *azcore.RequestInformation, additionalAuthenticationContext map[string]interface{}) error { + if request.Headers == nil { + request.Headers = azcore.NewRequestHeaders() + } + request.Headers.Add("Authorization", "Bearer fake-jwt-token") + + return nil +} + +// --- 2. Replace MS API with Python Mock Server --- +type HijackTransport struct { + MockServerURL string + Transport http.RoundTripper +} + +func (t *HijackTransport) RoundTrip(req *http.Request) (*http.Response, error) { + parsed, err := url.Parse(t.MockServerURL) + if err != nil { + return nil, err + } + + req.URL.Scheme = parsed.Scheme + req.URL.Host = parsed.Host + + return t.Transport.RoundTrip(req) +} + +// --- 3. Fake JSON Client Factory --- +func NewJSONClient(req jsonModel.Request) (*TeamsJSONClient, error) { + // Parse parameters + mockServerURL, ok := req.Params["mockServerUrl"].(string) + if !ok || mockServerURL == "" { + return nil, fmt.Errorf("invalid mockServerUrl parameter") + } + + senderConfig := lib.SenderConfig{ + MaxRetries: 3, + NextRetryDelay: 2, + Timeout: 5, + } + + cacheEnabled := false + var cachePath *string = nil + + hijackedHttpClient := &http.Client{ + Transport: &HijackTransport{ + MockServerURL: mockServerURL, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + }, + } + authProvider := &FakeAuthProvider{} + + adapter, err := graph.NewGraphRequestAdapterWithParseNodeFactoryAndSerializationWriterFactoryAndHttpClient( + authProvider, + nil, // default ParseNodeFactory + nil, // default SerializationWriterFactory + hijackedHttpClient, + ) + if err != nil { + return nil, err + } + + graphClient := graph.NewGraphServiceClient(adapter) + + client, err := lib.NewClientFromGraphClient(graphClient, &senderConfig, cacheEnabled, cachePath) + if err != nil { + return nil, err + } + + return &TeamsJSONClient{client}, nil +} diff --git a/go/internal/json-client/real-client.go b/go/internal/json-client/real-client.go new file mode 100644 index 0000000..a00178c --- /dev/null +++ b/go/internal/json-client/real-client.go @@ -0,0 +1,44 @@ +//go:build real + +package jsonclient + +import ( + "context" + + lib "github.com/pzsp-teams/lib" + jsonModel "github.com/pzsp-teams/lib-python/internal/json-model" +) + +func NewJSONClient(req jsonModel.Request) (*TeamsJSONClient, error) { + authConfig, err := req.Config.AuthConfigMap.ToAuthConfig() + if err != nil { + return nil, err + } + + senderConfig, err := req.Config.SenderConfigMap.ToSenderConfig() + if err != nil { + return nil, err + } + + cacheEnabled, err := jsonModel.ParseCacheEnabled(req.Params["cacheEnabled"]) + if err != nil { + return nil, err + } + + var cachePath *string + if cacheEnabled { + path, err := jsonModel.ParseCachePath(req.Params["cachePath"]) + if err != nil { + return nil, err + } + cachePath = path + } + + + client, err := lib.NewClient(context.TODO(), authConfig, senderConfig, cacheEnabled, cachePath) + if err != nil { + return nil, err + } + + return &TeamsJSONClient{client}, nil +} diff --git a/go/internal/json-client/requests-channels.go b/go/internal/json-client/requests-channels.go new file mode 100644 index 0000000..cdfb18a --- /dev/null +++ b/go/internal/json-client/requests-channels.go @@ -0,0 +1,23 @@ +package jsonclient + +import ( + "context" + "fmt" +) + +type ListChannelsParams struct { + TeamRef string `json:"teamRef"` +} + +func (jsonclient *TeamsJSONClient) ListChannels(params map[string]interface{}) (interface{}, error) { + teamRef, ok := params["teamRef"].(string) + if !ok || teamRef == "" { + return nil, fmt.Errorf("invalid teamRef parameter") + } + channels, err := jsonclient.client.Channels.ListChannels(context.TODO(), teamRef) + if err != nil { + return nil, err + } else { + return channels, nil + } +} diff --git a/go/internal/json-client/teams-json-client.go b/go/internal/json-client/teams-json-client.go new file mode 100644 index 0000000..afe9430 --- /dev/null +++ b/go/internal/json-client/teams-json-client.go @@ -0,0 +1,9 @@ +package jsonclient + +import ( + lib "github.com/pzsp-teams/lib" +) + +type TeamsJSONClient struct { + client *lib.Client +} diff --git a/go/internal/json-model/json-model.go b/go/internal/json-model/json-model.go new file mode 100644 index 0000000..3d76018 --- /dev/null +++ b/go/internal/json-model/json-model.go @@ -0,0 +1,111 @@ +package jsonmodel + +import ( + "fmt" + + "github.com/pzsp-teams/lib" +) + +type Request struct { + Type string `json:"type"` + Method string `json:"method,omitempty"` + Config Config `json:"config,omitempty"` + Params map[string]interface{} `json:"params,omitempty"` +} + +type Config struct { + SenderConfigMap SenderConfigMap `json:"senderConfig"` + AuthConfigMap AuthConfigMap `json:"authConfig"` + CacheEnabled bool `json:"cacheEnabled"` + CachePath string `json:"cachePath,omitempty"` +} + +type SenderConfigMap struct { + MaxRetries int `json:"maxRetries"` + NextRetryDelay int `json:"nextRetryDelay"` + Timeout int `json:"timeout"` +} + +func (scm SenderConfigMap) ToSenderConfig() (*lib.SenderConfig, error) { + if scm.MaxRetries < 0 { + return nil, fmt.Errorf("maxRetries cannot be negative") + } + if scm.NextRetryDelay < 0 { + return nil, fmt.Errorf("nextRetryDelay cannot be negative") + } + if scm.Timeout <= 0 { + return nil, fmt.Errorf("timeout must be > 0") + } + + return &lib.SenderConfig{ + MaxRetries: scm.MaxRetries, + NextRetryDelay: scm.NextRetryDelay, + Timeout: scm.Timeout, + }, nil +} + +type AuthConfigMap struct { + ClientID string `json:"clientId"` + Tenant string `json:"tenant"` + Email string `json:"email"` + Scopes []string `json:"scopes"` + AuthMethod string `json:"authMethod"` +} + +func (acm AuthConfigMap) ToAuthConfig() (*lib.AuthConfig, error) { + if acm.ClientID == "" { + return nil, fmt.Errorf("clientId is required") + } + if acm.Tenant == "" { + return nil, fmt.Errorf("tenant is required") + } + if acm.Email == "" { + return nil, fmt.Errorf("email is required") + } + if len(acm.Scopes) == 0 { + return nil, fmt.Errorf("scopes cannot be empty") + } + if acm.AuthMethod == "" { + return nil, fmt.Errorf("authMethod is required") + } + + authMethod, err := validateAuthMethod(acm.AuthMethod) + if err != nil { + return nil, err + } + + return &lib.AuthConfig{ + ClientID: acm.ClientID, + Tenant: acm.Tenant, + Email: acm.Email, + Scopes: acm.Scopes, + AuthMethod: authMethod, + }, nil +} + +func ParseCacheEnabled(value interface{}) (bool, error) { + if value == nil { + return false, nil // default + } + + enabled, ok := value.(bool) + if !ok { + return false, fmt.Errorf("cacheEnabled must be boolean") + } + return enabled, nil +} + +func ParseCachePath(value interface{}) (*string, error) { + path, ok := value.(string) + if !ok || path == "" { + return nil, fmt.Errorf("cachePath must be a non-empty string") + } + return &path, nil +} + +func validateAuthMethod(method string) (string, error) { + if method == "DEVICE_CODE" || method == "INTERACTIVE" { + return method, nil + } + return "", fmt.Errorf("invalid auth method: %s", method) +} diff --git a/python/.python-version b/python/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/python/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/python/MANIFEST.in b/python/MANIFEST.in new file mode 100644 index 0000000..44af821 --- /dev/null +++ b/python/MANIFEST.in @@ -0,0 +1 @@ +recursive-include teams_lib_pzsp2_z1/bin * \ No newline at end of file diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..e69de29 diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..74dfadb --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "teams_lib_pzsp2_z1" +version = "0.1.0" +description = "Bridge to Go client for Teams API" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "dotenv>=0.9.9", + "pytest-httpserver>=1.1.3", +] + +[dependency-groups] +dev = [ + "pytest>=9.0.0", + "ruff>=0.14.4", +] + +[tool.ruff] +line-length = 88 +indent-width = 4 +target-version = "py312" +fix = true +show-fixes = true +src = ["teams_lib_pzsp2_z1"] +exclude = ["test_*.py", "tests/*"] + + +[tool.ruff.lint] +extend-select = ["I", "E", "W", "F", "C90", "B", "S", "UP", "PL"] + +[tool.setuptools] +include-package-data = true diff --git a/python/requirements.txt b/python/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/python/teams_lib_pzsp2_z1/__init__.py b/python/teams_lib_pzsp2_z1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/teams_lib_pzsp2_z1/client.py b/python/teams_lib_pzsp2_z1/client.py new file mode 100644 index 0000000..584d1f1 --- /dev/null +++ b/python/teams_lib_pzsp2_z1/client.py @@ -0,0 +1,117 @@ +import json +import pathlib +import platform +import subprocess +import threading +from typing import Any + +from teams_lib_pzsp2_z1 import config +from teams_lib_pzsp2_z1.services.channels import ChannelsService + + +class TeamsClient: + def __init__( + self, + auto_init: bool = True, + cache_enabled: bool = False, + cache_path: str | None = None, + ): + self._lock = threading.Lock() + + self.proc = subprocess.Popen( # noqa: S603 + [str(self._binary())], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + bufsize=1, + ) + + self.channels = ChannelsService(self) + + if auto_init: + self.init_client(cache_enabled, cache_path) + + def _binary(self): + base = pathlib.Path(__file__).parent / "bin" + osname = platform.system() + + if osname == "Windows": + return base / "teamsClientLib_windows.exe" + elif osname == "Linux": + return base / "teamsClientLib_linux" + else: + raise RuntimeError("Unsupported OS") + + def init_client( + self, cache_enabled: bool = False, cache_path: str | None = None + ) -> Any: + sender_config = config.SenderConfig() + auth_config = config.load_auth_config() + return self.execute( + cmd_type="init", + config={ + "senderConfig": { + "maxRetries": sender_config.max_retries, + "nextRetryDelay": sender_config.next_retry_delay, + "timeout": sender_config.timeout, + }, + "authConfig": { + "clientID": auth_config.client_id, + "tenant": auth_config.tenant, + "email": auth_config.email, + "scopes": auth_config.scopes, + "authMethod": auth_config.auth_method, + }, + "cacheEnabled": cache_enabled, + "cachePath": cache_path, + }, + ) + + def init_fake_client(self, mock_server_url: str) -> Any: + return self.execute( + cmd_type="init", + params={ + "mockServerUrl": mock_server_url, + }, + ) + + def execute( + self, + cmd_type: str, + method: str | None = None, + config: dict[str, Any] | None = None, + params: dict[str, Any] | None = None, + ) -> Any: + payload = {"type": cmd_type} + if method: + payload["method"] = method + if params: + payload["params"] = params + if config: + payload["config"] = config + + json_payload = json.dumps(payload) + + # Critical section to avoid interleaving requests/responses + with self._lock: + try: + self.proc.stdin.write(json_payload + "\n") + self.proc.stdin.flush() + + raw_response = self.proc.stdout.readline() + except BrokenPipeError: + raise RuntimeError("Go process crashed or closed connection") # noqa: B904 + + if not raw_response: + raise RuntimeError("Go process returned empty response") + + res = json.loads(raw_response) + + if "error" in res and res["error"]: + raise RuntimeError(f"Go Error: {res['error']}") + + return res.get("result") + + def close(self): + self.proc.terminate() diff --git a/python/teams_lib_pzsp2_z1/config.py b/python/teams_lib_pzsp2_z1/config.py new file mode 100644 index 0000000..77440e9 --- /dev/null +++ b/python/teams_lib_pzsp2_z1/config.py @@ -0,0 +1,61 @@ +import os +import sys +from dataclasses import dataclass + +from dotenv import load_dotenv + + +@dataclass +class SenderConfig: + max_retries: int = 3 + next_retry_delay: int = 2 + timeout: int = 10 + + +@dataclass +class AuthConfig: + client_id: str + tenant: str + email: str + scopes: list[str] + auth_method: str + + +def load_auth_config() -> AuthConfig: + load_dotenv() + + cfg = AuthConfig( + client_id=get_env("CLIENT_ID", ""), + tenant=get_env("TENANT_ID", ""), + email=get_env("EMAIL", ""), + scopes=get_env( + "SCOPES", + "https://graph.microsoft.com/.default" + ).split(","), + auth_method=get_env("AUTH_METHOD", "DEVICE_CODE"), + ) + + validate(cfg) + return cfg + + +def get_env(key: str, fallback: str) -> str: + return os.getenv(key, fallback) + + +def validate(cfg: AuthConfig): + if not cfg.client_id: + print("Missing CLIENT ID", file=sys.stderr) + sys.exit(1) + + if not cfg.tenant: + print("Missing TENANT ID", file=sys.stderr) + sys.exit(1) + + if not cfg.email: + print("Missing EMAIL", file=sys.stderr) + sys.exit(1) + + if cfg.auth_method not in ("DEVICE_CODE", "INTERACTIVE"): + print("AUTH METHOD must be either DEVICE_CODE or INTERACTIVE", file=sys.stderr) + sys.exit(1) diff --git a/python/teams_lib_pzsp2_z1/model/channel.py b/python/teams_lib_pzsp2_z1/model/channel.py new file mode 100644 index 0000000..ace8276 --- /dev/null +++ b/python/teams_lib_pzsp2_z1/model/channel.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class Channel: + ID: str + Name: str + IsGeneral: bool diff --git a/python/teams_lib_pzsp2_z1/model/team.py b/python/teams_lib_pzsp2_z1/model/team.py new file mode 100644 index 0000000..9de6e1b --- /dev/null +++ b/python/teams_lib_pzsp2_z1/model/team.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + + +@dataclass +class Team: + ID: str + DisplayName: str + Description: str + IsArchived: bool + Visibility: str diff --git a/python/teams_lib_pzsp2_z1/services/base_service.py b/python/teams_lib_pzsp2_z1/services/base_service.py new file mode 100644 index 0000000..073288d --- /dev/null +++ b/python/teams_lib_pzsp2_z1/services/base_service.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from teams_lib_pzsp2_z1.client import TeamsClient + +class BaseService: + def __init__(self, client: TeamsClient): + self.client = client diff --git a/python/teams_lib_pzsp2_z1/services/channels.py b/python/teams_lib_pzsp2_z1/services/channels.py new file mode 100644 index 0000000..303e74e --- /dev/null +++ b/python/teams_lib_pzsp2_z1/services/channels.py @@ -0,0 +1,21 @@ +from teams_lib_pzsp2_z1.model.channel import Channel +from teams_lib_pzsp2_z1.services.base_service import BaseService + + +class ChannelsService(BaseService): + def list_channels(self, teamRef: str) -> list[Channel]: + response = self.client.execute( + cmd_type="request", + method="listChannels", + params={ + "teamRef": teamRef, + }, + ) + return [ + Channel( + ID=channel["ID"], + Name=channel["Name"], + IsGeneral=(True if channel["IsGeneral"] else False), + ) + for channel in response + ] diff --git a/python/tests/__init__.py b/python/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/tests/fake_server/data.py b/python/tests/fake_server/data.py new file mode 100644 index 0000000..d917f45 --- /dev/null +++ b/python/tests/fake_server/data.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass +from teams_lib_pzsp2_z1.model.team import Team +from teams_lib_pzsp2_z1.model.channel import Channel + +@dataclass +class FakeServerData: + teams: list[Team] + channels: dict[str, list[Channel]] + + def __init__(self) -> FakeServerData: + self.teams = [ + Team( + ID="team-123-abc", + DisplayName="Test Team", + Description="A team for testing", + IsArchived=False, + Visibility="Private", + ), + ] + self.channels = { + "team-123-abc": [ + Channel( + ID="19:123123@thread.tacv2", + Name="General", + IsGeneral=True, + ), + Channel( + ID="19:999999@thread.tacv2", + Name="Development", + IsGeneral=False, + ), + ], + } + + def get_myJoinedTeams_response(self) -> dict: + return { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#teams", + "value": [ + { + "id": team.ID, + "displayName": team.DisplayName, + "description": team.Description, + "isArchived": team.IsArchived, + "visibility": team.Visibility, + } + for team in self.teams + ], + } + + def get_listChannels_response(self, team_id: str) -> dict: + return { + "@odata.context": f"https://graph.microsoft.com/v1.0/$metadata#teams('{team_id}')/channels", + "value": [ + { + "id": channel.ID, + "displayName": channel.Name, + "isGeneral": channel.IsGeneral, + } + for channel in self.channels.get(team_id, []) + ], + } \ No newline at end of file diff --git a/python/tests/fake_server/setup.py b/python/tests/fake_server/setup.py new file mode 100644 index 0000000..aa92527 --- /dev/null +++ b/python/tests/fake_server/setup.py @@ -0,0 +1,21 @@ +from tests.fake_server.data import FakeServerData + +def setup_fake_server(httpserver) -> FakeServerData: + """ + Sets up a fake HTTP server to simulate Microsoft Graph API responses. + """ + + data = FakeServerData() + + # Mock response for listing joined teams + httpserver.expect_request( + "/v1.0/users/me-token-to-replace/joinedTeams", method="GET" + ).respond_with_json(data.get_myJoinedTeams_response()) + + # Mock response for listing channels in the fake team + httpserver.expect_request( + f"/v1.0/teams/{data.teams[0].ID}/channels", method="GET" + ).respond_with_json(data.get_listChannels_response(data.teams[0].ID)) + + return data + diff --git a/python/tests/init_fake_client.py b/python/tests/init_fake_client.py new file mode 100644 index 0000000..1cda919 --- /dev/null +++ b/python/tests/init_fake_client.py @@ -0,0 +1,16 @@ +from typing import Any +from teams_lib_pzsp2_z1 import config +from teams_lib_pzsp2_z1.client import TeamsClient + +def init_fake_client(client: TeamsClient, mock_server_url: str) -> Any: + """ + Helper function to initialize the Go client in FAKE mode. + It injects the mockServerUrl. + """ + + return client.execute( + cmd_type="init", + params={ + "mockServerUrl": mock_server_url, + } + ) \ No newline at end of file diff --git a/python/tests/test_channels.py b/python/tests/test_channels.py new file mode 100644 index 0000000..121f315 --- /dev/null +++ b/python/tests/test_channels.py @@ -0,0 +1,31 @@ +from teams_lib_pzsp2_z1.client import TeamsClient +from tests.init_fake_client import init_fake_client +from tests.fake_server.setup import setup_fake_server + + +def test_list_channels_integration(httpserver): + """ + Integration test: Python -> Go Binary -> Fake HTTP -> Python Mock Server + """ + + data = setup_fake_server(httpserver) + + # Init fake client + client = TeamsClient(auto_init=False) + try: + init_fake_client(client, httpserver.url_for("")) + + channels = client.channels.list_channels(data.teams[0].DisplayName) + + assert len(channels) == len(data.channels[data.teams[0].ID]) + + assert channels[0].Name == data.channels[data.teams[0].ID][0].Name + assert channels[0].ID == data.channels[data.teams[0].ID][0].ID + assert channels[0].IsGeneral == data.channels[data.teams[0].ID][0].IsGeneral + + assert channels[1].Name == data.channels[data.teams[0].ID][1].Name + assert channels[1].ID == data.channels[data.teams[0].ID][1].ID + assert channels[1].IsGeneral == data.channels[data.teams[0].ID][1].IsGeneral + + finally: + client.close() diff --git a/python/uv.lock b/python/uv.lock new file mode 100644 index 0000000..bc82c5f --- /dev/null +++ b/python/uv.lock @@ -0,0 +1,224 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-httpserver" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/d8/def15ba33bd696dd72dd4562a5287c0cba4d18a591eeb82e0b08ab385afc/pytest_httpserver-1.1.3.tar.gz", hash = "sha256:af819d6b533f84b4680b9416a5b3f67f1df3701f1da54924afd4d6e4ba5917ec", size = 68870, upload-time = "2025-04-10T08:17:15.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/d2/dfc2f25f3905921c2743c300a48d9494d29032f1389fc142e718d6978fb2/pytest_httpserver-1.1.3-py3-none-any.whl", hash = "sha256:5f84757810233e19e2bb5287f3826a71c97a3740abe3a363af9155c0f82fdbb9", size = 21000, upload-time = "2025-04-10T08:17:13.906Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385, upload-time = "2025-12-04T15:06:17.669Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540, upload-time = "2025-12-04T15:06:14.896Z" }, + { url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384, upload-time = "2025-12-04T15:06:51.809Z" }, + { url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917, upload-time = "2025-12-04T15:06:08.925Z" }, + { url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112, upload-time = "2025-12-04T15:06:23.498Z" }, + { url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559, upload-time = "2025-12-04T15:06:33.432Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379, upload-time = "2025-12-04T15:06:02.687Z" }, + { url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786, upload-time = "2025-12-04T15:06:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029, upload-time = "2025-12-04T15:06:36.812Z" }, + { url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037, upload-time = "2025-12-04T15:06:39.979Z" }, + { url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390, upload-time = "2025-12-04T15:06:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793, upload-time = "2025-12-04T15:06:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039, upload-time = "2025-12-04T15:06:49.06Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158, upload-time = "2025-12-04T15:06:54.574Z" }, + { url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550, upload-time = "2025-12-04T15:05:59.209Z" }, + { url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332, upload-time = "2025-12-04T15:06:06.027Z" }, + { url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890, upload-time = "2025-12-04T15:06:11.668Z" }, + { url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826, upload-time = "2025-12-04T15:06:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522, upload-time = "2025-12-04T15:06:43.212Z" }, +] + +[[package]] +name = "teams-lib-pzsp2-z1" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "dotenv" }, + { name = "pytest-httpserver" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "pytest-httpserver", specifier = ">=1.1.3" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.0" }, + { name = "ruff", specifier = ">=0.14.4" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, +] diff --git a/scripts/commit-msg.config.json b/scripts/commit-msg.config.json new file mode 100644 index 0000000..b22c860 --- /dev/null +++ b/scripts/commit-msg.config.json @@ -0,0 +1,20 @@ +{ + "enabled": true, + "revert": true, + "length": { + "min": 1, + "max": 52 + }, + "types": [ + "build", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "style", + "test", + "chore" + ] +} diff --git a/scripts/setup-commit-msg-hook b/scripts/setup-commit-msg-hook new file mode 100644 index 0000000..69642fb --- /dev/null +++ b/scripts/setup-commit-msg-hook @@ -0,0 +1,78 @@ +#!/usr/bin/env bash + +set -e + +HOOK_FILE=".git/hooks/commit-msg" +CONFIG_FILE="./scripts/commit-msg.config.json" + +if ! command -v jq >/dev/null 2>&1; then + echo "jq not found. Installing..." + + if command -v apt >/dev/null 2>&1; then + sudo apt update && sudo apt install -y jq + elif command -v dnf >/dev/null 2>&1; then + sudo dnf install -y jq + elif command -v brew >/dev/null 2>&1; then + brew install jq + else + echo "No supported package manager detected. Install jq manually." + exit 1 + fi +else + echo "jq already installed." +fi + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Missing $CONFIG_FILE – create it before using this hook." + exit 1 +fi + +mkdir -p .git/hooks + +cat >"$HOOK_FILE" <<'EOF' +#!/usr/bin/env bash + +config=./scripts/commit-msg.config.json + +# set variables +enabled=$(jq -r .enabled $config) +revert=$(jq -r .revert $config) +types=($(jq -r '.types[]' $config)) +min_length=$(jq -r .length.min $config) +max_length=$(jq -r .length.max $config) + +if [[ ! -f $config || ! $enabled ]]; then + exit 0 +fi + +regexp="^(" + +if $revert; then + regexp="${regexp}revert: )?(\w+)(" +fi + +for type in "${types[@]}" +do + regexp="${regexp}$type|" +done + +regexp="${regexp})(\(.+\))?: " + +regexp="${regexp}.{$min_length,$max_length}$" + +msg=$(head -1 "$1") + +if [[ ! $msg =~ $regexp ]]; then + echo -e "\n\e[1m\e[31m[INVALID COMMIT MESSAGE]" + echo -e "------------------------\033[0m\e[0m" + echo -e "\e[1mValid types:\e[0m \e[34m${types[@]}\033[0m" + echo -e "\e[1mMax length (first line):\e[0m \e[34m$max_length\033[0m" + echo -e "\e[1mMin length (first line):\e[0m \e[34m$min_length\033[0m\n" + + exit 1 +fi +EOF + +chmod +x "$HOOK_FILE" + +echo "commit-msg hook installed successfully."