GuíasGuides · Nivel 2Level 2

Python para IT: entorno virtual, un CSV, una API con token, un informe programado y el script que falla en el servidorPython for IT: virtual environment, a CSV, an API with a token, a scheduled report and the script that fails on the server

Un entorno virtual por script, leer un CSV con el módulo csv, llamar a una API REST con el token en una variable de entorno, escribir el informe en CSV y HTML, logging, programarlo con el Programador de tareas o cron, y el script que funciona a mano pero falla por codificación o por el proxy.One virtual environment per script, reading a CSV with the csv module, calling a REST API with the token in an environment variable, writing the report as CSV and HTML, logging, scheduling it with Task Scheduler or cron, and the script that works by hand but fails on encoding or the proxy.

Qué vas a conseguir

Un script de Python que lee la lista de equipos de un CSV, pregunta a una API REST el estado de cada uno, escribe un informe en CSV y en HTML, deja un log de lo que ha hecho y se ejecuta solo cada mañana. Es el patrón de casi cualquier automatización de operaciones: entrada, consulta, informe, programación. Y lo que hacer cuando funciona en tu equipo y falla en el servidor.

Antes de empezar

  • Python 3 de python.org (en Windows, con el lanzador py) o el del sistema en Linux.
  • Una carpeta para el script, por ejemplo C:\scripts\informe-equipos o /opt/informe-equipos.
  • La URL de la API y un token con permisos de solo lectura, pedido a quien la administra.
  • Si la empresa usa proxy con inspección TLS, el certificado de la CA corporativa en formato PEM (te lo da el equipo de red o seguridad).

1. Un entorno virtual por script

Cada script con sus propias librerías, sin tocar el Python del sistema:

PowerShell · crear el entorno e instalar requests
cd C:\scripts\informe-equipospy -m venv .venv.\.venv\Scripts\Activate.ps1pip install requestspip freeze > requirements.txt

En Linux, python3 -m venv .venv y source .venv/bin/activate. Con requirements.txt el entorno se rehace en otro servidor (pip install -r requirements.txt); .venv no se copia ni va a Git.

2. El script

informe.py
import csvimport htmlimport loggingimport osfrom datetime import date import requests API = "https://api.contoso.local/v1/devices"TOKEN = os.environ["INVENTARIO_TOKEN"]  # nunca escrito en el códigologging.basicConfig(filename="informe.log", level=logging.INFO, encoding="utf-8",                    format="%(asctime)s %(levelname)s %(message)s") def estado(equipo):    r = requests.get(f"{API}/{equipo}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=15)    r.raise_for_status()    return r.json() filas = []with open("equipos.csv", newline="", encoding="utf-8-sig") as f:    for linea in csv.DictReader(f):        nombre = linea["Equipo"]        try:            d = estado(nombre)            filas.append({"Equipo": nombre, "SO": d.get("os"), "Ultimo contacto": d.get("lastSeen")})        except requests.RequestException as e:            logging.warning("%s: %s", nombre, e)            filas.append({"Equipo": nombre, "SO": "", "Ultimo contacto": "ERROR"})logging.info("%d equipos procesados", len(filas)) salida = f"informe-{date.today()}.csv"with open(salida, "w", newline="", encoding="utf-8-sig") as f:    w = csv.DictWriter(f, fieldnames=["Equipo", "SO", "Ultimo contacto"])    w.writeheader()    w.writerows(filas) celdas = "".join(f"<tr><td>{html.escape(str(x['Equipo']))}</td><td>{html.escape(str(x['SO']))}</td>"                 f"<td>{html.escape(str(x['Ultimo contacto']))}</td></tr>" for x in filas)with open(salida.replace(".csv", ".html"), "w", encoding="utf-8") as f:    f.write(f"<meta charset='utf-8'><table><tr><th>Equipo</th><th>SO</th><th>Último contacto</th></tr>{celdas}</table>")

Lo que importa de cada parte:

  • encoding="utf-8-sig" al leer: Excel guarda los CSV en UTF-8 con BOM, y sin -sig la primera columna se llama \ufeffEquipo y el KeyError desconcierta. Al escribir, el BOM hace que Excel abra bien las tildes.
  • timeout=15: sin él, una API que no responde deja el script colgado para siempre, y la tarea programada nunca termina.
  • raise_for_status() convierte un 401 o un 500 en una excepción que el try recoge: un equipo que falla no tumba el informe entero, y queda en el log.

3. El token, en una variable de entorno

El token es una contraseña. No va en el script, ni en el CSV, ni en Git. Se pone en una variable de entorno del usuario que ejecuta el script:

PowerShell · variable de entorno para el usuario actual
[Environment]::SetEnvironmentVariable("INVENTARIO_TOKEN", "pega-aqui-el-token", "User")# abre una consola nueva para que la veapy -c "import os; print('INVENTARIO_TOKEN' in os.environ)"True

En Linux, en un fichero chmod 600 de la cuenta de servicio. Con un almacén de secretos (Key Vault, CyberArk), el token se lee de ahí.

