Api v1.16.0 (#1744)

* feat(api): usability testing (#1686)

* feat(api): usability testing - wip

* feat(db): usabiity testing

* feat(api): usability testing - api

* feat(api): usability testing - api

* feat(api): usability testing - db change

* feat(api): usability testing - db change

* feat(api): usability testing - unit tests update

* feat(api): usability testing - test and tasks stats

* feat(api): usability testing - sessions list fix, return zeros if test id is not having signals

* Api v1.16.0 (#1698)

* feat: canvas support [assist] (#1641)

* feat(tracker/ui): start canvas support

* feat(tracker): slpeer -> peerjs for canvas streams

* fix(ui): fix agent canvas peer id

* fix(ui): fix agent canvas peer id

* fix(ui): fix peer removal

* feat(tracker): canvas recorder

* feat(tracker): canvas recorder

* feat(tracker): canvas recorder

* feat(tracker): canvas recorder

* feat(ui): canvas support for ui

* fix(tracker): fix falling tests

* feat(ui): replay canvas in video

* feat(ui): refactor video streaming to draw on canvas

* feat(ui): 10hz check for canvas replay

* feat(ui): fix for tests

* feat(ui): fix for tests

* feat(ui): fix for tests

* feat(ui): fix for tests cov

* feat(ui): mroe test coverage

* fix(ui): styling

* fix(tracker): support backend settings for canvas

* feat(ui): allow devtools to be resizeable (#1605)

* fix(ui): console redux tab null check

* Api v1.15.0 (#1689)

* fix(chalice): fix create alert with MS Teams notification channel
closes openreplay/openreplay#1677

* fix(chalice): fix MS Teams notifications
* refactor(chalice): enhanced MS Teams notifications
closes openreplay/openreplay#1681

(cherry picked from commit 265897f509)

* fix(ui): filter keys conflcit with metadata, path analysis 4 col

* fix(ui): clear the filers and series on card type change

* fix(player): fix msg reader bug

* fix(DB): fix CH wrong version (#1692)

(cherry picked from commit 48dbbb55db)

* fix(ui): filter keys conflcit with metadata

* fix(tracker): unique broadcast channel name

* fix(chalice): fixed delete cards (#1697)

(cherry picked from commit 92fedd310c)

* fix(tracker): add trycatch to ignore iframe errors

* feat(backend): added ARM arch support to backend services [Dockerfile]

* feat(backend): removed userAgent from sessions and unstarted-sessions tables

* fix(DB): change path-analysis card size

---------

Co-authored-by: Delirium <nikita@openreplay.com>
Co-authored-by: Shekar Siri <sshekarsiri@gmail.com>
Co-authored-by: Alexander <zavorotynskiy@pm.me>

* refactor(chalice): cleaned code (#1699)

* feat(api): usability testing - added start_path to the resposne, remove count from the list

* feat(api): usability testing - test to have response count and live count

* feat(api): usability testing - test to have additional data

* Revert "refactor(chalice): cleaned code (#1699)" (#1702)

This reverts commit 83f2b0c12c.

* feat(api): usability testing - responses with total and other improvements

* change(api): vulnerability whitelist udpate

* feat(api): usability testing - create added missing columns, and sessions with user_id search

* feat(api): usability testing - update test with responseCount

* feat(api): usability testing - timestamps in unix

* feat(api): usability testing - request with proper case change

* feat(api): usability testing - task.description nullable

* feat(api): usability testing - check deleted status

* Api v1.16.0 (#1707)

* fix(chalice): fixed search sessions

* fix(chalice): fixed search sessions
* refactor(chalice): upgraded dependencies
* refactor(crons): upgraded dependencies
* refactor(alerts): upgraded dependencies

* Api v1.16.0 (#1712)

* feat(DB): user-testing support

* feat(chalice): user testing support

* feat(chalice): support utxVideo (#1726)

* feat(chalice): changed bucket name for ux testing webcamera videos

* refactored(chalice): refactored code (#1743)

refactored(chalice): upgraded dependencies

---------

Co-authored-by: Shekar Siri <sshekarsiri@gmail.com>
Co-authored-by: Delirium <nikita@openreplay.com>
Co-authored-by: Alexander <zavorotynskiy@pm.me>
This commit is contained in:
Kraiem Taha Yassine 2023-12-06 13:36:27 +01:00 committed by GitHub
parent 2e6dd17f0b
commit df10875a00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 58 additions and 56 deletions

View file

@ -5,18 +5,19 @@ name = "pypi"
[packages] [packages]
requests = "==2.31.0" requests = "==2.31.0"
boto3 = "==1.28.67" boto3 = "==1.33.8"
pyjwt = "==2.8.0" pyjwt = "==2.8.0"
psycopg2-binary = "==2.9.9" psycopg2-binary = "==2.9.9"
elasticsearch = "==8.10.1" elasticsearch = "==8.11.0"
jira = "==3.5.2" jira = "==3.5.2"
fastapi = "==0.104.0" fastapi = "==0.104.1"
python-decouple = "==3.8" python-decouple = "==3.8"
apscheduler = "==3.10.4" apscheduler = "==3.10.4"
redis = "==5.0.1" redis = "==5.0.1"
urllib3 = "==1.26.16" urllib3 = "==1.26.16"
uvicorn = {extras = ["standard"], version = "==0.23.2"} uvicorn = {extras = ["standard"], version = "==0.23.2"}
pydantic = {extras = ["email"], version = "==2.3.0"} pydantic = {extras = ["email"], version = "==2.3.0"}
psycopg = {extras = ["binary", "pool"], version = "==3.1.14"}
[dev-packages] [dev-packages]

View file

@ -14,8 +14,8 @@ from starlette.responses import StreamingResponse
from chalicelib.utils import helper from chalicelib.utils import helper
from chalicelib.utils import pg_client from chalicelib.utils import pg_client
from crons import core_crons, core_dynamic_crons from crons import core_crons, core_dynamic_crons
from routers import core, core_dynamic, additional_routes from routers import core, core_dynamic
from routers.subs import insights, metrics, v1_api, health from routers.subs import insights, metrics, v1_api, health, usability_tests
loglevel = config("LOGLEVEL", default=logging.WARNING) loglevel = config("LOGLEVEL", default=logging.WARNING)
print(f">Loglevel set to: {loglevel}") print(f">Loglevel set to: {loglevel}")
@ -30,7 +30,6 @@ class ORPYAsyncConnection(AsyncConnection):
super().__init__(*args, row_factory=dict_row, **kwargs) super().__init__(*args, row_factory=dict_row, **kwargs)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# Startup # Startup
@ -49,7 +48,6 @@ async def lifespan(app: FastAPI):
for job in app.schedule.get_jobs(): for job in app.schedule.get_jobs():
ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)}) ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)})
database = { database = {
"host": config("pg_host", default="localhost"), "host": config("pg_host", default="localhost"),
"dbname": config("pg_dbname", default="orpy"), "dbname": config("pg_dbname", default="orpy"),
@ -122,9 +120,9 @@ app.include_router(health.public_app)
app.include_router(health.app) app.include_router(health.app)
app.include_router(health.app_apikey) app.include_router(health.app_apikey)
app.include_router(additional_routes.public_app) app.include_router(usability_tests.public_app)
app.include_router(additional_routes.app) app.include_router(usability_tests.app)
app.include_router(additional_routes.app_apikey) app.include_router(usability_tests.app_apikey)
# @app.get('/private/shutdown', tags=["private"]) # @app.get('/private/shutdown', tags=["private"])
# async def stop_server(): # async def stop_server():

View file

@ -1,9 +1,10 @@
# Keep this version to not have conflicts between requests and boto3 # Keep this version to not have conflicts between requests and boto3
urllib3==1.26.16 urllib3==1.26.16
requests==2.31.0 requests==2.31.0
boto3==1.29.7 boto3==1.33.8
pyjwt==2.8.0 pyjwt==2.8.0
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
psycopg[pool,binary]==3.1.14
elasticsearch==8.11.0 elasticsearch==8.11.0
jira==3.5.2 jira==3.5.2

View file

@ -1,9 +1,10 @@
# Keep this version to not have conflicts between requests and boto3 # Keep this version to not have conflicts between requests and boto3
urllib3==1.26.16 urllib3==1.26.16
requests==2.31.0 requests==2.31.0
boto3==1.29.7 boto3==1.33.8
pyjwt==2.8.0 pyjwt==2.8.0
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
psycopg[pool,binary]==3.1.14
elasticsearch==8.11.0 elasticsearch==8.11.0
jira==3.5.2 jira==3.5.2
@ -16,5 +17,3 @@ pydantic[email]==2.3.0
apscheduler==3.10.4 apscheduler==3.10.4
redis==5.0.1 redis==5.0.1
psycopg[pool,binary]==3.1.12

View file

@ -1,3 +0,0 @@
from routers.base import get_routers
public_app, app, app_apikey = get_routers()

View file

@ -1,7 +1,7 @@
from typing import Union, Optional from typing import Union
from decouple import config from decouple import config
from fastapi import Depends, Body, Query from fastapi import Depends, Body
import schemas import schemas
from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assignments, projects, \ from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assignments, projects, \
@ -14,7 +14,6 @@ from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assig
from chalicelib.core.collaboration_msteams import MSTeams from chalicelib.core.collaboration_msteams import MSTeams
from chalicelib.core.collaboration_slack import Slack from chalicelib.core.collaboration_slack import Slack
from or_dependencies import OR_context, OR_role from or_dependencies import OR_context, OR_role
from chalicelib.core.usability_testing.routes import app as usability_testing_routes
from routers.base import get_routers from routers.base import get_routers
public_app, app, app_apikey = get_routers() public_app, app, app_apikey = get_routers()
@ -704,14 +703,14 @@ def get_slack_channels(context: schemas.CurrentContext = Depends(OR_context)):
return {"data": webhook.get_by_type(tenant_id=context.tenant_id, webhook_type=schemas.WebhookType.slack)} return {"data": webhook.get_by_type(tenant_id=context.tenant_id, webhook_type=schemas.WebhookType.slack)}
@app.get('/integrations/slack/{webhookId}', tags=["integrations"]) @app.get('/integrations/slack/{integrationId}', tags=["integrations"])
def get_slack_webhook(webhookId: int, context: schemas.CurrentContext = Depends(OR_context)): def get_slack_webhook(integrationId: int, context: schemas.CurrentContext = Depends(OR_context)):
return {"data": Slack.get_integration(tenant_id=context.tenant_id, integration_id=webhookId)} return {"data": Slack.get_integration(tenant_id=context.tenant_id, integration_id=integrationId)}
@app.delete('/integrations/slack/{webhookId}', tags=["integrations"]) @app.delete('/integrations/slack/{integrationId}', tags=["integrations"])
def delete_slack_integration(webhookId: int, _=Body(None), context: schemas.CurrentContext = Depends(OR_context)): def delete_slack_integration(integrationId: int, _=Body(None), context: schemas.CurrentContext = Depends(OR_context)):
return webhook.delete(tenant_id=context.tenant_id, webhook_id=webhookId) return webhook.delete(tenant_id=context.tenant_id, webhook_id=integrationId)
@app.put('/webhooks', tags=["webhooks"]) @app.put('/webhooks', tags=["webhooks"])
@ -861,6 +860,3 @@ async def check_recording_status(project_id: int):
@public_app.get('/', tags=["health"]) @public_app.get('/', tags=["health"])
def health_check(): def health_check():
return {} return {}
app.include_router(usability_testing_routes)

View file

@ -1,8 +1,7 @@
from fastapi import Body, Depends from fastapi import Body, Depends
from chalicelib.core.usability_testing.schema import UTTestCreate, UTTestRead, UTTestUpdate, UTTestDelete, SearchResult, \
UTTestSearch, UTTestSessionsSearch, UTTestResponsesSearch, StatusEnum, UTTestStatusUpdate
from chalicelib.core.usability_testing import service from chalicelib.core.usability_testing import service
from chalicelib.core.usability_testing.schema import UTTestCreate, UTTestUpdate, UTTestSearch
from or_dependencies import OR_context from or_dependencies import OR_context
from routers.base import get_routers from routers.base import get_routers
from schemas import schemas from schemas import schemas
@ -11,7 +10,7 @@ public_app, app, app_apikey = get_routers()
tags = ["usability-tests"] tags = ["usability-tests"]
@app.post("/{projectId}/usability-tests/search", tags=tags) @app.post('/{projectId}/usability-tests/search', tags=tags)
async def search_ui_tests( async def search_ui_tests(
projectId: int, projectId: int,
search: UTTestSearch = Body(..., search: UTTestSearch = Body(...,
@ -28,7 +27,7 @@ async def search_ui_tests(
return service.search_ui_tests(projectId, search) return service.search_ui_tests(projectId, search)
@app.post("/{projectId}/usability-tests", tags=tags) @app.post('/{projectId}/usability-tests', tags=tags)
async def create_ut_test(projectId: int, test_data: UTTestCreate, async def create_ut_test(projectId: int, test_data: UTTestCreate,
context: schemas.CurrentContext = Depends(OR_context)): context: schemas.CurrentContext = Depends(OR_context)):
""" """
@ -42,7 +41,7 @@ async def create_ut_test(projectId: int, test_data: UTTestCreate,
return service.create_ut_test(test_data) return service.create_ut_test(test_data)
@app.get("/{projectId}/usability-tests/{test_id}", tags=tags) @app.get('/{projectId}/usability-tests/{test_id}', tags=tags)
async def get_ut_test(projectId: int, test_id: int): async def get_ut_test(projectId: int, test_id: int):
""" """
Retrieve a specific UT test by its ID. Retrieve a specific UT test by its ID.
@ -53,7 +52,7 @@ async def get_ut_test(projectId: int, test_id: int):
return service.get_ut_test(projectId, test_id) return service.get_ut_test(projectId, test_id)
@app.delete("/{projectId}/usability-tests/{test_id}", tags=tags) @app.delete('/{projectId}/usability-tests/{test_id}', tags=tags)
async def delete_ut_test(projectId: int, test_id: int): async def delete_ut_test(projectId: int, test_id: int):
""" """
Delete a specific UT test by its ID. Delete a specific UT test by its ID.
@ -64,7 +63,7 @@ async def delete_ut_test(projectId: int, test_id: int):
return service.delete_ut_test(projectId, test_id) return service.delete_ut_test(projectId, test_id)
@app.put("/{projectId}/usability-tests/{test_id}", tags=tags) @app.put('/{projectId}/usability-tests/{test_id}', tags=tags)
async def update_ut_test(projectId: int, test_id: int, test_update: UTTestUpdate): async def update_ut_test(projectId: int, test_id: int, test_update: UTTestUpdate):
""" """
Update a specific UT test by its ID. Update a specific UT test by its ID.
@ -77,7 +76,7 @@ async def update_ut_test(projectId: int, test_id: int, test_update: UTTestUpdate
return service.update_ut_test(projectId, test_id, test_update) return service.update_ut_test(projectId, test_id, test_update)
@app.get("/{projectId}/usability-tests/{test_id}/sessions", tags=tags) @app.get('/{projectId}/usability-tests/{test_id}/sessions', tags=tags)
async def get_sessions(projectId: int, test_id: int, page: int = 1, limit: int = 10, async def get_sessions(projectId: int, test_id: int, page: int = 1, limit: int = 10,
live: bool = False, live: bool = False,
user_id: str = None): user_id: str = None):
@ -91,8 +90,8 @@ async def get_sessions(projectId: int, test_id: int, page: int = 1, limit: int =
return service.ut_tests_sessions(projectId, test_id, page, limit, user_id, live) return service.ut_tests_sessions(projectId, test_id, page, limit, user_id, live)
@app.get("/{projectId}/usability-tests/{test_id}/responses/{task_id}", tags=tags) @app.get('/{projectId}/usability-tests/{test_id}/responses/{task_id}', tags=tags)
async def get_responses(test_id: int, task_id: int, page: int = 1, limit: int = 10, query: str = None): async def get_responses(projectId: int, test_id: int, task_id: int, page: int = 1, limit: int = 10, query: str = None):
""" """
Get responses related to a specific UT test. Get responses related to a specific UT test.
@ -102,8 +101,8 @@ async def get_responses(test_id: int, task_id: int, page: int = 1, limit: int =
return service.get_responses(test_id, task_id, page, limit, query) return service.get_responses(test_id, task_id, page, limit, query)
@app.get("/{projectId}/usability-tests/{test_id}/statistics", tags=tags) @app.get('/{projectId}/usability-tests/{test_id}/statistics', tags=tags)
async def get_statistics(test_id: int): async def get_statistics(projectId: int, test_id: int):
""" """
Get statistics related to a specific UT test. Get statistics related to a specific UT test.
@ -113,8 +112,8 @@ async def get_statistics(test_id: int):
return service.get_statistics(test_id=test_id) return service.get_statistics(test_id=test_id)
@app.get("/{projectId}/usability-tests/{test_id}/task-statistics", tags=tags) @app.get('/{projectId}/usability-tests/{test_id}/task-statistics', tags=tags)
async def get_task_statistics(test_id: int): async def get_task_statistics(projectId: int, test_id: int):
""" """
Get statistics related to a specific UT test. Get statistics related to a specific UT test.

4
ee/api/.gitignore vendored
View file

@ -263,6 +263,7 @@ Pipfile.lock
/routers/core.py /routers/core.py
/routers/subs/__init__.py /routers/subs/__init__.py
/routers/subs/v1_api.py /routers/subs/v1_api.py
/routers/subs/usability_tests.py
/run-alerts-dev.sh /run-alerts-dev.sh
/run-dev.sh /run-dev.sh
/schemas/overrides.py /schemas/overrides.py
@ -270,3 +271,6 @@ Pipfile.lock
/schemas/transformers_validators.py /schemas/transformers_validators.py
/test/ /test/
/chalicelib/core/user_testing.py /chalicelib/core/user_testing.py
/orpy.py
/chalicelib/core/usability_testing/
/NOTES.md

View file

@ -6,22 +6,23 @@ name = "pypi"
[packages] [packages]
urllib3 = "==1.26.16" urllib3 = "==1.26.16"
requests = "==2.31.0" requests = "==2.31.0"
boto3 = "==1.28.67" boto3 = "==1.29.7"
pyjwt = "==2.8.0" pyjwt = "==2.8.0"
psycopg2-binary = "==2.9.9" psycopg2-binary = "==2.9.9"
elasticsearch = "==8.10.1" elasticsearch = "==8.11.0"
jira = "==3.5.2" jira = "==3.5.2"
fastapi = "==0.104.0" fastapi = "==0.104.1"
gunicorn = "==21.2.0" gunicorn = "==21.2.0"
python-decouple = "==3.8" python-decouple = "==3.8"
apscheduler = "==3.10.4" apscheduler = "==3.10.4"
python-multipart = "==0.0.6" python-multipart = "==0.0.6"
redis = "==5.0.1" redis = "==5.0.1"
python3-saml = "==1.16.0"
azure-storage-blob = "==12.19.0"
uvicorn = {extras = ["standard"], version = "==0.23.2"} uvicorn = {extras = ["standard"], version = "==0.23.2"}
pydantic = {extras = ["email"], version = "==2.3.0"} pydantic = {extras = ["email"], version = "==2.3.0"}
clickhouse-driver = {extras = ["lz4"], version = "==0.2.6"} clickhouse-driver = {extras = ["lz4"], version = "==0.2.6"}
python3-saml = "==1.16.0" psycopg = {extras = ["binary", "pool"], version = "==3.1.12"}
azure-storage-blob = "==12.18.3"
[dev-packages] [dev-packages]

View file

@ -23,8 +23,8 @@ from routers import ee
if config("ENABLE_SSO", cast=bool, default=True): if config("ENABLE_SSO", cast=bool, default=True):
from routers import saml from routers import saml
from crons import core_crons, ee_crons, core_dynamic_crons from crons import core_crons, ee_crons, core_dynamic_crons
from routers.subs import insights, metrics, v1_api_ee from routers.subs import insights, metrics, v1_api, health, usability_tests
from routers.subs import v1_api, health from routers.subs import v1_api_ee
loglevel = config("LOGLEVEL", default=logging.WARNING) loglevel = config("LOGLEVEL", default=logging.WARNING)
print(f">Loglevel set to: {loglevel}") print(f">Loglevel set to: {loglevel}")
@ -39,7 +39,6 @@ class ORPYAsyncConnection(AsyncConnection):
super().__init__(*args, row_factory=dict_row, **kwargs) super().__init__(*args, row_factory=dict_row, **kwargs)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# Startup # Startup
@ -60,7 +59,6 @@ async def lifespan(app: FastAPI):
for job in app.schedule.get_jobs(): for job in app.schedule.get_jobs():
ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)}) ap_logger.info({"Name": str(job.id), "Run Frequency": str(job.trigger), "Next Run": str(job.next_run_time)})
database = { database = {
"host": config("pg_host", default="localhost"), "host": config("pg_host", default="localhost"),
"dbname": config("pg_dbname", default="orpy"), "dbname": config("pg_dbname", default="orpy"),
@ -143,6 +141,10 @@ app.include_router(health.public_app)
app.include_router(health.app) app.include_router(health.app)
app.include_router(health.app_apikey) app.include_router(health.app_apikey)
app.include_router(usability_tests.public_app)
app.include_router(usability_tests.app)
app.include_router(usability_tests.app_apikey)
if config("ENABLE_SSO", cast=bool, default=True): if config("ENABLE_SSO", cast=bool, default=True):
app.include_router(saml.public_app) app.include_router(saml.public_app)
app.include_router(saml.app) app.include_router(saml.app)

View file

@ -86,8 +86,11 @@ rm -rf ./routers/base.py
rm -rf ./routers/core.py rm -rf ./routers/core.py
rm -rf ./routers/subs/__init__.py rm -rf ./routers/subs/__init__.py
rm -rf ./routers/subs/v1_api.py rm -rf ./routers/subs/v1_api.py
rm -rf ./routers/subs/usability_tests.py
rm -rf ./run-alerts-dev.sh rm -rf ./run-alerts-dev.sh
rm -rf ./run-dev.sh rm -rf ./run-dev.sh
rm -rf ./schemas/overrides.py rm -rf ./schemas/overrides.py
rm -rf ./schemas/schemas.py rm -rf ./schemas/schemas.py
rm -rf ./schemas/transformers_validators.py rm -rf ./schemas/transformers_validators.py
rm -rf ./orpy.py
rm -rf ./chalicelib/core/usability_testing/

View file

@ -4,6 +4,7 @@ requests==2.31.0
boto3==1.29.7 boto3==1.29.7
pyjwt==2.8.0 pyjwt==2.8.0
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
psycopg[pool,binary]==3.1.14
elasticsearch==8.11.0 elasticsearch==8.11.0
jira==3.5.2 jira==3.5.2

View file

@ -4,6 +4,7 @@ requests==2.31.0
boto3==1.29.7 boto3==1.29.7
pyjwt==2.8.0 pyjwt==2.8.0
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
psycopg[pool,binary]==3.1.14
elasticsearch==8.11.0 elasticsearch==8.11.0
jira==3.5.2 jira==3.5.2

View file

@ -4,6 +4,7 @@ requests==2.31.0
boto3==1.29.7 boto3==1.29.7
pyjwt==2.8.0 pyjwt==2.8.0
psycopg2-binary==2.9.9 psycopg2-binary==2.9.9
psycopg[pool,binary]==3.1.14
elasticsearch==8.11.0 elasticsearch==8.11.0
jira==3.5.2 jira==3.5.2
@ -19,12 +20,10 @@ apscheduler==3.10.4
clickhouse-driver[lz4]==0.2.6 clickhouse-driver[lz4]==0.2.6
# TODO: enable after xmlsec fix https://github.com/xmlsec/python-xmlsec/issues/252 # TODO: enable after xmlsec fix https://github.com/xmlsec/python-xmlsec/issues/252
#--no-binary is used to avoid libxml2 library version incompatibilities between xmlsec and lxml #--no-binary is used to avoid libxml2 library version incompatibilities between xmlsec and lxml
#python3-saml==1.15.0 --no-binary=lxml #python3-saml==1.16.0 --no-binary=lxml
python3-saml==1.16.0 python3-saml==1.16.0
python-multipart==0.0.6 python-multipart==0.0.6
redis==5.0.1 redis==5.0.1
#confluent-kafka==2.1.0 #confluent-kafka==2.1.0
azure-storage-blob==12.19.0 azure-storage-blob==12.19.0
psycopg[pool,binary]==3.1.12