From 271cef65db9c0084ed809ce8ecbcbc2739b34cb7 Mon Sep 17 00:00:00 2001 From: Farah <91642487+FarahAbdo@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:37:05 +0300 Subject: [PATCH 1/9] Add materials for "Coordinating Teams of AI Agents with CrewAI in Python" (#775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add materials for CrewAI tutorial * Apply Black formatting * Apply Ruff formatting * Update coordinating-teams-of-ai-agents-with-crewai-in-python/README.md Co-authored-by: Martin Breuss * Update coordinating-teams-of-ai-agents-with-crewai-in-python/requirements.txt Co-authored-by: Martin Breuss * Update README.md * Rename the folder to reflect the keyphrase * Pin requirements for a reproducible environment * Bump the model to avoid hallucinations and sort import statements --------- Co-authored-by: Martin Breuss Co-authored-by: brendaweles <160772586+brendaweles@users.noreply.github.com> Co-authored-by: Bartosz Zaczyński --- crewai-python/01_single_agent.py | 25 ++++ crewai-python/02_research_and_writer_crew.py | 53 +++++++ crewai-python/03_explicit_context.py | 54 +++++++ crewai-python/04_agent_with_tools.py | 58 ++++++++ crewai-python/README.md | 58 ++++++++ crewai-python/requirements.txt | 149 +++++++++++++++++++ 6 files changed, 397 insertions(+) create mode 100644 crewai-python/01_single_agent.py create mode 100644 crewai-python/02_research_and_writer_crew.py create mode 100644 crewai-python/03_explicit_context.py create mode 100644 crewai-python/04_agent_with_tools.py create mode 100644 crewai-python/README.md create mode 100644 crewai-python/requirements.txt diff --git a/crewai-python/01_single_agent.py b/crewai-python/01_single_agent.py new file mode 100644 index 0000000000..0f747522c7 --- /dev/null +++ b/crewai-python/01_single_agent.py @@ -0,0 +1,25 @@ +from crewai import LLM, Agent, Crew, Task + +llm = LLM(model="gemini/gemini-2.5-flash") + +travel_agent = Agent( + role="Travel Advisor", + goal="Provide helpful travel recommendations", + backstory=( + "You're an experienced travel advisor who has visited" + " over fifty countries and specializes in budget-friendly" + " adventure travel." + ), + llm=llm, + verbose=True, +) + +task = Task( + description="Suggest three budget-friendly destinations in Southeast Asia", + expected_output="A short list of three destinations with brief descriptions", + agent=travel_agent, +) + +crew = Crew(agents=[travel_agent], tasks=[task]) +result = crew.kickoff() +print(result.raw) diff --git a/crewai-python/02_research_and_writer_crew.py b/crewai-python/02_research_and_writer_crew.py new file mode 100644 index 0000000000..7c677f61a2 --- /dev/null +++ b/crewai-python/02_research_and_writer_crew.py @@ -0,0 +1,53 @@ +from crewai import LLM, Agent, Crew, Process, Task + +llm = LLM(model="gemini/gemini-2.5-flash") + +researcher = Agent( + role="Senior Research Analyst", + goal="Find comprehensive information about a given topic", + backstory=( + "You're a seasoned research analyst with a knack for" + " uncovering the most relevant and accurate information." + " You're known for your thorough and well-organized research." + ), + llm=llm, +) + +writer = Agent( + role="Content Writer", + goal="Write clear, engaging content based on research findings", + backstory=( + "You're an experienced writer who excels at transforming" + " complex research into accessible, well-structured articles" + " that readers enjoy." + ), + llm=llm, +) + +research_task = Task( + description="Research the latest trends in renewable energy technology", + expected_output=( + "A detailed list of the top five renewable energy trends" + " with explanations" + ), + agent=researcher, +) + +writing_task = Task( + description="Write a short article based on the research findings", + expected_output=( + "A well-structured article of about three hundred words" + " on renewable energy trends" + ), + agent=writer, +) + +crew = Crew( + agents=[researcher, writer], + tasks=[research_task, writing_task], + process=Process.sequential, + verbose=True, +) + +result = crew.kickoff() +print(result.raw) diff --git a/crewai-python/03_explicit_context.py b/crewai-python/03_explicit_context.py new file mode 100644 index 0000000000..98a5ac024e --- /dev/null +++ b/crewai-python/03_explicit_context.py @@ -0,0 +1,54 @@ +from crewai import LLM, Agent, Crew, Task + +llm = LLM(model="gemini/gemini-2.5-flash") + +researcher = Agent( + role="Senior Research Analyst", + goal="Find comprehensive information about a given topic", + backstory=( + "You're a seasoned research analyst with a knack for" + " uncovering the most relevant and accurate information." + " You're known for your thorough and well-organized research." + ), + llm=llm, +) + +writer = Agent( + role="Content Writer", + goal="Write clear, engaging content based on research findings", + backstory=( + "You're an experienced writer who excels at transforming" + " complex research into accessible, well-structured articles" + " that readers enjoy." + ), + llm=llm, +) + +research_task = Task( + description=( + "Research the current state of electric vehicle adoption worldwide" + ), + expected_output=( + "A bullet-point list of key statistics and trends in EV adoption" + ), + agent=researcher, +) + +writing_task = Task( + description=( + "Using the provided research, write a concise summary article" + " about electric vehicle adoption" + ), + expected_output="A two hundred word article summarizing EV adoption trends", + agent=writer, + context=[research_task], +) + +crew = Crew( + agents=[researcher, writer], + tasks=[research_task, writing_task], + verbose=True, +) + +result = crew.kickoff() +print(result.raw) diff --git a/crewai-python/04_agent_with_tools.py b/crewai-python/04_agent_with_tools.py new file mode 100644 index 0000000000..94a5721e28 --- /dev/null +++ b/crewai-python/04_agent_with_tools.py @@ -0,0 +1,58 @@ +from crewai import LLM, Agent, Crew, Task +from crewai_tools import ScrapeWebsiteTool + +llm = LLM(model="gemini/gemini-2.5-pro") + +scrape_tool = ScrapeWebsiteTool() + +researcher = Agent( + role="Python Release Analyst", + goal="Find the latest Python release information", + backstory=( + "You're a developer advocate who tracks Python releases" + " and summarizes what's new for the community." + ), + tools=[scrape_tool], + llm=llm, +) + +writer = Agent( + role="Tech Blogger", + goal="Write concise release summaries for a developer audience", + backstory=( + "You write clear, engaging blog posts that help developers" + " stay up to date with the Python ecosystem." + ), + llm=llm, +) + +research_task = Task( + description=( + "Scrape https://www.python.org/downloads/ and report" + " the latest stable Python version number and its release date" + ), + expected_output="The latest Python version number and release date", + agent=researcher, +) + +writing_task = Task( + description=( + "Write a one-paragraph announcement based on the Python" + " release information provided by the research task" + ), + expected_output=( + "A concise one hundred word announcement covering the" + " latest Python version number and release date" + ), + agent=writer, + context=[research_task], +) + +crew = Crew( + agents=[researcher, writer], + tasks=[research_task, writing_task], + verbose=True, +) + +result = crew.kickoff() +print(result.raw) diff --git a/crewai-python/README.md b/crewai-python/README.md new file mode 100644 index 0000000000..442f102fb9 --- /dev/null +++ b/crewai-python/README.md @@ -0,0 +1,58 @@ +# Coordinating Teams of AI Agents with CrewAI in Python + +Sample code for the Real Python tutorial +[CrewAI in Python: Coordinating Teams of AI Agents](https://realpython.com/crewai-python/). + +## Requirements + +- Python 3.10 to 3.13 (CrewAI does not support 3.14+) +- A free [Google AI Studio](https://aistudio.google.com/) API key for Gemini + +## Setup + +Create and activate a virtual environment, then install the dependencies: + +```console +$ python -m venv venv +$ source venv/bin/activate # On Windows: .\venv\Scripts\activate +$ python -m pip install -r requirements.txt +``` + +Set your Gemini API key as an environment variable: + +```console +$ export GEMINI_API_KEY="your-gemini-api-key-here" +``` + +On Windows PowerShell: + +```pscon +PS> $ENV:GEMINI_API_KEY = "your-gemini-api-key-here" +``` + +## Running the Examples + +Each script corresponds to one section of the tutorial: + +| File | Tutorial Section | +|------|------------------| +| `01_single_agent.py` | Get Started With CrewAI in Python | +| `02_research_and_writer_crew.py` | Build Your First Multi-Agent Team | +| `03_explicit_context.py` | Control Task Dependencies Explicitly | +| `04_agent_with_tools.py` | Expand Agent Capabilities With Tools | + +Run any script with: + +```console +$ python 01_single_agent.py +``` + +## Notes + +- The `verbose=True` flag prints detailed agent reasoning logs — useful for + learning, but noisy for production. +- If CrewAI complains about a missing `OPENAI_API_KEY` (e.g., when memory + or certain tools are enabled), set it to any non-empty string to suppress + the error. +- On first run, CrewAI may show a "Would you like to view your execution + traces?" prompt that auto-dismisses after twenty seconds. diff --git a/crewai-python/requirements.txt b/crewai-python/requirements.txt new file mode 100644 index 0000000000..821a46020b --- /dev/null +++ b/crewai-python/requirements.txt @@ -0,0 +1,149 @@ +aiofiles==24.1.0 +aiohappyeyeballs==2.7.1 +aiohttp==3.14.1 +aiosignal==1.4.0 +aiosqlite==0.21.0 +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.14.1 +appdirs==1.4.4 +attrs==26.1.0 +backoff==2.2.1 +bcrypt==5.0.0 +beautifulsoup4==4.13.5 +build==1.5.0 +cel-python==0.5.0 +certifi==2026.6.17 +cffi==2.0.0 +charset-normalizer==3.4.7 +chromadb==1.1.1 +click==8.4.2 +crewai==1.15.1 +crewai-cli==1.15.1 +crewai-core==1.15.1 +crewai-tools==1.15.1 +cryptography==49.0.0 +defusedxml==0.7.1 +deprecation==2.1.0 +distro==1.9.0 +docstring-parser==0.18.0 +durationpy==0.10 +et-xmlfile==2.0.0 +filelock==3.29.4 +flatbuffers==25.12.19 +frozenlist==1.8.0 +fsspec==2026.6.0 +google-auth==2.55.1 +google-genai==1.65.0 +google-re2==1.1.20251105 +googleapis-common-protos==1.75.0 +grpcio==1.81.1 +h11==0.16.0 +hf-xet==1.5.1 +httpcore==1.0.9 +httptools==0.8.0 +httpx==0.28.1 +httpx-sse==0.4.3 +huggingface-hub==1.21.0 +idna==3.18 +importlib-resources==7.1.0 +instructor==1.15.4 +jinja2==3.1.6 +jiter==0.14.0 +jmespath==1.1.0 +json-repair==0.25.3 +json5==0.10.0 +jsonref==1.1.0 +jsonschema==4.26.0 +jsonschema-specifications==2025.9.1 +kubernetes==36.0.2 +lance-namespace==0.9.0 +lance-namespace-urllib3-client==0.9.0 +lancedb==0.30.0 +lark==1.3.1 +linkify-it-py==2.1.0 +lxml==6.1.1 +markdown-it-py==4.2.0 +markupsafe==3.0.3 +mcp==1.26.0 +mdit-py-plugins==0.6.1 +mdurl==0.1.2 +mmh3==5.2.1 +multidict==6.7.1 +numpy==2.5.0 +oauthlib==3.3.1 +onnxruntime==1.27.0 +openai==2.44.0 +openpyxl==3.1.5 +opentelemetry-api==1.42.1 +opentelemetry-exporter-otlp-proto-common==1.42.1 +opentelemetry-exporter-otlp-proto-grpc==1.42.1 +opentelemetry-exporter-otlp-proto-http==1.42.1 +opentelemetry-proto==1.42.1 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 +orjson==3.11.9 +overrides==7.7.0 +packaging==26.2 +pdfminer-six==20260107 +pdfplumber==0.11.10 +pendulum==3.2.0 +pillow==12.3.0 +platformdirs==4.10.0 +portalocker==2.7.0 +posthog==5.4.0 +propcache==0.5.2 +protobuf==6.33.6 +pyarrow==24.0.0 +pyasn1==0.6.3 +pyasn1-modules==0.4.2 +pybase64==1.4.3 +pycparser==3.0 +pydantic==2.12.5 +pydantic-core==2.41.5 +pydantic-settings==2.10.1 +pygments==2.20.0 +pyjwt==2.13.0 +pymupdf==1.26.7 +pypdfium2==5.11.0 +pypika==0.51.1 +pyproject-hooks==1.2.0 +python-dateutil==2.9.0.post0 +python-docx==1.2.0 +python-dotenv==1.2.2 +python-multipart==0.0.32 +pytube==15.0.0 +pyyaml==6.0.3 +referencing==0.37.0 +regex==2026.1.15 +requests==2.34.2 +requests-oauthlib==2.0.0 +rich==14.3.4 +rpds-py==2026.6.3 +shellingham==1.5.4 +six==1.17.0 +sniffio==1.3.1 +soupsieve==2.8.4 +sse-starlette==3.4.5 +starlette==1.3.1 +tenacity==9.1.4 +textual==8.2.8 +tiktoken==0.12.0 +tokenizers==0.23.1 +tomli==2.0.2 +tomli-w==1.1.0 +tqdm==4.68.3 +typer==0.25.1 +typing-extensions==4.16.0 +typing-inspection==0.4.2 +tzdata==2026.2 +uc-micro-py==2.0.0 +urllib3==2.7.0 +uv==0.11.26 +uvicorn==0.49.0 +uvloop==0.22.1 +watchfiles==1.2.0 +websocket-client==1.9.0 +websockets==16.0 +yarl==1.24.2 +youtube-transcript-api==1.2.4 From d2ee4bd827fc115ed5e87fa77c3e69f7409b77bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Sun, 5 Jul 2026 15:16:31 +0200 Subject: [PATCH 2/9] Materials for Antigravity CLI (#786) * Materials for Antigravity CLI * Add a test fixture to reproduce the zerodivision error --- antigravity-cli/README.md | 3 + antigravity-cli/expense-report/.gitignore | 283 ++++++++++++++++++ antigravity-cli/expense-report/AGENTS.md | 14 + antigravity-cli/expense-report/README.md | 7 + antigravity-cli/expense-report/cli.py | 14 + .../expense-report/transactions.csv | 11 + .../expense-report/transactions.py | 34 +++ .../expense-report/transactions_zero.csv | 3 + 8 files changed, 369 insertions(+) create mode 100644 antigravity-cli/README.md create mode 100644 antigravity-cli/expense-report/.gitignore create mode 100644 antigravity-cli/expense-report/AGENTS.md create mode 100644 antigravity-cli/expense-report/README.md create mode 100644 antigravity-cli/expense-report/cli.py create mode 100644 antigravity-cli/expense-report/transactions.csv create mode 100644 antigravity-cli/expense-report/transactions.py create mode 100644 antigravity-cli/expense-report/transactions_zero.csv diff --git a/antigravity-cli/README.md b/antigravity-cli/README.md new file mode 100644 index 0000000000..6f97b0873b --- /dev/null +++ b/antigravity-cli/README.md @@ -0,0 +1,3 @@ +# How to Use Google's Antigravity CLI for AI Code Assistance + +This folder contains code associated with the Real Python tutorial [How to Use Google's Antigravity CLI for AI Code Assistance](https://realpython.com/antigravity-cli/). diff --git a/antigravity-cli/expense-report/.gitignore b/antigravity-cli/expense-report/.gitignore new file mode 100644 index 0000000000..b4197023c8 --- /dev/null +++ b/antigravity-cli/expense-report/.gitignore @@ -0,0 +1,283 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,visualstudiocode + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Python ### +# 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 + +# 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/#use-with-ide +.pdm.toml + +# 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/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all,visualstudiocode diff --git a/antigravity-cli/expense-report/AGENTS.md b/antigravity-cli/expense-report/AGENTS.md new file mode 100644 index 0000000000..1e7a6f8461 --- /dev/null +++ b/antigravity-cli/expense-report/AGENTS.md @@ -0,0 +1,14 @@ +# Project conventions + +This is a small command-line tool that reads a CSV of personal transactions and +prints a per-category expense summary. + +## Money + +- Represent every monetary amount with `decimal.Decimal`, never `float`. +- Quantize money to exactly two decimal places before displaying it. + +## Style + +- Keep functions small and single-purpose. +- Standard library only. No third-party dependencies. diff --git a/antigravity-cli/expense-report/README.md b/antigravity-cli/expense-report/README.md new file mode 100644 index 0000000000..e74674e621 --- /dev/null +++ b/antigravity-cli/expense-report/README.md @@ -0,0 +1,7 @@ +# expense-report + +A tiny command-line tool that summarizes personal transactions from a CSV file. + +```console +$ python cli.py transactions.csv +``` diff --git a/antigravity-cli/expense-report/cli.py b/antigravity-cli/expense-report/cli.py new file mode 100644 index 0000000000..25a190f585 --- /dev/null +++ b/antigravity-cli/expense-report/cli.py @@ -0,0 +1,14 @@ +"""Command-line entry point for the expense reporter.""" + +import sys + +from transactions import report + + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else "transactions.csv" + print(report(path)) + + +if __name__ == "__main__": + main() diff --git a/antigravity-cli/expense-report/transactions.csv b/antigravity-cli/expense-report/transactions.csv new file mode 100644 index 0000000000..b1fbcde68f --- /dev/null +++ b/antigravity-cli/expense-report/transactions.csv @@ -0,0 +1,11 @@ +date,category,description,amount +2026-01-03,Groceries,Corner store,12.10 +2026-01-05,Transport,Bus pass,40.00 +2026-01-09,Groceries,Farmers market,8.20 +2026-01-12,Dining,Coffee,4.10 +2026-01-15,Dining,Lunch,11.20 +2026-01-18,Groceries,Supermarket,53.40 +2026-01-21,Transport,Taxi,18.75 +2026-01-24,Dining,Dinner,29.30 +2026-01-27,Utilities,Electricity,64.30 +2026-01-29,Groceries,Bakery,3.10 diff --git a/antigravity-cli/expense-report/transactions.py b/antigravity-cli/expense-report/transactions.py new file mode 100644 index 0000000000..9691a45c5f --- /dev/null +++ b/antigravity-cli/expense-report/transactions.py @@ -0,0 +1,34 @@ +"""Summarize a CSV of personal transactions by category.""" + +import csv + + +def report(path): + rows = [] + with open(path, newline="") as f: + reader = csv.DictReader(f) + for row in reader: + rows.append(row) + + totals = {} + count = 0 + grand_total = 0.0 + for row in rows: + category = row["category"] + amount = float(row["amount"]) + if category not in totals: + totals[category] = 0.0 + totals[category] += amount + grand_total += amount + count += 1 + + lines = [] + lines.append("Expense report") + lines.append("=" * 32) + for category in sorted(totals, key=lambda c: totals[c], reverse=True): + share = totals[category] / grand_total * 100 + lines.append(f"{category:<14}{totals[category]:>10} ({share:.1f}%)") + lines.append("-" * 32) + lines.append(f"{'TOTAL':<14}{grand_total:>10}") + lines.append(f"{count} transactions") + return "\n".join(lines) diff --git a/antigravity-cli/expense-report/transactions_zero.csv b/antigravity-cli/expense-report/transactions_zero.csv new file mode 100644 index 0000000000..125ab4e460 --- /dev/null +++ b/antigravity-cli/expense-report/transactions_zero.csv @@ -0,0 +1,3 @@ +date,category,amount +2026-01-05,Refunds,-50.00 +2026-01-06,Purchases,50.00 From cbf5c359d77e12394811ae11e2bd7190cf7fc812 Mon Sep 17 00:00:00 2001 From: Leodanis Pozo Ramos Date: Sun, 5 Jul 2026 15:42:04 +0200 Subject: [PATCH 3/9] Sample code for: Qt Designer and Python: Build Your GUI Applications Faster (#790) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update the sample text editor from PyQt5 to PyQt6 (app.py and the pyuic6-generated main_window_ui.py), add a standard top-level README plus a detailed app README in sample_editor/, and commit the generated module in ruff-formatted form so CI passes without linter-config changes. Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Bartosz Zaczyński --- qt-designer-python/README.md | 31 +------ qt-designer-python/sample_editor/README.md | 29 +++++++ qt-designer-python/sample_editor/app.py | 4 +- .../sample_editor/main_window_ui.py | 82 +++++++++---------- 4 files changed, 74 insertions(+), 72 deletions(-) create mode 100644 qt-designer-python/sample_editor/README.md diff --git a/qt-designer-python/README.md b/qt-designer-python/README.md index 3aa3bdbae4..6827db3913 100644 --- a/qt-designer-python/README.md +++ b/qt-designer-python/README.md @@ -1,30 +1,3 @@ -# Sample Text Editor +# Qt Designer and Python: Build Your GUI Applications Faster -A **sample text editor** that shows how to use Qt Designer to create GUI applications. - -## How to Run this Application - -To run this application, you need to [install `PyQt5`](https://realpython.com/python-pyqt-gui-calculator/#installing-pyqt) on your Python environment. To do that, you can run the following commands in a terminal or command prompt: - -```sh -$ python3 -m venv ./venv -$ source venv/bin/activate -(venv) $ pip install PyQt5 -``` - -Once you have [PyQt](https://www.riverbankcomputing.com/static/Docs/PyQt5/) installed, you can run the application by executing the following command: - -```sh -(venv) $ cd sample_editor/ -(venv) $ python app.py -``` - -This command will launch the application, so you'll be able to experiment with it. - -## About the Author - -Leodanis Pozo Ramos – [@lpozo78](https://twitter.com/lpozo78) – lpozor78@gmail.com - -## License - -The set of icons used in this application are part of the [TurkinOS](https://github.com/llamaret/turkinos-icon) icon theme, which is distributed under the [GPL v3.0 license](https://github.com/llamaret/turkinos-icon/blob/master/LICENSE). See `ui/resources/LICENSE` for details. \ No newline at end of file +This folder provides the code examples for the Real Python tutorial [Qt Designer and Python: Build Your GUI Applications Faster](https://realpython.com/qt-designer-python/) diff --git a/qt-designer-python/sample_editor/README.md b/qt-designer-python/sample_editor/README.md new file mode 100644 index 0000000000..854db7d952 --- /dev/null +++ b/qt-designer-python/sample_editor/README.md @@ -0,0 +1,29 @@ +# Sample Text Editor + +A **sample text editor** that shows how to use Qt Designer to create GUI applications. + +## How to Run this Application + +To run this application, you need to [install `PyQt6`](https://realpython.com/python-pyqt-gui-calculator/#installing-pyqt) on your Python environment. To do that, you can run the following commands in a terminal or command prompt: + +```sh +$ python3 -m venv ./venv +$ source venv/bin/activate +(venv) $ pip install PyQt6 +``` + +Once you have [PyQt](https://www.riverbankcomputing.com/static/Docs/PyQt6/) installed, you can run the application by executing the following command: + +```sh +(venv) $ python app.py +``` + +This command will launch the application, so you'll be able to experiment with it. + +## About the Author + +Leodanis Pozo Ramos – [@lpozo78](https://twitter.com/lpozo78) – lpozor78@gmail.com + +## License + +The set of icons used in this application are part of the [TurkinOS](https://github.com/llamaret/turkinos-icon) icon theme, which is distributed under the [GPL v3.0 license](https://github.com/llamaret/turkinos-icon/blob/master/LICENSE). See `ui/resources/LICENSE.txt` for details. diff --git a/qt-designer-python/sample_editor/app.py b/qt-designer-python/sample_editor/app.py index f37347e8e5..01441197c6 100644 --- a/qt-designer-python/sample_editor/app.py +++ b/qt-designer-python/sample_editor/app.py @@ -1,8 +1,8 @@ import sys from main_window_ui import Ui_MainWindow -from PyQt5.QtWidgets import QApplication, QDialog, QMainWindow, QMessageBox -from PyQt5.uic import loadUi +from PyQt6.QtWidgets import QApplication, QDialog, QMainWindow, QMessageBox +from PyQt6.uic import loadUi class Window(QMainWindow, Ui_MainWindow): diff --git a/qt-designer-python/sample_editor/main_window_ui.py b/qt-designer-python/sample_editor/main_window_ui.py index d62d0457cd..e83ac486f3 100644 --- a/qt-designer-python/sample_editor/main_window_ui.py +++ b/qt-designer-python/sample_editor/main_window_ui.py @@ -1,122 +1,122 @@ -# -*- coding: utf-8 -*- - # Form implementation generated from reading ui file 'ui/main_window.ui' # -# Created by: PyQt5 UI code generator 5.15.1 +# Created by: PyQt6 UI code generator 6.11.0 # -# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt6 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(413, 299) - self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget = QtWidgets.QWidget(parent=MainWindow) self.centralwidget.setObjectName("centralwidget") self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) self.verticalLayout.setContentsMargins(1, 1, 1, 1) self.verticalLayout.setObjectName("verticalLayout") - self.textEdit = QtWidgets.QTextEdit(self.centralwidget) + self.textEdit = QtWidgets.QTextEdit(parent=self.centralwidget) self.textEdit.setObjectName("textEdit") self.verticalLayout.addWidget(self.textEdit) MainWindow.setCentralWidget(self.centralwidget) - self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar = QtWidgets.QMenuBar(parent=MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 413, 20)) self.menubar.setObjectName("menubar") - self.menu_File = QtWidgets.QMenu(self.menubar) + self.menu_File = QtWidgets.QMenu(parent=self.menubar) self.menu_File.setObjectName("menu_File") - self.menuOpen_Recent = QtWidgets.QMenu(self.menu_File) + self.menuOpen_Recent = QtWidgets.QMenu(parent=self.menu_File) self.menuOpen_Recent.setObjectName("menuOpen_Recent") - self.menu_Edit = QtWidgets.QMenu(self.menubar) + self.menu_Edit = QtWidgets.QMenu(parent=self.menubar) self.menu_Edit.setObjectName("menu_Edit") - self.menu_Help = QtWidgets.QMenu(self.menubar) + self.menu_Help = QtWidgets.QMenu(parent=self.menubar) self.menu_Help.setObjectName("menu_Help") MainWindow.setMenuBar(self.menubar) - self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar = QtWidgets.QStatusBar(parent=MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) - self.toolBar = QtWidgets.QToolBar(MainWindow) + self.toolBar = QtWidgets.QToolBar(parent=MainWindow) self.toolBar.setObjectName("toolBar") - MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar) - self.action_New = QtWidgets.QAction(MainWindow) + MainWindow.addToolBar( + QtCore.Qt.ToolBarArea.TopToolBarArea, self.toolBar + ) + self.action_New = QtGui.QAction(parent=MainWindow) icon = QtGui.QIcon() icon.addPixmap( QtGui.QPixmap("ui/resources/file-new.png"), - QtGui.QIcon.Normal, - QtGui.QIcon.Off, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, ) self.action_New.setIcon(icon) self.action_New.setObjectName("action_New") - self.action_Open = QtWidgets.QAction(MainWindow) + self.action_Open = QtGui.QAction(parent=MainWindow) icon1 = QtGui.QIcon() icon1.addPixmap( QtGui.QPixmap("ui/resources/file-open.png"), - QtGui.QIcon.Normal, - QtGui.QIcon.Off, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, ) self.action_Open.setIcon(icon1) self.action_Open.setObjectName("action_Open") - self.action_Save = QtWidgets.QAction(MainWindow) + self.action_Save = QtGui.QAction(parent=MainWindow) icon2 = QtGui.QIcon() icon2.addPixmap( QtGui.QPixmap("ui/resources/file-save.png"), - QtGui.QIcon.Normal, - QtGui.QIcon.Off, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, ) self.action_Save.setIcon(icon2) self.action_Save.setObjectName("action_Save") - self.action_Exit = QtWidgets.QAction(MainWindow) + self.action_Exit = QtGui.QAction(parent=MainWindow) icon3 = QtGui.QIcon() icon3.addPixmap( QtGui.QPixmap("ui/resources/file-exit.png"), - QtGui.QIcon.Normal, - QtGui.QIcon.Off, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, ) self.action_Exit.setIcon(icon3) self.action_Exit.setObjectName("action_Exit") - self.action_Copy = QtWidgets.QAction(MainWindow) + self.action_Copy = QtGui.QAction(parent=MainWindow) icon4 = QtGui.QIcon() icon4.addPixmap( QtGui.QPixmap("ui/resources/edit-copy.png"), - QtGui.QIcon.Normal, - QtGui.QIcon.Off, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, ) self.action_Copy.setIcon(icon4) self.action_Copy.setObjectName("action_Copy") - self.action_Paste = QtWidgets.QAction(MainWindow) + self.action_Paste = QtGui.QAction(parent=MainWindow) icon5 = QtGui.QIcon() icon5.addPixmap( QtGui.QPixmap("ui/resources/edit-paste.png"), - QtGui.QIcon.Normal, - QtGui.QIcon.Off, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, ) self.action_Paste.setIcon(icon5) self.action_Paste.setObjectName("action_Paste") - self.action_Cut = QtWidgets.QAction(MainWindow) + self.action_Cut = QtGui.QAction(parent=MainWindow) icon6 = QtGui.QIcon() icon6.addPixmap( QtGui.QPixmap("ui/resources/edit-cut.png"), - QtGui.QIcon.Normal, - QtGui.QIcon.Off, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, ) self.action_Cut.setIcon(icon6) self.action_Cut.setObjectName("action_Cut") - self.actionOpen_All = QtWidgets.QAction(MainWindow) + self.actionOpen_All = QtGui.QAction(parent=MainWindow) self.actionOpen_All.setObjectName("actionOpen_All") - self.action_About = QtWidgets.QAction(MainWindow) + self.action_About = QtGui.QAction(parent=MainWindow) icon7 = QtGui.QIcon() icon7.addPixmap( QtGui.QPixmap("ui/resources/help-content.png"), - QtGui.QIcon.Normal, - QtGui.QIcon.Off, + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, ) self.action_About.setIcon(icon7) self.action_About.setObjectName("action_About") - self.action_Find_Replace = QtWidgets.QAction(MainWindow) + self.action_Find_Replace = QtGui.QAction(parent=MainWindow) self.action_Find_Replace.setObjectName("action_Find_Replace") self.menuOpen_Recent.addAction(self.actionOpen_All) self.menu_File.addAction(self.action_New) From 3ec52ca83c2232c49184317b0077ebb6339b758d Mon Sep 17 00:00:00 2001 From: Rohit Goswami Date: Mon, 13 Jul 2026 12:19:55 +0200 Subject: [PATCH 4/9] feat: add pointblank tutorial materials (#764) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add pointblank tutorial materials * docs: refine pointblank materials * fix: sync pointblank materials with tutorial scripts Align the quickstart and atom scripts and the atom YAML with the published tutorial examples, and refresh the generated HTML report. * Update README --------- Co-authored-by: Bartosz Zaczyński --- python-pointblank/README.md | 21 + python-pointblank/pointblank_atoms.csv | 14 + python-pointblank/pointblank_atoms.py | 41 ++ python-pointblank/pointblank_atoms.yaml | 22 + python-pointblank/pointblank_quickstart.py | 38 ++ python-pointblank/pointblank_report.html | 526 ++++++++++++++++++ python-pointblank/pointblank_report.png | Bin 0 -> 72739 bytes .../pointblank_starter_validation.yaml | 24 + python-pointblank/pointblank_thresholds.py | 48 ++ 9 files changed, 734 insertions(+) create mode 100644 python-pointblank/README.md create mode 100644 python-pointblank/pointblank_atoms.csv create mode 100644 python-pointblank/pointblank_atoms.py create mode 100644 python-pointblank/pointblank_atoms.yaml create mode 100644 python-pointblank/pointblank_quickstart.py create mode 100644 python-pointblank/pointblank_report.html create mode 100644 python-pointblank/pointblank_report.png create mode 100644 python-pointblank/pointblank_starter_validation.yaml create mode 100644 python-pointblank/pointblank_thresholds.py diff --git a/python-pointblank/README.md b/python-pointblank/README.md new file mode 100644 index 0000000000..94f935daff --- /dev/null +++ b/python-pointblank/README.md @@ -0,0 +1,21 @@ +# Validating Data With Pointblank in Python + +Supporting code and sample data for the Real Python tutorial [Validating Data With Pointblank in Python](https://realpython.com/python-pointblank/). + +## Requirements + +The Python scripts use PEP 723 dependency metadata and run with [uv](https://docs.astral.sh/uv/): + +```console +$ uv run pointblank_quickstart.py +$ uv run pointblank_thresholds.py +$ uv run pointblank_atoms.py +``` + +The command-line examples can run without a project environment: + +```console +$ uv run --no-project --with 'pointblank[pl]' -- pb scan pointblank_atoms.csv +$ uv run --no-project --with 'pointblank[pl]' -- pb missing pointblank_atoms.csv +$ uvx --from 'pointblank[pl]' pb run pointblank_atoms.yaml --output-html pointblank_report.html +``` diff --git a/python-pointblank/pointblank_atoms.csv b/python-pointblank/pointblank_atoms.csv new file mode 100644 index 0000000000..f558fa9e69 --- /dev/null +++ b/python-pointblank/pointblank_atoms.csv @@ -0,0 +1,14 @@ +atom_id,symbol,x,y,z,fx,fy,fz +0,Cu,1.0,0.5,0.1,0.1,0.0,0.0 +1,Pt,2.1,1.5,0.2,-0.2,0.1,-0.1 +2,Cu,3.2,2.5,0.3,0.3,-0.1,0.1 +3,Pt,4.3,3.5,0.4,-0.1,0.0,0.0 +4,Cu,5.4,4.5,0.5,0.2,0.1,-0.1 +5,Pt,6.5,5.5,0.6,-0.3,-0.1,0.1 +6,Cu,7.6,6.5,0.7,0.1,0.0,0.0 +7,Pt,8.7,7.5,0.8,-0.2,0.1,-0.1 +8,Cu,9.8,8.5,0.9,0.3,-0.1,0.1 +9,Pt,10.9,9.5,1.0,-0.1,0.0,0.0 +10,Zz,0.5,0.5,0.1,0.0,0.0,0.0 +11,Cu,,1.5,0.2,0.0,0.0,0.0 +12,Pt,12.1,2.5,0.3,1500.0,0.0,0.0 diff --git a/python-pointblank/pointblank_atoms.py b/python-pointblank/pointblank_atoms.py new file mode 100644 index 0000000000..499b4b0355 --- /dev/null +++ b/python-pointblank/pointblank_atoms.py @@ -0,0 +1,41 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "pointblank[pl]", +# ] +# /// + +import polars as pl +import pointblank as pb + +VALID_ELEMENTS = ["Cu", "Pt"] + + +def main() -> None: + atoms = pl.read_csv("pointblank_atoms.csv") + + validation = ( + pb.Validate( + data=atoms, + tbl_name="atoms", + label="Atom data validation", + thresholds=pb.Thresholds(warning=0.02, error=0.05, critical=0.07), + ) + .col_vals_in_set(columns="symbol", set=VALID_ELEMENTS) + .col_vals_not_null(columns=["x", "y", "z"]) + .col_vals_between(columns=["x", "y", "z"], left=0, right=20) + .col_vals_between(columns=["fx", "fy", "fz"], left=-1000, right=1000) + .interrogate() + ) + + clean = validation.get_sundered_data(type="pass") + dirty = validation.get_sundered_data(type="fail") + + print(f"Clean rows: {len(clean)}") + print(clean.select(["atom_id", "symbol", "x", "fx"])) + print(f"\nDirty rows: {len(dirty)}") + print(dirty.select(["atom_id", "symbol", "x", "fx"])) + + +if __name__ == "__main__": + main() diff --git a/python-pointblank/pointblank_atoms.yaml b/python-pointblank/pointblank_atoms.yaml new file mode 100644 index 0000000000..f36e5d6786 --- /dev/null +++ b/python-pointblank/pointblank_atoms.yaml @@ -0,0 +1,22 @@ +tbl: pointblank_atoms.csv +df_library: polars +tbl_name: "atoms" +label: "Atom data validation" +thresholds: + warning: 0.02 + error: 0.05 + critical: 0.07 +steps: + - col_vals_in_set: + columns: symbol + set: [Cu, Pt] + - col_vals_not_null: + columns: [x, y, z] + - col_vals_between: + columns: [x, y, z] + left: 0 + right: 20 + - col_vals_between: + columns: [fx, fy, fz] + left: -1000 + right: 1000 diff --git a/python-pointblank/pointblank_quickstart.py b/python-pointblank/pointblank_quickstart.py new file mode 100644 index 0000000000..8026d1e73f --- /dev/null +++ b/python-pointblank/pointblank_quickstart.py @@ -0,0 +1,38 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "pointblank[pl]", +# ] +# /// + +import json + +import pointblank as pb + + +def main() -> None: + validation = ( + pb.Validate( + data=pb.load_dataset("small_table", tbl_type="polars"), + tbl_name="small_table", + label="Quickstart validation", + ) + .col_vals_between(columns="d", left=0, right=5000) + .col_vals_in_set(columns="f", set=["low", "mid", "high"]) + .col_vals_not_null(columns="c") + .interrogate() + ) + + report = json.loads(validation.get_json_report()) + + print("Validation summary:\n") + for step in report: + print( + f"{step['assertion_type']:20}" + f"passed={step['n_passed']:<4}" + f"failed={step['n_failed']}" + ) + + +if __name__ == "__main__": + main() diff --git a/python-pointblank/pointblank_report.html b/python-pointblank/pointblank_report.html new file mode 100644 index 0000000000..b056d61aa6 --- /dev/null +++ b/python-pointblank/pointblank_report.html @@ -0,0 +1,526 @@ +
+ + ++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Pointblank Validation
Atom data validation
PolarsatomsWARNING0.02ERROR0.05CRITICAL0.07
STEPCOLUMNSVALUESTBLEVALUNITSPASSFAILWECEXT
#FF33001 +
+ + + col_vals_in_set + + + + + + + +
+
+
col_vals_in_set()
+
+ +
symbolCu, Pt + + + + + + + +1312
0.92
1
0.08
#FF33002 +
+ + + col_vals_not_null + + + + + + + + + +
+
+
col_vals_not_null()
+
+ +
x + + + + + + + +1312
0.92
1
0.08
#4CA64C3 +
+ + + col_vals_not_null + + + + + + + + + +
+
+
col_vals_not_null()
+
+ +
y + + + + + + + +1313
1.00
0
0.00
#4CA64C4 +
+ + + col_vals_not_null + + + + + + + + + +
+
+
col_vals_not_null()
+
+ +
z + + + + + + + +1313
1.00
0
0.00
#FF33005 +
+ + + col_vals_between + + + + + + + +
+
+
col_vals_between()
+
+ +
x[0, 20] + + + + + + + +1312
0.92
1
0.08
#4CA64C6 +
+ + + col_vals_between + + + + + + + +
+
+
col_vals_between()
+
+ +
y[0, 20] + + + + + + + +1313
1.00
0
0.00
#4CA64C7 +
+ + + col_vals_between + + + + + + + +
+
+
col_vals_between()
+
+ +
z[0, 20] + + + + + + + +1313
1.00
0
0.00
#FF33008 +
+ + + col_vals_between + + + + + + + +
+
+
col_vals_between()
+
+ +
fx[-1000, 1000] + + + + + + + +1312
0.92
1
0.08
#4CA64C9 +
+ + + col_vals_between + + + + + + + +
+
+
col_vals_between()
+
+ +
fy[-1000, 1000] + + + + + + + +1313
1.00
0
0.00
#4CA64C10 +
+ + + col_vals_between + + + + + + + +
+
+
col_vals_between()
+
+ +
fz[-1000, 1000] + + + + + + + +1313
1.00
0
0.00
2026-06-09 14:03:27 UTC< 1 s2026-06-09 14:03:27 UTC
+ +
+ \ No newline at end of file diff --git a/python-pointblank/pointblank_report.png b/python-pointblank/pointblank_report.png new file mode 100644 index 0000000000000000000000000000000000000000..d3d2d268240e1ce5252a74e1201a0e89006edc86 GIT binary patch literal 72739 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

Ov!><$gu$-<(|JMQXu%1sFs@p9dBk)V>7i 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 literal 0 HcmV?d00001 diff --git a/python-pointblank/pointblank_starter_validation.yaml b/python-pointblank/pointblank_starter_validation.yaml new file mode 100644 index 0000000000..4a5f405b2c --- /dev/null +++ b/python-pointblank/pointblank_starter_validation.yaml @@ -0,0 +1,24 @@ +# Starter Pointblank template for adapting to your own pipeline. +# +# You can run this template against a real file with: +# uv run --no-project --with 'pointblank[pl]' pb run pointblank_starter_validation.yaml --data your_data.csv --fail-on critical + +tbl: small_table +df_library: polars +tbl_name: "Starter Validation" +label: "Adapt this template to your data" +thresholds: + warning: 0.02 + error: 0.05 + critical: 0.10 +steps: + - col_exists: + columns: [record_id, status, amount] + - col_vals_not_null: + columns: record_id + - col_vals_in_set: + columns: status + set: [pending, shipped, delivered] + - col_vals_gt: + columns: amount + value: 0 diff --git a/python-pointblank/pointblank_thresholds.py b/python-pointblank/pointblank_thresholds.py new file mode 100644 index 0000000000..7e9b8d99c0 --- /dev/null +++ b/python-pointblank/pointblank_thresholds.py @@ -0,0 +1,48 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = [ +# "pointblank[pl]", +# ] +# /// + +import pointblank as pb + + +def main() -> None: + validation = ( + pb.Validate( + data=pb.load_dataset("small_table", tbl_type="polars"), + tbl_name="small_table", + label="Threshold-driven validation", + thresholds=pb.Thresholds(warning=0.05, error=0.10, critical=0.15), + actions=pb.Actions( + warning=( + "Warning: step {step} reached {level} severity during " + "{type}." + ), + critical=( + "Critical: step {step} reached {level} severity during " + "{type}." + ), + ), + ) + .col_vals_between(columns="d", left=0, right=5000) + .col_vals_not_null(columns="c") + .rows_distinct() + .interrogate() + ) + + print("All checks passed perfectly:", validation.all_passed()) + print( + "Anything above the error threshold:", + validation.above_threshold(level="error"), + ) + + try: + validation.assert_below_threshold(level="critical") + except AssertionError as exc: + print("CI gate tripped:", exc) + + +if __name__ == "__main__": + main() From f9966389305defab4ff194a2e2b42d62e967eb90 Mon Sep 17 00:00:00 2001 From: Philipp Acsany Date: Wed, 15 Jul 2026 15:28:31 +0000 Subject: [PATCH 5/9] Add python-mixins-course sample code Publishes the code from the 'Understanding Mixin Classes in Python' video course so it can be offered as a free email opt-in download to the YouTube/free audience (course zip is otherwise members-only). --- python-mixins-course/README.md | 3 ++ python-mixins-course/as_dict_mixin.py | 22 ++++++++++++ python-mixins-course/as_dict_mixin_final.py | 23 ++++++++++++ python-mixins-course/sequence.py | 11 ++++++ python-mixins-course/sequence_abc.py | 35 ++++++++++++++++++ python-mixins-course/sequence_final.py | 35 ++++++++++++++++++ python-mixins-course/sequence_mixin.py | 40 +++++++++++++++++++++ 7 files changed, 169 insertions(+) create mode 100644 python-mixins-course/README.md create mode 100644 python-mixins-course/as_dict_mixin.py create mode 100644 python-mixins-course/as_dict_mixin_final.py create mode 100644 python-mixins-course/sequence.py create mode 100644 python-mixins-course/sequence_abc.py create mode 100644 python-mixins-course/sequence_final.py create mode 100644 python-mixins-course/sequence_mixin.py diff --git a/python-mixins-course/README.md b/python-mixins-course/README.md new file mode 100644 index 0000000000..b3fdbf067b --- /dev/null +++ b/python-mixins-course/README.md @@ -0,0 +1,3 @@ +# Understanding Mixin Classes in Python + +This folder contains sample code for the Real Python video course [Understanding Mixin Classes in Python](https://realpython.com/courses/understanding-mixin-classes-in-python/). diff --git a/python-mixins-course/as_dict_mixin.py b/python-mixins-course/as_dict_mixin.py new file mode 100644 index 0000000000..fc33f22e4a --- /dev/null +++ b/python-mixins-course/as_dict_mixin.py @@ -0,0 +1,22 @@ +class Person: + def __init__(self, name, age): + self.name = name + self.age = age + + def as_dict(self): + return { + "name": self.name, + "age": self.age, + } + + +class Car: + def __init__(self, make, model): + self.make = make + self.model = model + + def as_dict(self): + return { + "make": self.make, + "model": self.model, + } \ No newline at end of file diff --git a/python-mixins-course/as_dict_mixin_final.py b/python-mixins-course/as_dict_mixin_final.py new file mode 100644 index 0000000000..ed16c46049 --- /dev/null +++ b/python-mixins-course/as_dict_mixin_final.py @@ -0,0 +1,23 @@ +class AsDictMixin: + def as_dict(self): + return vars(self) + + +class Person(AsDictMixin): + def __init__(self, name, age): + self.name = name + self.age = age + + +john = Person("John", 42) +print(john.as_dict()) + + +class Car(AsDictMixin): + def __init__(self, make, model): + self.make = make + self.model = model + + +toyota = Car("Toyota", "Prius") +print(toyota.as_dict()) \ No newline at end of file diff --git a/python-mixins-course/sequence.py b/python-mixins-course/sequence.py new file mode 100644 index 0000000000..9452c935d7 --- /dev/null +++ b/python-mixins-course/sequence.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class Sequence(ABC): + @abstractmethod + def __getitem__(self, index): + pass + + @abstractmethod + def __len__(self): + pass \ No newline at end of file diff --git a/python-mixins-course/sequence_abc.py b/python-mixins-course/sequence_abc.py new file mode 100644 index 0000000000..f98582bb56 --- /dev/null +++ b/python-mixins-course/sequence_abc.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod + + +class Sequence(ABC): + @abstractmethod + def __getitem__(self, index): + pass + + @abstractmethod + def __len__(self): + pass + + def __iter__(self): + index = 0 + while index < len(self): + yield self[index] + index += 1 + + +class MyRange(Sequence): + def __init__(self, stop): + self.stop = stop # Assume that `stop` is >= 0. + + def __getitem__(self, index): + if 0 <= index < self.stop: + return index + raise IndexError + + def __len__(self): + return self.stop + + +r = MyRange(10) +for number in r: + print(number) \ No newline at end of file diff --git a/python-mixins-course/sequence_final.py b/python-mixins-course/sequence_final.py new file mode 100644 index 0000000000..f98582bb56 --- /dev/null +++ b/python-mixins-course/sequence_final.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod + + +class Sequence(ABC): + @abstractmethod + def __getitem__(self, index): + pass + + @abstractmethod + def __len__(self): + pass + + def __iter__(self): + index = 0 + while index < len(self): + yield self[index] + index += 1 + + +class MyRange(Sequence): + def __init__(self, stop): + self.stop = stop # Assume that `stop` is >= 0. + + def __getitem__(self, index): + if 0 <= index < self.stop: + return index + raise IndexError + + def __len__(self): + return self.stop + + +r = MyRange(10) +for number in r: + print(number) \ No newline at end of file diff --git a/python-mixins-course/sequence_mixin.py b/python-mixins-course/sequence_mixin.py new file mode 100644 index 0000000000..a015d2df42 --- /dev/null +++ b/python-mixins-course/sequence_mixin.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod + + +class IterMixin: + # __iter__ works IF the class inheriting the mixin + # already provides __getitem__ and __len__. + def __iter__(self): + print("Hey from inside IterMixin.__iter__.") + index = 0 + while index < len(self): + yield self[index] + index += 1 + + +class Sequence(IterMixin, ABC): + @abstractmethod + def __getitem__(self, index): + pass + + @abstractmethod + def __len__(self): + pass + + +class MyRange(Sequence): + def __init__(self, stop): + self.stop = stop # Assume that `stop` is >= 0. + + def __getitem__(self, index): + if 0 <= index < self.stop: + return index + raise IndexError + + def __len__(self): + return self.stop + + +r = MyRange(10) +for number in r: + print(number) \ No newline at end of file From ff0b631328e0755e38785bb5ef9dd50b33020a13 Mon Sep 17 00:00:00 2001 From: Philipp Acsany Date: Wed, 15 Jul 2026 15:35:48 +0000 Subject: [PATCH 6/9] Revert "Add python-mixins-course sample code" This reverts commit f9966389305defab4ff194a2e2b42d62e967eb90. --- python-mixins-course/README.md | 3 -- python-mixins-course/as_dict_mixin.py | 22 ------------ python-mixins-course/as_dict_mixin_final.py | 23 ------------ python-mixins-course/sequence.py | 11 ------ python-mixins-course/sequence_abc.py | 35 ------------------ python-mixins-course/sequence_final.py | 35 ------------------ python-mixins-course/sequence_mixin.py | 40 --------------------- 7 files changed, 169 deletions(-) delete mode 100644 python-mixins-course/README.md delete mode 100644 python-mixins-course/as_dict_mixin.py delete mode 100644 python-mixins-course/as_dict_mixin_final.py delete mode 100644 python-mixins-course/sequence.py delete mode 100644 python-mixins-course/sequence_abc.py delete mode 100644 python-mixins-course/sequence_final.py delete mode 100644 python-mixins-course/sequence_mixin.py diff --git a/python-mixins-course/README.md b/python-mixins-course/README.md deleted file mode 100644 index b3fdbf067b..0000000000 --- a/python-mixins-course/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Understanding Mixin Classes in Python - -This folder contains sample code for the Real Python video course [Understanding Mixin Classes in Python](https://realpython.com/courses/understanding-mixin-classes-in-python/). diff --git a/python-mixins-course/as_dict_mixin.py b/python-mixins-course/as_dict_mixin.py deleted file mode 100644 index fc33f22e4a..0000000000 --- a/python-mixins-course/as_dict_mixin.py +++ /dev/null @@ -1,22 +0,0 @@ -class Person: - def __init__(self, name, age): - self.name = name - self.age = age - - def as_dict(self): - return { - "name": self.name, - "age": self.age, - } - - -class Car: - def __init__(self, make, model): - self.make = make - self.model = model - - def as_dict(self): - return { - "make": self.make, - "model": self.model, - } \ No newline at end of file diff --git a/python-mixins-course/as_dict_mixin_final.py b/python-mixins-course/as_dict_mixin_final.py deleted file mode 100644 index ed16c46049..0000000000 --- a/python-mixins-course/as_dict_mixin_final.py +++ /dev/null @@ -1,23 +0,0 @@ -class AsDictMixin: - def as_dict(self): - return vars(self) - - -class Person(AsDictMixin): - def __init__(self, name, age): - self.name = name - self.age = age - - -john = Person("John", 42) -print(john.as_dict()) - - -class Car(AsDictMixin): - def __init__(self, make, model): - self.make = make - self.model = model - - -toyota = Car("Toyota", "Prius") -print(toyota.as_dict()) \ No newline at end of file diff --git a/python-mixins-course/sequence.py b/python-mixins-course/sequence.py deleted file mode 100644 index 9452c935d7..0000000000 --- a/python-mixins-course/sequence.py +++ /dev/null @@ -1,11 +0,0 @@ -from abc import ABC, abstractmethod - - -class Sequence(ABC): - @abstractmethod - def __getitem__(self, index): - pass - - @abstractmethod - def __len__(self): - pass \ No newline at end of file diff --git a/python-mixins-course/sequence_abc.py b/python-mixins-course/sequence_abc.py deleted file mode 100644 index f98582bb56..0000000000 --- a/python-mixins-course/sequence_abc.py +++ /dev/null @@ -1,35 +0,0 @@ -from abc import ABC, abstractmethod - - -class Sequence(ABC): - @abstractmethod - def __getitem__(self, index): - pass - - @abstractmethod - def __len__(self): - pass - - def __iter__(self): - index = 0 - while index < len(self): - yield self[index] - index += 1 - - -class MyRange(Sequence): - def __init__(self, stop): - self.stop = stop # Assume that `stop` is >= 0. - - def __getitem__(self, index): - if 0 <= index < self.stop: - return index - raise IndexError - - def __len__(self): - return self.stop - - -r = MyRange(10) -for number in r: - print(number) \ No newline at end of file diff --git a/python-mixins-course/sequence_final.py b/python-mixins-course/sequence_final.py deleted file mode 100644 index f98582bb56..0000000000 --- a/python-mixins-course/sequence_final.py +++ /dev/null @@ -1,35 +0,0 @@ -from abc import ABC, abstractmethod - - -class Sequence(ABC): - @abstractmethod - def __getitem__(self, index): - pass - - @abstractmethod - def __len__(self): - pass - - def __iter__(self): - index = 0 - while index < len(self): - yield self[index] - index += 1 - - -class MyRange(Sequence): - def __init__(self, stop): - self.stop = stop # Assume that `stop` is >= 0. - - def __getitem__(self, index): - if 0 <= index < self.stop: - return index - raise IndexError - - def __len__(self): - return self.stop - - -r = MyRange(10) -for number in r: - print(number) \ No newline at end of file diff --git a/python-mixins-course/sequence_mixin.py b/python-mixins-course/sequence_mixin.py deleted file mode 100644 index a015d2df42..0000000000 --- a/python-mixins-course/sequence_mixin.py +++ /dev/null @@ -1,40 +0,0 @@ -from abc import ABC, abstractmethod - - -class IterMixin: - # __iter__ works IF the class inheriting the mixin - # already provides __getitem__ and __len__. - def __iter__(self): - print("Hey from inside IterMixin.__iter__.") - index = 0 - while index < len(self): - yield self[index] - index += 1 - - -class Sequence(IterMixin, ABC): - @abstractmethod - def __getitem__(self, index): - pass - - @abstractmethod - def __len__(self): - pass - - -class MyRange(Sequence): - def __init__(self, stop): - self.stop = stop # Assume that `stop` is >= 0. - - def __getitem__(self, index): - if 0 <= index < self.stop: - return index - raise IndexError - - def __len__(self): - return self.stop - - -r = MyRange(10) -for number in r: - print(number) \ No newline at end of file From 4b3a0ccf4824b3dd94ae71bcf7d1327383f03716 Mon Sep 17 00:00:00 2001 From: Philipp Acsany Date: Wed, 15 Jul 2026 15:36:19 +0000 Subject: [PATCH 7/9] Add python-mixins-course sample code Publishes the code from the 'Understanding Mixin Classes in Python' video course (https://realpython.com/courses/understanding-mixin-classes-in-python/) so it can be offered as a free email opt-in download to the YouTube/free audience. The members-only course zip is otherwise paywalled. Files are ruff-formatted (line-length 79, trailing newlines) to pass the linters check. --- python-mixins-course/README.md | 3 ++ python-mixins-course/as_dict_mixin.py | 22 ++++++++++++ python-mixins-course/as_dict_mixin_final.py | 23 ++++++++++++ python-mixins-course/sequence.py | 11 ++++++ python-mixins-course/sequence_abc.py | 35 ++++++++++++++++++ python-mixins-course/sequence_final.py | 35 ++++++++++++++++++ python-mixins-course/sequence_mixin.py | 40 +++++++++++++++++++++ 7 files changed, 169 insertions(+) create mode 100644 python-mixins-course/README.md create mode 100644 python-mixins-course/as_dict_mixin.py create mode 100644 python-mixins-course/as_dict_mixin_final.py create mode 100644 python-mixins-course/sequence.py create mode 100644 python-mixins-course/sequence_abc.py create mode 100644 python-mixins-course/sequence_final.py create mode 100644 python-mixins-course/sequence_mixin.py diff --git a/python-mixins-course/README.md b/python-mixins-course/README.md new file mode 100644 index 0000000000..b3fdbf067b --- /dev/null +++ b/python-mixins-course/README.md @@ -0,0 +1,3 @@ +# Understanding Mixin Classes in Python + +This folder contains sample code for the Real Python video course [Understanding Mixin Classes in Python](https://realpython.com/courses/understanding-mixin-classes-in-python/). diff --git a/python-mixins-course/as_dict_mixin.py b/python-mixins-course/as_dict_mixin.py new file mode 100644 index 0000000000..6114eda1aa --- /dev/null +++ b/python-mixins-course/as_dict_mixin.py @@ -0,0 +1,22 @@ +class Person: + def __init__(self, name, age): + self.name = name + self.age = age + + def as_dict(self): + return { + "name": self.name, + "age": self.age, + } + + +class Car: + def __init__(self, make, model): + self.make = make + self.model = model + + def as_dict(self): + return { + "make": self.make, + "model": self.model, + } diff --git a/python-mixins-course/as_dict_mixin_final.py b/python-mixins-course/as_dict_mixin_final.py new file mode 100644 index 0000000000..a914420b00 --- /dev/null +++ b/python-mixins-course/as_dict_mixin_final.py @@ -0,0 +1,23 @@ +class AsDictMixin: + def as_dict(self): + return vars(self) + + +class Person(AsDictMixin): + def __init__(self, name, age): + self.name = name + self.age = age + + +john = Person("John", 42) +print(john.as_dict()) + + +class Car(AsDictMixin): + def __init__(self, make, model): + self.make = make + self.model = model + + +toyota = Car("Toyota", "Prius") +print(toyota.as_dict()) diff --git a/python-mixins-course/sequence.py b/python-mixins-course/sequence.py new file mode 100644 index 0000000000..44afe2e8e3 --- /dev/null +++ b/python-mixins-course/sequence.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class Sequence(ABC): + @abstractmethod + def __getitem__(self, index): + pass + + @abstractmethod + def __len__(self): + pass diff --git a/python-mixins-course/sequence_abc.py b/python-mixins-course/sequence_abc.py new file mode 100644 index 0000000000..c9a523367a --- /dev/null +++ b/python-mixins-course/sequence_abc.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod + + +class Sequence(ABC): + @abstractmethod + def __getitem__(self, index): + pass + + @abstractmethod + def __len__(self): + pass + + def __iter__(self): + index = 0 + while index < len(self): + yield self[index] + index += 1 + + +class MyRange(Sequence): + def __init__(self, stop): + self.stop = stop # Assume that `stop` is >= 0. + + def __getitem__(self, index): + if 0 <= index < self.stop: + return index + raise IndexError + + def __len__(self): + return self.stop + + +r = MyRange(10) +for number in r: + print(number) diff --git a/python-mixins-course/sequence_final.py b/python-mixins-course/sequence_final.py new file mode 100644 index 0000000000..c9a523367a --- /dev/null +++ b/python-mixins-course/sequence_final.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod + + +class Sequence(ABC): + @abstractmethod + def __getitem__(self, index): + pass + + @abstractmethod + def __len__(self): + pass + + def __iter__(self): + index = 0 + while index < len(self): + yield self[index] + index += 1 + + +class MyRange(Sequence): + def __init__(self, stop): + self.stop = stop # Assume that `stop` is >= 0. + + def __getitem__(self, index): + if 0 <= index < self.stop: + return index + raise IndexError + + def __len__(self): + return self.stop + + +r = MyRange(10) +for number in r: + print(number) diff --git a/python-mixins-course/sequence_mixin.py b/python-mixins-course/sequence_mixin.py new file mode 100644 index 0000000000..0d30391029 --- /dev/null +++ b/python-mixins-course/sequence_mixin.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod + + +class IterMixin: + # __iter__ works IF the class inheriting the mixin + # already provides __getitem__ and __len__. + def __iter__(self): + print("Hey from inside IterMixin.__iter__.") + index = 0 + while index < len(self): + yield self[index] + index += 1 + + +class Sequence(IterMixin, ABC): + @abstractmethod + def __getitem__(self, index): + pass + + @abstractmethod + def __len__(self): + pass + + +class MyRange(Sequence): + def __init__(self, stop): + self.stop = stop # Assume that `stop` is >= 0. + + def __getitem__(self, index): + if 0 <= index < self.stop: + return index + raise IndexError + + def __len__(self): + return self.stop + + +r = MyRange(10) +for number in r: + print(number) From fdf482495cbdf5e51ed9af5cbe387e3635424231 Mon Sep 17 00:00:00 2001 From: Yuka <254470+yukaty@users.noreply.github.com> Date: Sun, 19 Jul 2026 09:20:23 -0600 Subject: [PATCH 8/9] Add materials for FastAPI OpenTelemetry tutorial (#791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add materials for FastAPI OpenTelemetry tutorial * Sync with tutorial after TR * Fix README formatting and update tutorial link * Organize imports --------- Co-authored-by: Bartosz Zaczyński --- fastapi-opentelemetry/README.md | 3 ++ fastapi-opentelemetry/main.py | 51 ++++++++++++++++++++++++++ fastapi-opentelemetry/requirements.txt | 36 ++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 fastapi-opentelemetry/README.md create mode 100644 fastapi-opentelemetry/main.py create mode 100644 fastapi-opentelemetry/requirements.txt diff --git a/fastapi-opentelemetry/README.md b/fastapi-opentelemetry/README.md new file mode 100644 index 0000000000..d66e216620 --- /dev/null +++ b/fastapi-opentelemetry/README.md @@ -0,0 +1,3 @@ +# How to Integrate OpenTelemetry With a FastAPI App + +This folder contains sample code for the Real Python tutorial [How to Integrate OpenTelemetry With a FastAPI App]([https://realpython.com/](https://realpython.com/fastapi-opentelemetry/)). diff --git a/fastapi-opentelemetry/main.py b/fastapi-opentelemetry/main.py new file mode 100644 index 0000000000..47823dbdb4 --- /dev/null +++ b/fastapi-opentelemetry/main.py @@ -0,0 +1,51 @@ +import logging + +from fastapi import FastAPI +from opentelemetry import trace + +app = FastAPI() +tracer = trace.get_tracer(__name__) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def simulate_db_query(user_id: int): + with tracer.start_as_current_span("db_query") as span: + span.set_attribute("db.system", "postgresql") + span.set_attribute("user.id", user_id) + + trace_id = format(span.get_span_context().trace_id, "032x") + span_id = format(span.get_span_context().span_id, "016x") + + if user_id == 999: + logger.error( + "DB query failed for user %s. trace_id=%s, span_id=%s", + user_id, + trace_id, + span_id, + ) + raise ValueError("Database connection lost!") + + logger.info( + "Executing DB query for user %s. trace_id=%s, span_id=%s", + user_id, + trace_id, + span_id, + ) + return {"name": "Demo User", "role": "admin"} + + +@app.get("/users/{user_id}") +def get_user(user_id: int): + logger.info("Received request for user %s", user_id) + try: + data = simulate_db_query(user_id) + return {"status": "success", "data": data} + except ValueError: + return {"status": "error", "message": "Failed to fetch user"} + + +@app.get("/") +def read_root(): + return {"message": "Hello, OpenTelemetry!"} diff --git a/fastapi-opentelemetry/requirements.txt b/fastapi-opentelemetry/requirements.txt new file mode 100644 index 0000000000..0a9c0b1045 --- /dev/null +++ b/fastapi-opentelemetry/requirements.txt @@ -0,0 +1,36 @@ +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.14.0 +asgiref==3.11.1 +certifi==2026.6.17 +charset-normalizer==3.4.7 +click==8.4.1 +fastapi==0.138.0 +googleapis-common-protos==1.75.0 +grpcio==1.81.1 +h11==0.16.0 +idna==3.18 +opentelemetry-api==1.42.1 +opentelemetry-distro==0.63b1 +opentelemetry-exporter-otlp==1.42.1 +opentelemetry-exporter-otlp-proto-common==1.42.1 +opentelemetry-exporter-otlp-proto-grpc==1.42.1 +opentelemetry-exporter-otlp-proto-http==1.42.1 +opentelemetry-instrumentation==0.63b1 +opentelemetry-instrumentation-asgi==0.63b1 +opentelemetry-instrumentation-fastapi==0.63b1 +opentelemetry-proto==1.42.1 +opentelemetry-sdk==1.42.1 +opentelemetry-semantic-conventions==0.63b1 +opentelemetry-util-http==0.63b1 +packaging==26.2 +protobuf==6.33.6 +pydantic==2.13.4 +pydantic_core==2.46.4 +requests==2.34.2 +starlette==1.3.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.7.0 +uvicorn==0.49.0 +wrapt==2.2.2 From d6a9817361139b2797167d4ee9a43812fe236ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20Zaczy=C5=84ski?= Date: Sun, 19 Jul 2026 17:48:51 +0200 Subject: [PATCH 9/9] Fix link formatting in README.md --- fastapi-opentelemetry/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi-opentelemetry/README.md b/fastapi-opentelemetry/README.md index d66e216620..791a59e5c1 100644 --- a/fastapi-opentelemetry/README.md +++ b/fastapi-opentelemetry/README.md @@ -1,3 +1,3 @@ # How to Integrate OpenTelemetry With a FastAPI App -This folder contains sample code for the Real Python tutorial [How to Integrate OpenTelemetry With a FastAPI App]([https://realpython.com/](https://realpython.com/fastapi-opentelemetry/)). +This folder contains sample code for the Real Python tutorial [How to Integrate OpenTelemetry With a FastAPI App](https://realpython.com/fastapi-opentelemetry/).