4. Programarlo

En Windows, con el Programador de tareas, usando el Python del entorno virtual y la carpeta del script como directorio de inicio:

PowerShell · tarea diaria a las 07:00 con una cuenta de servicio
$a = New-ScheduledTaskAction -Execute "C:\scripts\informe-equipos\.venv\Scripts\python.exe" -Argument "informe.py" -WorkingDirectory "C:\scripts\informe-equipos"$t = New-ScheduledTaskTrigger -Daily -At 07:00Register-ScheduledTask -TaskName "IT\Informe equipos" -Action $a -Trigger $t -User "CONTOSO\svc-informes" -Password (Read-Host "Contraseña")

En Linux, con crontab -e del usuario de servicio:

crontab · de lunes a viernes a las 07:00
0 7 * * 1-5 cd /opt/informe-equipos && . ./token.env && .venv/bin/python informe.py

Sin -WorkingDirectory (o el cd en cron), el script busca equipos.csv en C:\Windows\System32 o en el home, y falla con FileNotFoundError aunque a mano funcionara.

5. El ticket: "a mano funciona, programado falla"

El log de la tarea dice una de estas tres cosas:

  1. KeyError: 'INVENTARIO_TOKEN': la tarea corre con otra cuenta (svc-informes) y la variable se definió para tu usuario. Se define para la cuenta de servicio, o a nivel de máquina si la política lo permite.
  2. UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1: el CSV lo ha guardado alguien como CSV (delimitado por comas) en lugar de CSV UTF-8, y viene en cp1252. La solución buena es pedir el fichero en UTF-8; mientras tanto, encoding="cp1252" para ese origen concreto. Si el error es UnicodeEncodeError al imprimir, es la consola de Windows: la variable PYTHONUTF8=1 en la tarea lo evita.
  3. ProxyError o SSLError: CERTIFICATE_VERIFY_FAILED: en tu sesión el proxy se toma de la configuración del usuario y en la tarea no. requests usa las variables HTTPS_PROXY y NO_PROXY; y si el proxy inspecciona TLS, necesita la CA corporativa en REQUESTS_CA_BUNDLE.
