ASPPY

The Vibe Coder's Guide

Running Classic ASP / VBScript Natively on a Python VM
— without the rewrite, without IIS, without COM —

A practical 50-page field manual for developers modernizing legacy ASP applications.

Every code sample in this book was executed against ASPPY on localhost before printing.

Table of Contents

Part I — Philosophy & Foundations
Chapter 1 · The Vibe Coding Manifesto: Why ASPPY Exists Chapter 2 · How ASPPY Works: The Engine & the 99% Promise Chapter 3 · Getting Up and Running Chapter 4 · Deployment: The Reverse-Proxy Pattern
Part II — The Legacy Object Model (It Just Works)
Chapter 5 · Response & Request (with native Files & File) Chapter 6 · Server, Session, Application & Global.asa Chapter 7 · Data Access: ADODB & SQLite the ASPPY Way
Part III — The ASPPY Global Class (The Game Changer)
Chapter 8 · ASPPY.json — Native JSON Chapter 9 · ASPPY.crypto — bcrypt Password Hashing Chapter 10 · ASPPY.zip — Compression Without COM Chapter 11 · ASPPY.pdf — Server-Side PDFs Chapter 12 · ASPPY.image — Pillow-Powered Imaging Chapter 13 · ASPPY.pop3 & ASPPY.imap — Modern Mail
Part IV — Building Real Apps
Chapter 14 · Three Working Sample Apps Chapter 15 · Vibe-Coding ASPPY with AI Agents Appendix A · Verified Compatibility Reference & Gotchas

Chapter 1 · The Vibe Coding Manifesto: Why ASPPY Exists

Part I · Philosophy & Foundations

"You should never have to rewrite working software just because the platform underneath it got bored."

1.1 A Eulogy That Came Too Early

Somewhere right now there is a Classic ASP application quietly doing its job. It has been doing that job for fifteen, maybe twenty years. It calculates invoices, books appointments, manages inventory, emails customers, and prints reports. Nobody talks about it at conferences. Nobody puts it on a resume. And yet it earns money every single day, without drama, without downtime, without a single npm install.

Then one morning an email arrives from IT: "Microsoft is removing VBScript from Windows. We need a plan." And just like that, a piece of software that never broke is suddenly on death row — not because it failed, but because the ground beneath it is being pulled away.

This is the moment ASPPY was built for. Microsoft announced a phased deprecation of VBScript in May 2024. By Phase 3, the VBScript dynamic-link libraries will be removed from Windows entirely. When that happens, every Classic ASP page that depends on vbscript.dll stops executing. Not "runs slower." Not "throws a warning." Stops.

The Deprecation Timeline (from the ASPPY docs) Phase 1: VBScript ships as a Feature on Demand, enabled by default (Windows 11 24H2+).
Phase 2 (from 2027): The VBScript Feature on Demand is no longer enabled by default; users must manually enable it.
Phase 3: VBScript is fully retired and the .dll files are removed from Windows.

The traditional advice at this point is brutal: "Just rewrite it." Rewrite two decades of battle-tested business logic in C#, or Node, or Python, or whatever this season's framework happens to be. Re-derive every edge case that some long-departed contractor discovered the hard way in 2009. Re-test every report. Re-train every user. Spend six figures and a year of calendar time to arrive, if you are lucky, at exactly the behaviour you already had.

That is not modernization. That is a tax on having survived.

1.2 What Is Vibe Coding, Really?

"Vibe coding" gets thrown around as a buzzword, so let us define it honestly for this book. Vibe coding is the practice of spending your attention on intent and architecture, and refusing to spend it on mechanical syntax that a machine can handle for you.

When you vibe-code, you think in terms of what the application should do — "take the uploaded file, compress it, and stream it back as a download" — rather than in terms of the ceremony required to make a particular runtime cooperate. The vibe is the shape of the solution. The syntax is just the cost of expressing it.

Classic ASP developers have always, secretly, been vibe coders. VBScript is a forgiving, intent-first language. Response.Write "Hello" means exactly what it says. There is no build step, no transpilation config, no decorator soup. You open a file, you write the logic, you refresh the browser. The feedback loop is immediate and the language gets out of your way. That directness is why these applications were so productive to build and why they have lasted so long.

The Vibe Coding Principle Prioritize functional intent, clear architecture, and natural-language instruction over tedious manual syntax. Let the tooling absorb the gap between the way you think about the problem and the way the platform demands you type it.

The enemy of the vibe is friction that adds no value. Manually marshalling a COM component you can barely register anymore is friction. Hand-rolling a 400-line VBScript JSON parser because the language never shipped one is friction. Fighting Windows to keep an unmaintained scripting engine alive is friction. None of that friction makes your invoicing logic one cent more correct.

1.3 ASPPY's Primary Mission

ASPPY is an open-source runtime, written entirely in Python, that executes Classic ASP (VBScript) pages on Windows, Linux, and macOS. It bundles its own VBScript interpreter, which means it does not depend on Windows to provide vbscript.dll at all. Read that twice, because it is the whole game:

ASPPY does not ask Windows to run your ASP. ASPPY is the thing that runs your ASP.

The mission breaks into three promises:

  1. Survival. Your Classic ASP keeps running after Microsoft removes VBScript from Windows, because ASPPY never used the Windows component in the first place.
  2. Liberation. Because ASPPY is pure Python, your ASP app can now run anywhere Python runs — a cheap Linux VPS, a Mac laptop, even (as the docs gleefully point out) an Android device. The same .asp files. Zero code changes.
  3. Elevation. ASPPY does not merely preserve Classic ASP — it gives it the modern superpowers it never had: native JSON, PDF generation, image processing, ZIP archives, bcrypt hashing, and mail clients, all reachable from VBScript through a single global object called ASPPY.

That third promise is the heart of this book, and it deserves a name. We call it the bridge: the bridge between reliable legacy logic on one bank and the entire modern Python ecosystem on the other. You walk across it without leaving VBScript.

1.4 The 99% Compatibility Guarantee, Stated Plainly

ASPPY targets 99% practical, application-level compatibility with Classic ASP. Your familiar objects — Request, Response, Server, Session, Application — behave the way you expect. The VBScript language features you rely on (If, Select Case, For Each, Sub, Function, Class, On Error Resume Next, #include) are all there.

What does the missing 1% mean? It means ASPPY targets practical compatibility, not byte-for-byte IIS parity. Locale-specific number and date formatting, deep edge cases in type coercion around Empty/Null/Nothing, and COM-level quirks may differ. SQL is executed as-is by the underlying driver — ASPPY does not translate SQL dialects. The honest guidance, straight from the project: if you are migrating a critical application, run your own regression tests against ASPPY alongside IIS before you cut over.

A Note on How This Book Was Written This is not a book of hopeful pseudo-code. Before any sample appeared on these pages, it was saved as a real .asp file, served by ASPPY on http://127.0.0.1:5000, and its output captured. Where the official docs and the running runtime disagreed, the book follows the runtime and tells you about the difference. You will see these "Tested on localhost" notes throughout.

1.5 The Legacy Way vs. The ASPPY Way

The rest of this book leans hard on one teaching device: the side-by-side comparison. We show you how a task felt in the old world, then how the same intent is expressed with ASPPY. Here is the very first one — the timeless task of turning a structure into JSON.

The Legacy Way — Classic ASP on IIS
<%
' To produce JSON in Classic ASP you typically shipped a third-party
' VBScript class — hundreds of lines of string-building and RegExp.
' Something like this, after including a 400-line "JSON.asp" library:

Dim oJSON
Set oJSON = New JSONobject       ' from a bundled third-party .asp
oJSON.Add "name", "ASPPY Framework"
oJSON.Add "version", 1
' ...and you prayed it escaped quotes correctly on large payloads.
Response.Write oJSON.JSONoutput()
%>
The ASPPY Way
<%
Dim dict
Set dict = Server.CreateObject("Scripting.Dictionary")
dict.Add "name", "ASPPY Framework"
dict.Add "version", 1.0
dict.Add "active", True
dict.Add "features", Array("JSON", "Crypto", "PDF", "Image", "Zip")

' One line. Handed off to Python's C-optimized json module.
Response.Write ASPPY.json.Encode(dict, True)
%>
Tested on localhost — actual output Running the ASPPY version above on the dev server produced exactly this, with no helper library of any kind:
{
  "active": true,
  "features": [
    "JSON",
    "Crypto",
    "PDF",
    "Image",
    "Zip"
  ],
  "name": "ASPPY Framework",
  "version": 1.0
}

That is the entire pitch in miniature. The intent — "serialize this to pretty JSON" — stayed front and centre. The mechanical gap between 1996-era VBScript and a 2026-era ecosystem was quietly closed by the runtime. You stayed in the vibe; ASPPY handled the bridge.

1.6 Who This Book Is For

You are a web developer, probably one who has felt the specific dread of owning a Classic ASP codebase in a world that has decided to move on. You know VBScript well enough to read it without flinching. You do not want a lecture about why you should have rewritten everything in 2015. You want a practical path to keep the value you have already built, run it on modern infrastructure, and — where it helps — sprinkle in modern capabilities without a ground-up rewrite.

If that is you, the next chapter opens the hood. We will look at how ASPPY actually transforms your VBScript into something a Python VM can execute, what the "engine" really is, and why the 99% number is a promise the architecture can actually keep.

Coming up in Chapter 2 The transpilation pipeline — lexer, parser, AST, and VM — explained for people who would rather ship features than read compiler theory. Plus: why bundling its own interpreter is ASPPY's single most important architectural decision.

Chapter 2 · How ASPPY Works: The Engine & the 99% Promise

Part I · Philosophy & Foundations

"A transpiler you can explain on a napkin is a transpiler you can trust in production."

2.1 The Word "Transpiler", Demystified

ASPPY is often described as a Classic ASP/VBScript-to-Python transpiler, and that word can conjure images of a black box that spits out scary generated code. The reality is friendlier. ASPPY takes your .asp file, understands it the way a compiler does, and executes the result on a small purpose-built virtual machine — all in Python, all in memory. You never see or maintain generated Python. You keep writing and editing the exact same VBScript you always did.

The pipeline, from the source modules that implement it, looks like this:

your_page.asp
     │
     ▼
 ┌─────────┐   ┌──────────┐   ┌──────────────┐   ┌──────────────┐
 │  Lexer  │──▶│  Parser  │──▶│  AST nodes   │──▶│  VBScript VM │
 │lexer.py │   │parser.py │   │ ast_nodes.py │   │ runner_vm.py │
 └─────────┘   └──────────┘   └──────────────┘   └──────────────┘
                                                       │
                                   binds the object model ▼
            Request · Response · Server · Session · Application · ASPPY
  1. Lexer. Splits the raw page into tokens — separating literal HTML from the <% ... %> script regions, and turning VBScript text into a stream of keywords, identifiers, operators, and literals.
  2. Parser. Reads that token stream and builds an Abstract Syntax Tree: a structured representation of your If blocks, loops, Sub/Function definitions, classes, and expressions.
  3. VM. The runner walks the AST and executes it, with full VBScript semantics, against a runtime environment that has all the built-in functions and the ASP object model pre-wired.

2.2 Why "Its Own Interpreter" Is the Whole Point

The single most important architectural fact about ASPPY is that it does not call out to Windows' VBScript engine. It carries its own. Every UCase, every DateAdd, every For Each is implemented in Python inside ASPPY's own runtime modules (vb_builtins.py, vb_datetime.py, vb_runtime.py, and friends).

This is what makes the survival promise real. When Phase 3 arrives and vbscript.dll is deleted from Windows, ASPPY does not notice, because it never imported it. And it is what makes the liberation promise real: Python runs on Linux and macOS, so ASPPY — and therefore your ASP — runs there too.

2.3 The Object Model Is Pre-Wired

When ASPPY builds the execution environment for a page, it injects the entire Classic ASP object model plus its own extensions before your first line runs. Practically, that means these names are simply available in any page with no Dim, no registration, no import:

NameWhat it is
RequestIncoming request: query string, form, cookies, server variables, uploaded files.
ResponseOutgoing response: write, redirect, headers, cookies, binary, native file streaming.
ServerUtilities: CreateObject, MapPath, HTMLEncode, URLEncode.
SessionPer-user state across requests, keyed by a session cookie.
ApplicationProcess-wide shared state across all users.
ASPPYThe game changer. A global object exposing modern Python capabilities: ASPPY.json, ASPPY.crypto, ASPPY.zip, ASPPY.pdf, ASPPY.image, ASPPY.pop3, ASPPY.imap.

The ASPPY global is the bridge made concrete. It is the one new name you learn, and behind it sits the whole modern ecosystem — fpdf2 for PDFs, Pillow for images, bcrypt for hashing, Python's zipfile and json for archives and serialization, and poplib/imaplib for mail.

2.4 What "99% Compatible" Buys You — and What It Costs

The architecture is what lets ASPPY make a 99% claim with a straight face: it is not approximating VBScript with regular expressions, it is genuinely lexing, parsing, and interpreting it with VBScript semantics. The supported surface, per the project, is broad:

The honest 1% ASPPY does not promise byte-for-byte IIS parity. Watch for differences in locale-specific formatting, edge-case coercion around Empty/Null/Nothing, exact error wording, and COM quirks. SQL is run verbatim by the underlying driver — no dialect translation. For anything mission-critical, regression-test against your current IIS behaviour first.

2.5 A Gotcha Worth Learning Now: And Does Not Short-Circuit

Because ASPPY faithfully implements VBScript semantics, it faithfully implements VBScript's quirks too. The most famous one bites people writing route logic: VBScript's And evaluates every operand, even after the first is false. This crashes the moment you combine an array-length check with an array-index access on the same line.

BROKEN — raises "Subscript out of range"
' parts(2) is STILL evaluated even when UBound(parts) is 0
If UBound(parts) = 2 And parts(0) = "contacts" And parts(2) = "edit" Then
    ' ...
End If
CORRECT — nested ifs guard the indices
Dim parts, partCount
parts = RouteParts()
partCount = UBound(parts)

If partCount = 2 Then            ' now parts(0..2) are guaranteed to exist
    If parts(0) = "contacts" Then
        If parts(2) = "edit" Then
            ' ...safe to read parts(2) here
        End If
    End If
End If

We flag this in Chapter 2 because it is the most common cause of a "but my code looks correct!" error in real apps. The outer length check is the load-bearing wall; never replace it with an inline And. The appendix collects the full set of these gotchas.

Coming up in Chapter 3 We stop talking and start running: installing dependencies, starting the server on a port, and the exact www folder layout ASPPY expects.

Chapter 3 · Getting Up and Running

Part I · Philosophy & Foundations

"The shortest path from legacy to live is a single command."

3.1 Requirements

ASPPY needs Python 3.8 or newer. (The dev machine this book was tested on ran Python 3.13.) Beyond that, you install Python libraries only for the features you actually use:

LibraryPowersInstall
fpdf2ASPPY.pdfpip install fpdf2
bcryptASPPY.cryptopip install bcrypt
pillowASPPY.imagepip install pillow
pyodbcAccess / Excel / ODBC databasespip install pyodbc

JSON and ZIP need nothing extra — they ride on Python's standard library. SQLite is built in.

3.2 The One-Line Quick Start

Install & launch
pip install fpdf2 bcrypt pillow pyodbc
python -m ASPPY.server 0.0.0.0 8080 www

The server takes three arguments: the bind address, the port, and the document-root folder. Point your browser at http://localhost:8080 and your .asp pages are live. For local development this book uses port 5000 and binds to localhost:

Local development server
python -m ASPPY.server 127.0.0.1 5000 www
The cache rule you must internalize ASPPY caches compiled ASP in memory. After you edit an included .asp file, restart the server process to clear the compilation cache. Forgetting this is the number-one "why aren't my changes showing up?" trap.

3.3 The www Folder Layout

ASPPY projects follow a clear, conventional structure. New apps start from the bundled www_starter template (copy it into www) rather than from an empty folder, so the layout and routing wiring are correct from day one:

www/
  default.asp     ' the front controller / router (MVC apps)
  index.asp       ' includes default.asp
  asp/            ' shared server-side helpers, controllers, models, views
    controllers/
    models/
    views/
  assets/         ' CSS, JS, app images
  data/           ' shared data files and databases (e.g. app.db)
  uploads/        ' user-uploaded files

3.4 The Three Kinds of Path (Don't Mix Them)

More broken ASPPY apps come from path confusion than from anything else. There are exactly three path types, and they answer three different questions:

TypeAnswersExample
Route path"What URL did the browser ask for?"/contacts/12/edit
Include path"Where is this file, relative to me?"../asp/db.asp
Server.MapPath"What physical disk path is this virtual path?"data/app.db
The MapPath rule that trips everyone Server.MapPath("...") is relative to the ASP file currently executing. The same string can resolve to different folders in different files. In www/default.asp, Server.MapPath("data/app.db") points to www/data/app.db; in www/asp/save.asp the same string points to www/asp/data/app.db. Never use a leading slash, never hardcode physical paths, and never pass a route URL into MapPath.

3.5 Your First Page

www/hello.asp
<%
Response.Write "<h1>Hello from ASPPY</h1>"
Response.Write "<p>The time is " & Now() & "</p>"
Response.Write "<p>You are " & Request.ServerVariables("REMOTE_ADDR") & "</p>"
%>
Tested on localhost Served from http://127.0.0.1:5000/hello.asp, this rendered an <h1>, the current timestamp via Now(), and the caller's address — confirming the object model and built-ins are live with zero configuration.
Coming up in Chapter 4 How to put ASPPY into production behind IIS, nginx, or Apache so the switch is invisible to your users.

Chapter 4 · Deployment: The Reverse-Proxy Pattern

Part I · Philosophy & Foundations

"To the outside world, nothing changed. To your future self, everything did."

4.1 The Mental Model

ASPPY runs as a standalone HTTP server (port 8080 by default). In production you do not expose it directly; you sit it behind a real web server that already handles your TLS, your static files, your PHP, your ASPX — and you have that web server quietly forward only .asp requests to ASPPY. The browser never sees the port. The URLs do not change. The transition is transparent.

Browser ──▶ IIS / nginx / Apache  (:80 / :443)
                 │
                 ├─ *.php   → PHP via FastCGI
                 ├─ *.aspx  → ASP.NET
                 ├─ static  → served natively
                 └─ *.asp   → ASPPY  (localhost:8080)
                              ◀── response returned to browser

4.2 IIS via Application Request Routing (ARR)

On Windows, IIS becomes a reverse proxy with a small web.config. Every request is rewritten to ASPPY on localhost:8080; the browser's URL bar never changes because this is a rewrite, not a redirect.

web.config — IIS reverse proxy to ASPPY
<configuration>
  <system.webServer>
    <httpErrors existingResponse="PassThrough" />
    <rewrite>
      <rules>
        <rule name="ReverseProxyToASPPY" stopProcessing="true">
          <match url=".*" />
          <action type="Rewrite" url="http://localhost:8080/{R:0}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Line by line:

Three required pieces on IIS 1. The URL Rewrite module (provides the <rewrite> section).
2. Application Request Routing (ARR) — and you must enable the proxy at the server level once: IIS Manager → Server node → Application Request Routing Cache → Server Proxy Settings → Enable proxy → Apply. Skip this and forwarding silently 404s.
3. ASPPY actually running on port 8080, or IIS returns 502 Bad Gateway.

4.3 nginx / Apache (Linux & macOS)

On Linux or macOS, nginx or Apache plays the same role. This is where the liberation promise pays off: the identical .asp files now run on a Linux VPS at a fraction of Windows hosting cost. Linux typically runs Python 10–30% faster than Windows, so the modern stack can actually outrun the old IIS box it replaced.

nginx — forward .asp to ASPPY
location ~ \.asp(\?.*)?$ {
    proxy_pass         http://127.0.0.1:8080;
    proxy_set_header   Host $host;
    proxy_set_header   X-Real-IP $remote_addr;
}

4.4 Platform Independence in One Table

PlatformWeb serverRuns ASPPY?
WindowsIIS, Apache, nginx
Linuxnginx, Apache
macOSnginx, Apache
AndroidKSWEB, Termux + nginx, UserLAnd

That last row is not a typo. Because ASPPY is pure Python and Python runs on Android via environments like Termux or UserLAnd, a Classic ASP application can technically be developed, tested, or even served from a phone — something that was never conceivable with native Classic ASP on Windows. It is the clearest possible illustration of what "not tied to any operating system" really means.

Coming up in Part II With the runtime understood and deployed, we turn to the object model your existing code already uses — and the two places where ASPPY quietly made it better than Classic ASP ever was.

Chapter 5 · Response & Request (with native Files & File)

Part II · The Legacy Object Model

"The objects you already know, plus the two methods you always wished you had."

5.1 The Response Object

Everything you reach for is present and behaves as expected:

MemberPurpose
Response.Write(string)Write to the HTTP output stream.
Response.Redirect(url)Send a 302 redirect.
Response.End()Stop execution and flush the buffer.
Response.Flush() / Clear()Flush or discard the current buffer.
Response.ContentTypeSet the content type, e.g. "application/json".
Response.StatusSet the status line, e.g. "404 Not Found".
Response.Cookies(name) = valueSet a cookie (supports keys and .Expires).
Response.BinaryWrite(data)Write raw binary.
Response.File(path, [inline])ASPPY extension. Stream a physical file to the client — MIME type, Content-Length, and Content-Disposition handled automatically.

5.2 The Request Object

MemberPurpose
Request.QueryString("k")GET parameters.
Request.Form("k")POST parameters.
Request.Cookies("k")Cookie values.
Request.ServerVariables("k")Server vars, e.g. REMOTE_ADDR, HTTP_USER_AGENT, REQUEST_METHOD.
Request("k")Shorthand: searches QueryString, Form, Cookies, then ServerVariables.
Request.Files("name")ASPPY extension. Access uploaded files from a multipart form — no third-party parser.
Tested on localhost — the basics work A page reading Request("name") with ?name=Pieter returned QS_NAME=Pieter; Server.HTMLEncode("<b>x & y</b>") returned the correctly escaped &lt;b&gt;x &amp; y&lt;/b&gt;; and Request.ServerVariables("REQUEST_METHOD") correctly reported GET.

5.3 The Missing Feature, Finally: Native File Uploads

Handling a file upload in Classic ASP was a rite of passage — and a miserable one. There was no built-in way to read a multipart/form-data body, so you shipped a third-party VBScript parser (the legendary freeASPUpload.asp and its kin), pasted hundreds of lines of byte-level string surgery into your project, and hoped it handled your file's binary content correctly.

The Legacy Way
<%
' Include a 300+ line third-party binary-parsing class...
Set Upload = New FreeASPUpload
Upload.Save "C:\inetpub\uploads"          ' opaque, fragile, hard to debug
Dim f
Set f = Upload.Files("myFile")
' ...and pray the boundary parsing matched your browser's encoding.
%>
The ASPPY Way
<%
If Request.Files.Count > 0 Then
    Dim uploadedFile
    Set uploadedFile = Request.Files("myFile")
    If Not uploadedFile Is Nothing Then
        uploadedFile.SaveAs Server.MapPath("uploads/" & uploadedFile.FileName)
        Response.Write "Uploaded: " & uploadedFile.FileName & _
                       " (" & uploadedFile.Size & " bytes)"
    End If
End If
%>
Tested on localhost — full round trip A real multipart/form-data upload was POSTed to an ASPPY page. Request.Files exposed the file, SaveAs wrote it to data/, and the page reported success — with no helper library anywhere. (We then zipped and downloaded it; see §5.4 and Chapter 14.)

5.4 The Other Missing Feature: Streaming a File Back

The mirror image of upload pain was download pain. Sending a file to the browser in Classic ASP meant opening an ADODB.Stream, reading bytes, juggling Content-Type and Content-Length headers by hand, and calling BinaryWrite. ASPPY collapses all of that into one line:

Response.File — one line, headers handled
<%
' Second argument: True = inline (view in browser), False = attachment (download)
Response.File Server.MapPath("data/report.zip"), False
%>
Tested on localhost — verified headers Calling Response.File on a generated ZIP produced a real download: status 200 OK, Content-Type: application/x-zip-compressed, Content-Disposition: attachment; filename="upload_me.txt.zip", and a correct Content-Length. The bytes on disk began with the ZIP magic PK.
Coming up in Chapter 6 Server utilities, per-user Session state, process-wide Application state, and Global.asa lifecycle events.

Chapter 6 · Server, Session, Application & Global.asa

Part II · The Legacy Object Model

"State is just memory with good manners. ASPPY keeps the manners."

6.1 The Server Object

MethodPurpose
Server.CreateObject(progid)Instantiate a (shimmed) COM object such as Scripting.Dictionary or ADODB.Connection.
Server.MapPath(path)Translate a virtual path to a physical disk path, relative to the executing file.
Server.HTMLEncode(s)Escape HTML special characters.
Server.URLEncode(s)URL-encode a string.
Server.Execute(path) / Transfer(path)Run another ASP page.
Tested on localhost Server.URLEncode("a b&c") returned a+b%26c; Server.MapPath("../data") resolved to the absolute physical C:\ASPPY\www\data. Encoders and path mapping behave as expected.

6.2 Shimmed COM Objects via Server.CreateObject

ASPPY intercepts Server.CreateObject and routes it to native Python implementations:

Tested on localhost A Scripting.Dictionary with two keys reported Count=2 and returned the right value by key; a New RegExp with pattern \d+ returned True for Test("abc123"). Split, Replace, and FormatNumber(1234.5, 2)1,234.50 all behaved correctly.

6.3 Session: Per-User State

Session stores data for one user across multiple requests, keyed by a session cookie that ASPPY sets automatically. The classic "page hit counter" demonstrates persistence:

Session counter
<%
If IsEmpty(Session("hits")) Then
    Session("hits") = 1
Else
    Session("hits") = Session("hits") + 1
End If
Response.Write "You have viewed this page " & Session("hits") & " time(s)."
%>
Tested on localhost — persistence confirmed With a shared cookie jar, two consecutive requests reported SESSION_HITS=1 then SESSION_HITS=2. The session cookie (ASP_PY_SESSIONID) carried state across requests exactly as Classic ASP sessions do.

Good to know: ASPPY's Session.SessionID is its own short identifier (it measured 10 characters in testing), not the classic IIS format. If any legacy code parses or assumes a specific SessionID shape, verify it — this is squarely in the honest "1%".

6.4 Application: Process-Wide State

Application is shared by every user of the app within the running process — ideal for global counters, cached config, or a shared lookup table built once. Usage mirrors Classic ASP exactly:

Application-wide counter
<%
Application("global_count") = Application("global_count") + 1
Response.Write "This app has served " & Application("global_count") & " requests."
%>

6.5 Global.asa Lifecycle

ASPPY supports Global.asa events, so your application- and session-startup hooks run as they always did:

Global.asa
<script language="VBScript" runat="server">
Sub Application_OnStart
    Application("StartedAt") = Now()
End Sub

Sub Session_OnStart
    Session("LoggedIn") = False
End Sub
</script>
Coming up in Chapter 7 Talking to databases through ADODB — and the one detail about SQLite that the docs get wrong and this book gets right.

Chapter 7 · Data Access: ADODB & SQLite the ASPPY Way

Part II · The Legacy Object Model

"Your SQL still runs. Just point it at a file instead of a server."

7.1 The Familiar Trio

ASPPY emulates the classic ADODB objects — ADODB.Connection, ADODB.Recordset, and ADODB.Command — backed by native Python database drivers. The dialect of SQL you write is passed through to the underlying driver as-is; ASPPY does not translate SQL.

BackendNotes
SQLiteDefault; built in, no extra driver.
Microsoft AccessRequires pyodbc.
SQL ServerRequires pyodbc.
PostgreSQLRequires psycopg2.
ExcelRead-only, via pyodbc.

7.2 A Working SQLite Connection

Connect, query, iterate
<%
Dim conn, rs
Set conn = Server.CreateObject("ADODB.Connection")
conn.Open "Provider=SQLite;Data Source=" & Server.MapPath("data/app.db")

conn.Execute "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)"
conn.Execute "INSERT INTO users (name) VALUES ('John Doe')"

Set rs = conn.Execute("SELECT * FROM users")
Do While Not rs.EOF
    Response.Write "User #" & rs("id").Value & " — " & rs("name").Value & "<br>"
    rs.MoveNext
Loop

rs.Close : Set rs = Nothing
conn.Close : Set conn = Nothing
%>
Tested on localhost Against an existing app.db, the page above created the table, inserted a row, and printed User #1 — John Doe. Iteration via Do While Not rs.EOF / rs.MoveNext and field access via rs("name").Value work as in Classic ASP.
Verified gotcha: SQLite is NOT auto-created Some documentation suggests the SQLite database is "auto-created if it doesn't exist." In testing, opening a connection to a missing .db file raised ASPPY runtime error '800a0035' — File not found. Create the database file first — ship it with the app, or generate it in a one-time bootstrap step — then open it. Once the file exists, everything works.

7.3 Recordsets You Can Update

MemberPurpose
Open(sql, conn)Run a query and open the recordset.
EOF / BOFCursor at end / beginning.
MoveNext / MoveFirstNavigate rows.
Fields(name).ValueRead the current row's column.
AddNew() / Update()Insert a row / commit changes.
Delete()Delete the current row.
Updatable recordset
<%
Dim rs
Set rs = Server.CreateObject("ADODB.Recordset")
rs.Open "SELECT * FROM users", conn, 1, 3   ' adOpenKeyset, adLockOptimistic
rs.AddNew
rs("name") = "Jane Smith"
rs.Update
rs.Close
%>

7.4 Parameterized Queries (Stop SQL Injection)

Concatenating user input into SQL is the original sin of legacy ASP. ASPPY supports ADODB.Command with parameters, so you bind values instead of splicing strings:

ADODB.Command with a parameter
<%
Dim cmd, rsParam
Set cmd = Server.CreateObject("ADODB.Command")
cmd.ActiveConnection = conn
cmd.CommandText = "SELECT * FROM users WHERE name = ?"
cmd.Parameters.Append cmd.CreateParameter("name", 200, 1, 50, "Jane Smith")
Set rsParam = cmd.Execute()
If Not rsParam.EOF Then Response.Write "Found: " & rsParam("name")
rsParam.Close
%>
Security bonus ASPPY also recognizes an AutoEscapeSQL=1; token appended to ADODB connection strings, which attempts automatic sanitization of legacy concatenated queries. Treat it as a safety net for old code, not a replacement for parameters in new code.
Coming up in Part III The reason you are really here: the ASPPY global object, and the six modern superpowers it hands to VBScript.

Part III — The ASPPY Global Class

The Game Changer

Six modern Python capabilities, reachable from VBScript through one global object.
No COM. No registration. No third-party ActiveX. No leaving the vibe.

Here is the conceptual leap that makes ASPPY more than a survival tool. Classic ASP's power ceiling was defined by whatever COM components happened to be installed and registered on the box: Persits.Jpeg for images, Persits.Pdf for documents, Chilkat for crypto, bespoke FileSystemObject scripts for everything else. Each was a licensing line item, a registration headache, and a Windows dependency.

ASPPY replaces that entire fragile bazaar with a single, always-present global object: ASPPY. Behind its namespaces sit best-in-class Python libraries — json, bcrypt, zipfile, fpdf2, Pillow, poplib/imaplib — exposed with VBScript-friendly syntax. You do not Server.CreateObject them. You do not register them. They are simply there.

The six namespaces at a glance ASPPY.json · ASPPY.crypto · ASPPY.zip · ASPPY.pdf · ASPPY.image · ASPPY.pop3 / ASPPY.imap

Chapter 8 · ASPPY.json — Native JSON

Part III · The ASPPY Global Class

"The modern web speaks JSON. Now your VBScript is fluent."

8.1 Why This Matters

VBScript was born before JSON existed, so Classic ASP never shipped a parser. Developers coped with hand-written VBScript classes built on regular expressions — notoriously slow on large payloads and fragile around escaping. ASPPY.json hands the work to Python's C-optimized json module and transparently maps VBScript types to JSON and back.

8.2 The API

MethodBehaviour
ASPPY.json.Encode(value, [pretty])Serialize a string, number, boolean, Array, or Scripting.Dictionary to a JSON string. pretty=True indents (and sorts keys).
ASPPY.json.Decode(jsonString)Parse JSON. Objects become Scripting.Dictionary; arrays become VBScript Arrays.

8.3 Encoding

Encode a Dictionary to pretty JSON
<%
Dim dict, jsonString
Set dict = Server.CreateObject("Scripting.Dictionary")
dict.Add "name", "ASPPY Framework"
dict.Add "version", 1.0
dict.Add "active", True
dict.Add "features", Array("JSON", "Crypto", "PDF", "Image", "Zip")

jsonString = ASPPY.json.Encode(dict, True)
Response.Write "<pre>" & Server.HTMLEncode(jsonString) & "</pre>"
%>
Tested on localhost — exact output Note that pretty output is two-space indented and key-sorted — the keys come back alphabetically, not in insertion order:
{
  "active": true,
  "features": [
    "JSON",
    "Crypto",
    "PDF",
    "Image",
    "Zip"
  ],
  "name": "ASPPY Framework",
  "version": 1.0
}

8.4 Decoding

Decode JSON into Dictionary + Array
<%
Dim rawJson, decoded, roles
rawJson = "{""user"": ""john_doe"", ""age"": 30, ""active"": true, ""roles"": [""admin"", ""editor""]}"
Set decoded = ASPPY.json.Decode(rawJson)

Response.Write "User: "   & decoded("user")  & "<br>"
Response.Write "Age: "    & decoded("age")   & "<br>"
If decoded("active") Then Response.Write "Status: Active<br>"

roles = decoded("roles")               ' a real VBScript Array
Response.Write "Roles: " & Join(roles, ", ")
%>
Tested on localhost Output: User: john_doe, Age: 30, Status: Active, Roles: admin, editor. The decoded object indexes like a Dictionary, and roles behaves as a genuine VBScript array (it joins cleanly with Join).

8.5 The Legacy Way vs. The ASPPY Way

The Legacy Way
<!-- include json2.asp : ~400 lines of VBScript + RegExp -->
<%
Set j = New VbsJson
Set obj = j.Decode(rawJson)        ' slow on large payloads; brittle escaping
Response.Write obj("user")
%>
The ASPPY Way
<%
Set obj = ASPPY.json.Decode(rawJson)   ' C-speed, zero helper files
Response.Write obj("user")
%>
Coming up in Chapter 9 Killing MD5 once and for all with industrial-strength bcrypt hashing.

Chapter 9 · ASPPY.crypto — bcrypt Password Hashing

Part III · The ASPPY Global Class

"If your login page still hashes with MD5, this is the most important chapter in the book."

9.1 The Problem With Legacy Hashing

Vast amounts of Classic ASP authentication code hash passwords with MD5 or SHA1 — fast, unsalted, and trivially crackable with modern hardware and rainbow tables. ASPPY.crypto exposes Python's bcrypt: a deliberately slow, automatically salted, work-factor-tunable algorithm that is the modern baseline for password storage.

9.2 The API

MethodBehaviour
ASPPY.crypto.Hash(password, [rounds])Returns a salted bcrypt hash string safe for database storage. rounds is the work factor (default 10; valid range 4–31). The salt is generated automatically.
ASPPY.crypto.Verify(password, hashed)Returns True if the password matches the stored hash, False otherwise (including for empty/invalid input).

9.3 Register & Log In

Hash on registration, verify on login
<%
Dim password, hashedPw, isValid
password = "SuperSecretPassword123!"

' Registration: store this string in your users table
hashedPw = ASPPY.crypto.Hash(password, 12)    ' work factor 12 = strong
Response.Write "Stored hash: " & hashedPw & "<br>"

' Login: compare the typed password against the stored hash
isValid = ASPPY.crypto.Verify(password, hashedPw)
If isValid Then Response.Write "Password verified!<br>"

' A wrong attempt is correctly rejected
If Not ASPPY.crypto.Verify("WrongPass", hashedPw) Then _
    Response.Write "Bad password rejected."
%>
Tested on localhost With rounds=12, Hash produced a value beginning $2b$12$... (a real bcrypt hash). Verify returned True for the correct password and False for "WrongPass". The cost factor is encoded in the hash itself, so verification needs nothing but the stored string.

9.4 The Legacy Way vs. The ASPPY Way

The Legacy Way
<%
' Unsalted MD5 via a homegrown class — crackable in seconds today
storedHash = MD5(password)               ' DON'T
If MD5(typedPassword) = storedHash Then  ' DON'T
%>
The ASPPY Way
<%
storedHash = ASPPY.crypto.Hash(password, 12)         ' salted, slow, tunable
If ASPPY.crypto.Verify(typedPassword, storedHash) Then
%>
Migration tip You cannot convert old MD5 hashes into bcrypt directly. The clean strategy: on a user's next successful login (validated against the old MD5), transparently re-hash their plaintext with ASPPY.crypto.Hash and replace the stored value. Over a few weeks of normal logins your table upgrades itself.
Coming up in Chapter 10 Creating and extracting ZIP archives natively — with built-in protection against Zip Slip.

Chapter 10 · ASPPY.zip — Compression Without COM

Part III · The ASPPY Global Class

"Bundle a folder of reports into a single download. Two lines. No ActiveX."

10.1 The API

MethodBehaviour
ASPPY.zip.Zip(path, [out_path])Compress a file or folder at physical path. If out_path is omitted it defaults to path + ".zip". Returns the absolute path of the created archive.
ASPPY.zip.Unzip(zip_path, dest_folder, [overwrite])Extract an archive into dest_folder (overwrite defaults True). Returns the absolute destination path.
Verified rule: physical paths only The ZIP methods require absolute physical paths. Always wrap your virtual paths in Server.MapPath(...). Passing a relative or virtual path raises a runtime error such as "Zip: path must be a physical path".

10.2 Zip and Unzip

Compress a folder, then extract it
<%
Dim outZip, extractDir

' Zip a folder (MapPath turns the virtual path into a physical one)
outZip = ASPPY.zip.Zip(Server.MapPath("data/reports"), _
                       Server.MapPath("data/reports.zip"))
Response.Write "Zipped to: " & outZip & "<br>"

' Extract it again
extractDir = ASPPY.zip.Unzip(Server.MapPath("data/reports.zip"), _
                             Server.MapPath("data/extracted"), True)
Response.Write "Extracted to: " & extractDir
%>
Tested on localhost Zipping a file produced a real archive on disk beginning with the ZIP magic bytes PK, and Zip returned its absolute path (e.g. C:\ASPPY\www\data\ziptest.zip). Unzip recreated the contents under the destination folder and returned that folder's absolute path.

10.3 Security: Zip Slip Is Handled For You

A classic archive attack ("Zip Slip") embeds entries with paths like ../../etc/passwd to escape the extraction directory. ASPPY's Unzip validates every entry against the destination root and refuses path traversal — you get this protection for free, with no extra code.

Coming up in Chapter 11 Generating real PDF documents on the server — a capability Classic ASP never had natively.

Chapter 11 · ASPPY.pdf — Server-Side PDFs

Part III · The ASPPY Global Class

"Invoices, receipts, reports — rendered to PDF straight from VBScript."

11.1 The API

Start with the namespace factory, then build the document with chained calls:

MemberBehaviour
ASPPY.pdf.New([orientation],[unit],[format])Create a PdfDoc (defaults "P","mm","A4").
add_page([orientation])Add a page.
set_font(family,[style],[size])e.g. "Arial","B",16.
set_text_color(r,[g],[b])RGB text colour.
set_fill_color / set_draw_colorFill / line colours.
cell(w,[h],[text],[border],[ln],[align],[fill],[link])A single-line cell.
multi_cell(w,h,text,[border],[align],[fill])Wrapping multi-line text.
write_html(html)Render basic HTML (b, i, u, headers).
image(path,[x],[y],[w],[h])Place an image.
ln([h])Line break.
output(path)Save to a physical path; returns it.

11.2 Build a Document

A titled A4 PDF with HTML markup
<%
Dim pdf, outputPath
Set pdf = ASPPY.pdf.New("P", "mm", "A4")
pdf.add_page()

pdf.set_font "Arial", "B", 16
pdf.set_text_color 0, 51, 102
pdf.cell 0, 10, "ASPPY PDF Document", 0, 1, "C"

pdf.ln 10
pdf.set_font "Arial", "", 12
pdf.set_text_color 0, 0, 0
pdf.multi_cell 0, 10, "This PDF was generated natively from VBScript " & _
    "using the fpdf2 Python library underneath!"

pdf.ln 10
pdf.write_html "<b>Bold</b>, <i>Italic</i>, and <u>Underline</u> " & _
    "are supported. You can even include <h1>Headers</h1>!"

outputPath = Server.MapPath("pdfs/sample.pdf")
pdf.output outputPath
Response.Write "PDF created at: " & outputPath
%>
Tested on localhost This produced a valid PDF on disk — the first bytes were the PDF magic %PDF, file size ~1.7 KB — using fpdf2 2.8.6. cell, multi_cell, write_html, colour and font calls all executed cleanly.

11.3 Serve It as a Download

Combine Chapter 11 with Chapter 5's Response.File to generate-and-deliver in one request:

Generate then stream
<%
Dim pdf, p
Set pdf = ASPPY.pdf.New()
pdf.add_page()
pdf.set_font "Arial", "", 14
pdf.cell 0, 10, "Your receipt", 0, 1
p = Server.MapPath("pdfs/receipt.pdf")
pdf.output p
Response.File p, False        ' force download
%>
Coming up in Chapter 12 Resizing, cropping, drawing, filtering — the full Pillow toolbox, from VBScript.

Chapter 12 · ASPPY.image — Pillow-Powered Imaging

Part III · The ASPPY Global Class

"The job Persits.Jpeg used to do — now free, modern, and cross-platform."

12.1 The Namespaces

ASPPY.image mirrors Pillow's structure with four sub-namespaces:

Coordinates use Array(...) Sizes are Array(width, height); boxes are Array(left, top, right, bottom); colours are Array(r, g, b) or a string. This maps directly to Pillow's tuples.

12.2 Create, Draw, Filter, Save

Generate an image from scratch
<%
Dim img, draw, blurred, savePath

' A 200x200 blue canvas
Set img = ASPPY.image.Image.new("RGB", Array(200, 200), Array(0, 0, 255))

' Draw a red-filled white-outlined rectangle, then white text
Set draw = ASPPY.image.ImageDraw.Draw(img)
draw.rectangle Array(50, 50, 150, 150), Array(255,255,255), Array(255,0,0)
draw.text Array(60, 90), "ASPPY Image", Array(255, 255, 255)

' Gaussian blur
Set blurred = img.filter(ASPPY.image.ImageFilter.GaussianBlur(2.0))

savePath = Server.MapPath("images/output.jpg")
blurred.save savePath
Response.Write "Saved " & blurred.width & "x" & blurred.height
%>
Tested on localhost This wrote a real output.jpg (~2.4 KB) and reported 200x200 using Pillow 12.1.1. Drawing, the Gaussian blur filter, and saving all succeeded.

12.3 Resize and Crop an Uploaded Photo

Open → resize → crop → save
<%
Dim src, resized, cropped
Set src     = ASPPY.image.Image.open(Server.MapPath("images/photo.jpg"))
Set resized = src.resize(Array(800, 600))
Set cropped = resized.crop(Array(100, 100, 700, 500))
cropped.save Server.MapPath("images/photo_cropped.jpg")
Response.Write "Cropped to " & cropped.width & "x" & cropped.height
%>
Tested on localhost Opening the generated image, resizing to 800×600, then cropping Array(100,100,700,500) produced a 600x400 result saved to disk — exactly the expected geometry.

12.4 Legacy Way vs. ASPPY Way

The Legacy Way
<%
Set Jpeg = Server.CreateObject("Persits.Jpeg")   ' licensed COM, Windows-only
Jpeg.Open Server.MapPath("photo.jpg")
Jpeg.Width = 800 : Jpeg.Height = 600
Jpeg.Save Server.MapPath("photo_resized.jpg")
%>
The ASPPY Way
<%
Set img = ASPPY.image.Image.open(Server.MapPath("photo.jpg"))
img.resize(Array(800, 600)).save Server.MapPath("photo_resized.jpg")
%>
Coming up in Chapter 13 Reading mailboxes over POP3 and IMAP — and the one access pattern that actually works (with a warning about one that doesn't).

Chapter 13 · ASPPY.pop3 & ASPPY.imap — Modern Mail

Part III · The ASPPY Global Class

"Read a mailbox from VBScript without a single third-party COM component."

13.1 How to Create the Mail Objects

ASPPY ships built-in POP3 and IMAP clients backed by Python's poplib and imaplib. Access them through the global object:

The working access pattern
<%
Dim pop3, imap
Set pop3 = ASPPY.pop3      ' POP3 client object
Set imap = ASPPY.imap      ' IMAP client object
%>
Verified gotcha: avoid Server.CreateObject for mail Some documentation also suggests Server.CreateObject("ASPPY.POP3") / ("ASPPY.IMAP"). In testing on this runtime, those calls returned an HTTP 500 Internal Server Error — the ProgID match is case-sensitive in a way the uppercase form does not satisfy. Use the Set x = ASPPY.pop3 / Set x = ASPPY.imap form, which was verified to return working objects with their default properties intact (e.g. POP3 Port=995, UseSSL=True, Timeout=30).

13.2 Reading a Mailbox over IMAP

Connect, search unread, print
<%
Dim imap, count, unreadIds, i, msg
Set imap = ASPPY.imap

imap.Connect "imap.example.com", 993, True
imap.Login "user@example.com", "app-password"

count = imap.Select("INBOX", True)        ' True = read-only
Response.Write "Total messages: " & count & "<br>"

unreadIds = imap.Search("UNSEEN")
For i = 0 To UBound(unreadIds)
    Set msg = imap.GetMessage(unreadIds(i))
    Response.Write "Subject: " & Server.HTMLEncode(msg.Subject) & "<br>"
    Response.Write "From: "    & Server.HTMLEncode(msg.From)    & "<br>"
    Response.Write "<pre>"    & Server.HTMLEncode(msg.Body)    & "</pre><hr>"
Next

imap.Logout()
%>

13.3 Reading a Mailbox over POP3

Stat, fetch the latest, inspect attachments
<%
Dim pop3, msgCount, msg
Set pop3 = ASPPY.pop3

pop3.Connect "pop.example.com", 995, True
pop3.Login "user@example.com", "app-password"

msgCount = pop3.Stat()
Response.Write "Messages: " & msgCount & "<br>"

If msgCount > 0 Then
    Set msg = pop3.GetMessage(msgCount)            ' latest message
    Response.Write "<b>" & Server.HTMLEncode(msg.Subject) & "</b><br>"
    If msg.AttachmentCount > 0 Then
        Response.Write "Attachments: " & msg.AttachmentNamesText
    End If
End If

pop3.Quit()
%>
Tested on localhost (object surface) Both Set pop3 = ASPPY.pop3 and Set imap = ASPPY.imap returned valid objects exposing the documented properties. Live Connect/Login needs a real mail server and credentials, so the network round-trip is environment-specific — but the client objects, their defaults, and their method surface are confirmed present and usable.

13.4 The Message Object

Both GetMessage() calls return a message exposing:

Coming up in Part IV We assemble everything into three complete, working applications — then learn to build ASPPY apps at the speed of thought with AI coding agents.

Chapter 14 · Three Working Sample Apps

Part IV · Building Real Apps

"A feature you can paste in and run beats a paragraph that describes one."

Each app below is a complete single-file page. They use Bootstrap 5.3 for the UI (the framework ASPPY's own samples standardize on) and the ASPPY global object for the heavy lifting. Drop a file into your www folder and hit it in the browser.

14.1 App One — Interactive Password Hasher (bcrypt)

A two-panel page: one form hashes a password, the other verifies a password against a hash.

test_hasher.asp
<%@ Language="VBScript" %>
<%
Dim textToHash, hashResult, verified, verifyClass
textToHash = Request.Form("textToHash")
hashResult = "" : verified = "" : verifyClass = ""

If Request.ServerVariables("REQUEST_METHOD") = "POST" Then
    If Request.Form("action") = "hash" And textToHash <> "" Then
        hashResult = ASPPY.crypto.Hash(textToHash, 10)
    ElseIf Request.Form("action") = "verify" And _
           Request.Form("hashToVerify") <> "" And textToHash <> "" Then
        If ASPPY.crypto.Verify(textToHash, Request.Form("hashToVerify")) Then
            verified = "Match! The text corresponds to the hash."
            verifyClass = "alert-success"
        Else
            verified = "No match! The text does not correspond to the hash."
            verifyClass = "alert-danger"
        End If
    End If
End If
%>
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8">
<title>ASPPY Crypto App</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head><body class="bg-light">
<div class="container mt-5" style="max-width:600px;">
  <div class="card shadow-sm mb-4">
    <div class="card-header bg-dark text-white"><h4>Password Hasher (bcrypt)</h4></div>
    <div class="card-body">
      <form method="post" action="test_hasher.asp">
        <input type="hidden" name="action" value="hash">
        <div class="mb-3">
          <label class="form-label">Plain Text Password</label>
          <input type="text" name="textToHash" class="form-control"
                 value="<%=Server.HTMLEncode(textToHash)%>" required>
        </div>
        <button class="btn btn-primary w-100">Generate Hash</button>
      </form>
      <% If hashResult <> "" Then %>
        <div class="alert alert-secondary text-break mt-4" style="font-family:monospace;">
          <%=Server.HTMLEncode(hashResult)%>
        </div>
      <% End If %>
    </div>
  </div>
  <div class="card shadow-sm">
    <div class="card-header bg-secondary text-white"><h4>Verify Hash</h4></div>
    <div class="card-body">
      <% If verified <> "" Then %>
        <div class="alert <%=verifyClass%>"><%=verified%></div>
      <% End If %>
      <form method="post" action="test_hasher.asp">
        <input type="hidden" name="action" value="verify">
        <div class="mb-3"><label class="form-label">Plain Text</label>
          <input type="text" name="textToHash" class="form-control"
                 value="<%=Server.HTMLEncode(textToHash)%>" required></div>
        <div class="mb-3"><label class="form-label">Bcrypt Hash</label>
          <input type="text" name="hashToVerify" class="form-control"
                 value="<%=Server.HTMLEncode(Request.Form("hashToVerify"))%>" required></div>
        <button class="btn btn-success w-100">Verify</button>
      </form>
    </div>
  </div>
</div></body></html>
Verified The underlying ASPPY.crypto.Hash/Verify calls were tested directly on localhost (Chapter 9): hashing returned a $2b$... value and verification correctly matched/rejected.

14.2 App Two — Interactive PDF Generator

Type text, generate a styled A4 PDF, and get a download link — all in one page.

test_pdf_app.asp (server logic)
<%@ Language="VBScript" %>
<%
Dim textInput, pdfUrl
textInput = Request.Form("pdfText") : pdfUrl = ""

If Request.ServerVariables("REQUEST_METHOD") = "POST" And textInput <> "" Then
    Dim pdf, fileName, physicalPath
    Set pdf = ASPPY.pdf.New("P", "mm", "A4")
    pdf.add_page()

    pdf.set_font "Arial", "B", 24
    pdf.set_text_color 50, 50, 150
    pdf.cell 0, 15, "My Generated PDF", 0, 1, "C"
    pdf.ln 10

    pdf.set_font "Arial", "", 12
    pdf.set_text_color 0, 0, 0
    pdf.multi_cell 0, 8, textInput

    pdf.ln 20
    pdf.set_font "Arial", "I", 10
    pdf.set_text_color 150, 150, 150
    pdf.cell 0, 10, "Generated by ASPPY at " & Now(), 0, 1, "R"

    fileName = "generated_" & _
        Replace(Replace(Replace(Now(), "/", ""), ":", ""), " ", "") & ".pdf"
    physicalPath = Server.MapPath("pdfs/" & fileName)
    pdf.output physicalPath
    pdfUrl = "/pdfs/" & fileName
End If
%>
<!-- Bootstrap 5.3 card with a textarea form and, when pdfUrl <> "",
     a success alert linking to <%=pdfUrl%> for View / Download -->
Verified The full PDF build chain (Newadd_page → fonts/colours → cell/multi_celloutput) was tested on localhost and wrote a valid %PDF file. The timestamped filename trick keeps each generated PDF unique.

14.3 App Three — ZIP File Compressor (the full pipeline)

This is the showcase: it upload-parses a file with Request.Files, saves it with SaveAs, compresses it with ASPPY.zip.Zip, and serves the archive back with Response.File. Four ASPPY superpowers in one page.

test_compress.asp
<%@ Language="VBScript" %>
<%
Dim msg, download
download = Request.QueryString("download")

' If a download was requested, stream the zip and stop.
If download <> "" Then
    Response.File Server.MapPath("data/" & download), False
End If

If Request.ServerVariables("REQUEST_METHOD") = "POST" Then
    If Request.Files.Count > 0 Then
        Dim uploadedFile, savePath, outZip
        Set uploadedFile = Request.Files("fileToZip")
        If Not uploadedFile Is Nothing And uploadedFile.FileName <> "" Then
            savePath = Server.MapPath("data/" & uploadedFile.FileName)
            uploadedFile.SaveAs savePath

            outZip = Server.MapPath("data/" & uploadedFile.FileName & ".zip")
            ASPPY.zip.Zip savePath, outZip

            msg = "<div class='alert alert-success'>Compressed! " & _
                  "<a href='?download=" & _
                  Server.URLEncode(uploadedFile.FileName & ".zip") & _
                  "'>Download ZIP</a></div>"
        End If
    End If
End If
%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>ASPPY Zip Compressor</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head><body class="bg-light">
<div class="container mt-5" style="max-width:500px;">
  <div class="card shadow-sm">
    <div class="card-header bg-dark text-white"><h4>File to ZIP Compressor</h4></div>
    <div class="card-body">
      <%=msg%>
      <form method="post" action="test_compress.asp" enctype="multipart/form-data">
        <div class="mb-3">
          <label class="form-label">Select a file to compress:</label>
          <input class="form-control" type="file" name="fileToZip" required>
        </div>
        <button class="btn btn-primary w-100">Upload & Compress</button>
      </form>
    </div>
  </div>
</div></body></html>
Verified end-to-end on localhost A real file was POSTed as multipart/form-data. The page reported "Compressed: upload_me.txt.zip"; the archive on disk started with the ZIP magic PK; and requesting ?download=upload_me.txt.zip returned 200 OK with Content-Type: application/x-zip-compressed and Content-Disposition: attachment. Upload → save → zip → download, all native.
One setup note Make sure the target folders exist (data/, pdfs/, images/) and remember that ASPPY.zip needs the physical paths that Server.MapPath provides.
Coming up in Chapter 15 The ultimate vibe: describing what you want in plain language and letting an AI agent build correct ASPPY apps — guided by the project's own rulebook.

Chapter 15 · Vibe-Coding ASPPY with AI Agents

Part IV · Building Real Apps

"Describe the vibe. Let the agent type the syntax. Review the result."

15.1 Why ASPPY Is an AI-Agent Sweet Spot

Everything this book celebrates — intent over syntax, a small well-defined object model, a single ASPPY superpower object — also makes ASPPY unusually friendly to AI coding agents (OpenCode, Claude Code, Codex, Cursor, GitHub Copilot). VBScript is simple and regular; the surface area is small; and the project ships an explicit rulebook the agent can follow. The result is that "vibe coding" stops being a metaphor and becomes a literal workflow: you state intent in English, the agent produces ASPPY-correct VBScript, and you verify it in the browser.

15.2 Feed the Agent the Rulebook First

The single highest-leverage move is to point your agent at the project's developers.md before any work begins. The project reports this reduces development time and cost by roughly 30–40% and materially improves code quality — even with free agents. There is also an online prompt builder that generates clean, ASPPY-aware prompts to paste in.

A good opening prompt "Read developers.md. Then build an ASPPY MVC app in www starting from www_starter. It's a contacts manager: list, create, edit, delete contacts stored in a SQLite database under data/. Use Bootstrap 5. Route everything through default.asp as the front controller, with controllers, models, and views under asp/. Run on port 5000."

15.3 The Rules an Agent Must Respect

These are the load-bearing conventions from developers.md. Tell your agent to honour them:

15.4 The Human's Job in the Loop

Vibe coding is not abdication. The agent absorbs syntax; you own intent, architecture, and verification. Read the browser's error messages fully, fix the first real error before moving on, and — in the spirit of this entire book — test the result. Every sample in these pages earned its place by being run, not by sounding plausible. Hold your agent's output to the same bar.

The vibe-coding loop, distilled 1. State intent in plain language.   2. Let the agent (briefed on developers.md) produce ASPPY VBScript.   3. Run it on localhost.   4. Read errors, refine intent, repeat.   You stay on the bridge; the agent and ASPPY handle the legacy-to-modern syntax gap.
Coming up in the Appendix A consolidated, tested compatibility reference — the built-ins that work, the gotchas that bite, and the two documentation discrepancies this book caught by actually running the code.

Appendix A · Verified Compatibility Reference & Gotchas

Reference

A.1 The ASPPY Global Object — Quick Reference

NamespaceKey callsBacked by
ASPPY.json.Encode(v,[pretty]), .Decode(s)Python json
ASPPY.crypto.Hash(pw,[rounds]), .Verify(pw,hash)bcrypt
ASPPY.zip.Zip(path,[out]), .Unzip(zip,dest,[overwrite])zipfile
ASPPY.pdf.New(...)add_page/set_font/cell/multi_cell/write_html/outputfpdf2
ASPPY.image.Image, .ImageDraw, .ImageFilter, .ImageEnhancePillow
ASPPY.pop3 / ASPPY.imapConnect/Login/... → message objectspoplib / imaplib

A.2 Verified Discrepancies (docs vs. runtime)

These were caught by running the samples. The book follows the runtime; here is what to watch for:

TopicWhat docs implyWhat actually happensDo this
Mail object creation Server.CreateObject("ASPPY.POP3"/"ASPPY.IMAP") works Returns HTTP 500 (ProgID case mismatch) Use Set x = ASPPY.pop3 / Set x = ASPPY.imap
SQLite database file "Auto-created if it doesn't exist" Missing .db raises 800a0035 — File not found Create/ship the .db first, then Open it
JSON pretty output (unspecified ordering) Keys are sorted alphabetically, 2-space indent Don't rely on insertion order in pretty mode
Session.SessionID (classic IIS shape assumed) ASPPY's own short id (~10 chars) Don't parse/assume the legacy SessionID format

A.3 The Top VBScript Gotchas

A.4 Selected VBScript Built-ins (confirmed present)

ASPPY's built-in coverage is near-complete. Categories include:

Spot-checked on localhost UCase("hello")HELLO; Split("x,y,z", ",")(0)x; Replace("foofoo","foo","bar")barbar; FormatNumber(1234.5, 2)1,234.50; Now() returned a valid timestamp.

A.5 The 50-Page Takeaway

ASPPY lets you keep the business logic you trust, run it anywhere Python runs, outlive the deprecation of VBScript, and reach the modern ecosystem — JSON, PDFs, images, archives, bcrypt, mail — through one global object, all without leaving VBScript. That is the bridge between reliable legacy logic and modern ecosystem power. Stay in the vibe; let ASPPY handle the gap.