Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python CI actions #8

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@ version: 2
updates:
- package-ecosystem: "gradle"
directory: "/client-samples/java/rest"
schedule:
interval: "weekly"
day: "monday"
time: "08:00"
commit-message:
prefix: "gradle"
prefix: "gradle"

- package-ecosystem: "pip"
directory: "/client-samples/python/rest"
commit-message:
prefix: "pip-rest"

- package-ecosystem: "pip"
directory: "/client-samples/python/websockets"
commit-message:
prefix: "pip-websocket"

schedule:
interval: "weekly"
day: "monday"
time: "08:00"
28 changes: 28 additions & 0 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Python Tests

on:
pull_request:
branches:
- main
jobs:
test-python-rest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
defaults:
run:
working-directory: ./client-samples/python/rest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Upgrade pip
run: python -m pip install --upgrade pip
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run pytest
run: pytest tests/
171 changes: 171 additions & 0 deletions client-samples/python/rest/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
.pybuilder/
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version

# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock

# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# PyPI configuration file
.pypirc
154 changes: 154 additions & 0 deletions client-samples/python/rest/MsRequestsWrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from msal import ConfidentialClientApplication
import json
import logging
import requests
import time
from typing import List, Union


class MsRequestsWrapper:
def __init__(self, config_file: str):
"""
Constructor method for the MsRequestsWrapper class. This class is used to make requests to the Morgan Stanley API.

Parameters
----------
config_file: str
The path to the config file to load.
"""
self.config = self.load_config(config_file)
self.url = self.config["url"]
self.proxies = self.get_proxies(self.config)
self.requests_ca_bundle = self.get_requests_ca_bundle(self.config)
self.app = self.get_client_app(self.config)

def load_config(self, config_file: str):
"""
Load the config map from a JSON file with the given path.

Parameters
----------
config_file: str
The path to the config file to load.
"""
with open(config_file, mode="r") as f:
return json.load(f)

def load_private_key(self, private_key_file: str):
"""
Load the private key from a PEM file with the given path.

Parameters
----------
private_key_file: str
The path to the private key to load.
"""
with open(private_key_file, mode="r") as f:
return f.read()

def get_proxies(self, config: dict) -> Union[dict, None]:
"""
Returns proxy config from the config dictionary if the correct config has been provided.
Otherwise returns None.

Parameters
----------
config: dict
The config map to use.
"""
proxy_host = config.get("proxy_host")
proxy_port = config.get("proxy_port")
proxies = None
if proxy_host is not None:
if proxy_port is None:
raise Exception("Missing proxy port.")
proxies = {
"http": f"{proxy_host}:{proxy_port}",
"https": f"{proxy_host}:{proxy_port}",
}
return proxies

def get_requests_ca_bundle(self, config: dict) -> Union[str, bool]:
"""
Get the system CA bundle, if it's set. This is only necessary if your environment uses a proxy, since the bundled certificates will not work.
This returns True if no CA bundle is set; this tells requests to use the default, bundled certificates.

Parameters
----------
config: dict
The config map to use.

Returns
-------
If SSL has been explicitly disabled: False
If SSL is enabled and should use the default settings: False
If a custom SSL bundle will be used: a string with an absolute path to a .pem file on the system. The config map to use.
"""

if config.get("disable_ssl_verification"):
return False
return config.get("requests_ca_bundle") or True

def get_client_app(self, config: dict):
"""
Configures an MSAL client application, that can later be used to request an access token.

Parameters
----------
config: dict
The config map to use.
"""
client_id = config["client_id"]
thumbprint = config["thumbprint"]
private_key_path = config["private_key_file"]
authority = f"https://login.microsoftonline.com/{config['tenant']}"
proxies = self.get_proxies(config)

private_key = self.load_private_key(private_key_path)

requests_ca_bundle = self.get_requests_ca_bundle(config)

return ConfidentialClientApplication(
client_id=client_id,
authority=authority,
client_credential={"thumbprint": thumbprint, "private_key": private_key},
proxies=proxies,
verify=requests_ca_bundle,
)

def acquire_token(self, app: ConfidentialClientApplication, scopes: List[str]):
"""
Gets an access token against the provided scopes using a pre-configured MSAL app.

Parameters
----------
app: ConfidentialClientApplication
The preconfigured MSAL ConfidentialClientApplication to request a token with.
scopes: List[str]
The list of scopes to request a token against.
"""

result = app.acquire_token_silent(scopes, account=None)

if not result:
print(
"No suitable token exists in cache. Retrieving a new token from Azure AD."
)
result = app.acquire_token_for_client(scopes=scopes)

if "access_token" not in result:
print("Expected an access token in response. Instead, got the following:")
print(result)
raise Exception("Bad response from Azure AD")

return result["access_token"]

def call_api(self):
access_token = self.acquire_token(self.app, self.config["scopes"])

return requests.get( # Use token to call downstream service
self.url,
headers={"Authorization": "Bearer " + access_token},
proxies=self.proxies,
verify=self.requests_ca_bundle,
)
Loading
Loading