PowerShell · variables de máquina para la cuenta de servicio (como administrador)
[Environment]::SetEnvironmentVariable("HTTPS_PROXY", "http://proxy.contoso.local:8080", "Machine")[Environment]::SetEnvironmentVariable("NO_PROXY", "localhost,.contoso.local", "Machine")[Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", "C:\scripts\ca\contoso-root.pem", "Machine")

verify=False hace que el error desaparezca y deja la conexión abierta a cualquiera que se ponga en medio, con el token viajando dentro. No se usa, tampoco "para probar".

Siguientes pasos

  • La guía de PowerShell cubre lo mismo en el lenguaje nativo de Windows; muchas tareas de IT se hacen mejor en uno u otro, y conviene conocer los dos.
  • Microsoft Graph: la misma estructura de script sirve para leer equipos de Intune o usuarios de Entra ID, con un token de aplicación en lugar de uno fijo.
  • Cuando tienes varios de estos scripts, el siguiente paso es una consola que los reúna. Es la idea del repositorio 05-it-ops-console de mi GitHub.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

What you'll get

A Python script that reads a list of devices from a CSV, asks a REST API for each one's status, writes a report as CSV and HTML, leaves a log of what it did and runs by itself every morning. It is the pattern of almost any operations automation: input, query, report, schedule. And what to do when it works on your machine and fails on the server.

Before you start

  • Python 3 from python.org (on Windows, with the py launcher) or the system one on Linux.
  • A folder for the script, for example C:\scripts\device-report or /opt/device-report.
  • The API URL and a read-only token, requested from whoever administers it.
  • If the company uses a proxy with TLS inspection, the corporate CA certificate in PEM format (the network or security team will give it to you).

1. One virtual environment per script

Each script with its own libraries, without touching the system Python:

PowerShell · create the environment and install requests
cd C:\scripts\device-reportpy -m venv .venv.\.venv\Scripts\Activate.ps1pip install requestspip freeze > requirements.txt

On Linux, python3 -m venv .venv and source .venv/bin/activate. With requirements.txt the environment is rebuilt on another server (pip install -r requirements.txt); .venv is not copied or put in Git.

2. The script

report.py
import csvimport htmlimport loggingimport osfrom datetime import date import requests API = "https://api.contoso.local/v1/devices"TOKEN = os.environ["INVENTORY_TOKEN"]  # never written in the codelogging.basicConfig(filename="report.log", level=logging.INFO, encoding="utf-8",                    format="%(asctime)s %(levelname)s %(message)s") def status(device):    r = requests.get(f"{API}/{device}", headers={"Authorization": f"Bearer {TOKEN}"}, timeout=15)    r.raise_for_status()    return r.json() rows = []with open("devices.csv", newline="", encoding="utf-8-sig") as f:    for line in csv.DictReader(f):        name = line["Device"]        try:            d = status(name)            rows.append({"Device": name, "OS": d.get("os"), "Last seen": d.get("lastSeen")})        except requests.RequestException as e:            logging.warning("%s: %s", name, e)            rows.append({"Device": name, "OS": "", "Last seen": "ERROR"})logging.info("%d devices processed", len(rows)) out = f"report-{date.today()}.csv"with open(out, "w", newline="", encoding="utf-8-sig") as f:    w = csv.DictWriter(f, fieldnames=["Device", "OS", "Last seen"])    w.writeheader()    w.writerows(rows) cells = "".join(f"<tr><td>{html.escape(str(x['Device']))}</td><td>{html.escape(str(x['OS']))}</td>"                f"<td>{html.escape(str(x['Last seen']))}</td></tr>" for x in rows)with open(out.replace(".csv", ".html"), "w", encoding="utf-8") as f:    f.write(f"<meta charset='utf-8'><table><tr><th>Device</th><th>OS</th><th>Last seen</th></tr>{cells}</table>")

What matters in each part:

  • encoding="utf-8-sig" when reading: Excel saves CSVs as UTF-8 with a BOM, and without -sig the first column is called \ufeffDevice and the KeyError is baffling. When writing, the BOM makes Excel open accented characters correctly.
  • timeout=15: without it, an API that doesn't respond leaves the script hanging forever, and the scheduled task never finishes.
  • raise_for_status() turns a 401 or a 500 into an exception the try catches: one failing device doesn't bring down the whole report, and it ends up in the log.

3. The token, in an environment variable

The token is a password. It doesn't go in the script, the CSV or Git. It goes in an environment variable of the user who runs the script:

PowerShell · environment variable for the current user
[Environment]::SetEnvironmentVariable("INVENTORY_TOKEN", "paste-the-token-here", "User")# open a new console so it picks it uppy -c "import os; print('INVENTORY_TOKEN' in os.environ)"True

On Linux, in a chmod 600 file owned by the service account. With a secrets store (Key Vault, CyberArk), the token is read from there.

4. Scheduling it

On Windows, with Task Scheduler, using the virtual environment's Python and the script folder as the start directory:

PowerShell · daily task at 07:00 with a service account
$a = New-ScheduledTaskAction -Execute "C:\scripts\device-report\.venv\Scripts\python.exe" -Argument "report.py" -WorkingDirectory "C:\scripts\device-report"$t = New-ScheduledTaskTrigger -Daily -At 07:00Register-ScheduledTask -TaskName "IT\Device report" -Action $a -Trigger $t -User "CONTOSO\svc-reports" -Password (Read-Host "Password")

On Linux, with the service user's crontab -e:

crontab · Monday to Friday at 07:00
0 7 * * 1-5 cd /opt/device-report && . ./token.env && .venv/bin/python report.py

Without -WorkingDirectory (or the cd in cron), the script looks for devices.csv in C:\Windows\System32 or the home folder, and fails with FileNotFoundError even though it worked by hand.

5. The ticket: "it works by hand, it fails when scheduled"

The task's log says one of these three things:

  1. KeyError: 'INVENTORY_TOKEN': the task runs under another account (svc-reports) and the variable was defined for your user. Define it for the service account, or at machine level if policy allows.
  2. UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf1: someone saved the CSV as CSV (Comma delimited) instead of CSV UTF-8, and it comes in cp1252. The proper fix is to ask for the file in UTF-8; in the meantime, encoding="cp1252" for that specific source. If the error is UnicodeEncodeError when printing, it is the Windows console: the PYTHONUTF8=1 variable on the task avoids it.
  3. ProxyError or SSLError: CERTIFICATE_VERIFY_FAILED: in your session the proxy comes from the user's settings and in the task it doesn't. requests uses the HTTPS_PROXY and NO_PROXY variables; and if the proxy inspects TLS, it needs the corporate CA in REQUESTS_CA_BUNDLE.
PowerShell · machine variables for the service account (as administrator)
[Environment]::SetEnvironmentVariable("HTTPS_PROXY", "http://proxy.contoso.local:8080", "Machine")[Environment]::SetEnvironmentVariable("NO_PROXY", "localhost,.contoso.local", "Machine")[Environment]::SetEnvironmentVariable("REQUESTS_CA_BUNDLE", "C:\scripts\ca\contoso-root.pem", "Machine")

verify=False makes the error go away and leaves the connection open to anyone who sits in the middle, with the token travelling inside it. It is not used, not even "to test".

Next steps

  • The PowerShell guide covers the same in Windows' native language; many IT tasks are better done in one or the other, and it pays to know both.
  • Microsoft Graph: the same script structure works for reading Intune devices or Entra ID users, with an application token instead of a fixed one.
  • Once you have several of these scripts, the next step is a console that brings them together. That is the idea behind the 05-it-ops-console repository on my GitHub.

Is something missing, or different in your environment? Get in touch and I'll fix it.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →