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.
"You should never have to rewrite working software just because the platform underneath it got bored."
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.
.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.
"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 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.
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:
.asp files. Zero code changes.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.
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.
.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.
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)
%>
{
"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.
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.
"A transpiler you can explain on a napkin is a transpiler you can trust in production."
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
<% ... %> script regions, and turning VBScript text into a stream of keywords,
identifiers, operators, and literals.If blocks, loops, Sub/Function
definitions, classes, and expressions.
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.
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:
| Name | What it is |
|---|---|
Request | Incoming request: query string, form, cookies, server variables, uploaded files. |
Response | Outgoing response: write, redirect, headers, cookies, binary, native file streaming. |
Server | Utilities: CreateObject, MapPath, HTMLEncode, URLEncode. |
Session | Per-user state across requests, keyed by a session cookie. |
Application | Process-wide shared state across all users. |
ASPPY | The 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.
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:
Scripting.Dictionary,
Scripting.FileSystemObject, VBScript.RegExp, Global.asa events,
#include (file and virtual).ADODB family, MSXML HTTP & DOM,
CDO.Message over SMTP, POP3/IMAP.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.
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.
' 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.
www folder layout ASPPY expects.
"The shortest path from legacy to live is a single command."
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:
| Library | Powers | Install |
|---|---|---|
fpdf2 | ASPPY.pdf | pip install fpdf2 |
bcrypt | ASPPY.crypto | pip install bcrypt |
pillow | ASPPY.image | pip install pillow |
pyodbc | Access / Excel / ODBC databases | pip install pyodbc |
JSON and ZIP need nothing extra — they ride on Python's standard library. SQLite is built in.
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:
python -m ASPPY.server 127.0.0.1 5000 www
.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.
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
More broken ASPPY apps come from path confusion than from anything else. There are exactly three path types, and they answer three different questions:
| Type | Answers | Example |
|---|---|---|
| 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 |
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.
<%
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>"
%>
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.
"To the outside world, nothing changed. To your future self, everything did."
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
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.
<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:
httpErrors existingResponse="PassThrough" — stops IIS from replacing ASPPY's own
error responses (404, 500) with its branded pages. ASPPY's errors reach the browser unmodified.match url=".*" — matches every incoming request.stopProcessing="true" — once matched, no further rules run.Rewrite url="http://localhost:8080/{R:0}" — forwards to ASPPY, appending the full
original path via {R:0}.<rewrite> section).
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.
location ~ \.asp(\?.*)?$ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
| Platform | Web server | Runs ASPPY? |
|---|---|---|
| Windows | IIS, Apache, nginx | ✅ |
| Linux | nginx, Apache | ✅ |
| macOS | nginx, Apache | ✅ |
| Android | KSWEB, 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.
"The objects you already know, plus the two methods you always wished you had."
Everything you reach for is present and behaves as expected:
| Member | Purpose |
|---|---|
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.ContentType | Set the content type, e.g. "application/json". |
Response.Status | Set the status line, e.g. "404 Not Found". |
Response.Cookies(name) = value | Set 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. |
| Member | Purpose |
|---|---|
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. |
Request("name") with ?name=Pieter returned
QS_NAME=Pieter; Server.HTMLEncode("<b>x & y</b>") returned the
correctly escaped <b>x & y</b>; and
Request.ServerVariables("REQUEST_METHOD") correctly reported GET.
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.
<%
' 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
%>
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.)
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:
<%
' Second argument: True = inline (view in browser), False = attachment (download)
Response.File Server.MapPath("data/report.zip"), False
%>
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.
"State is just memory with good manners. ASPPY keeps the manners."
| Method | Purpose |
|---|---|
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. |
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.
ASPPY intercepts Server.CreateObject and routes it to native Python implementations:
Scripting.Dictionary — case-insensitive key/value store.Scripting.FileSystemObject — native file/folder/textstream IO.ADODB.Connection / Recordset / Command / Stream — database & stream access.VBScript.RegExp — regular expressions, powered by Python's re.MSXML2.ServerXMLHTTP / DOMDocument — HTTP & XML (security-sandboxed).CDO.Message — sending email over SMTP.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.
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:
<%
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)."
%>
SESSION_HITS=1 then
SESSION_HITS=2. The session cookie (ASP_PY_SESSIONID) carried state across
requests exactly as Classic ASP sessions do.
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%".
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("global_count") = Application("global_count") + 1
Response.Write "This app has served " & Application("global_count") & " requests."
%>
ASPPY supports Global.asa events, so your application- and session-startup hooks run as
they always did:
<script language="VBScript" runat="server">
Sub Application_OnStart
Application("StartedAt") = Now()
End Sub
Sub Session_OnStart
Session("LoggedIn") = False
End Sub
</script>
"Your SQL still runs. Just point it at a file instead of a server."
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.
| Backend | Notes |
|---|---|
| SQLite | Default; built in, no extra driver. |
| Microsoft Access | Requires pyodbc. |
| SQL Server | Requires pyodbc. |
| PostgreSQL | Requires psycopg2. |
| Excel | Read-only, via pyodbc. |
<%
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
%>
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.
.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.
| Member | Purpose |
|---|---|
Open(sql, conn) | Run a query and open the recordset. |
EOF / BOF | Cursor at end / beginning. |
MoveNext / MoveFirst | Navigate rows. |
Fields(name).Value | Read the current row's column. |
AddNew() / Update() | Insert a row / commit changes. |
Delete() | Delete the current row. |
<%
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
%>
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:
<%
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
%>
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.
ASPPY global object, and the six modern superpowers
it hands to VBScript.
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.
ASPPY.json · ASPPY.crypto · ASPPY.zip ·
ASPPY.pdf · ASPPY.image · ASPPY.pop3 /
ASPPY.imap
"The modern web speaks JSON. Now your VBScript is fluent."
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.
| Method | Behaviour |
|---|---|
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. |
<%
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>"
%>
{
"active": true,
"features": [
"JSON",
"Crypto",
"PDF",
"Image",
"Zip"
],
"name": "ASPPY Framework",
"version": 1.0
}
<%
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, ", ")
%>
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).
<!-- 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")
%>
"If your login page still hashes with MD5, this is the most important chapter in the book."
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.
| Method | Behaviour |
|---|---|
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). |
<%
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."
%>
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.
<%
' 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
%>
ASPPY.crypto.Hash and replace the stored value. Over a few weeks of normal logins your
table upgrades itself.
"Bundle a folder of reports into a single download. Two lines. No ActiveX."
| Method | Behaviour |
|---|---|
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. |
Server.MapPath(...). Passing a relative or virtual path raises a runtime error such as
"Zip: path must be a physical path".
<%
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
%>
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.
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.
"Invoices, receipts, reports — rendered to PDF straight from VBScript."
Start with the namespace factory, then build the document with chained calls:
| Member | Behaviour |
|---|---|
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_color | Fill / 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. |
<%
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
%>
%PDF, file
size ~1.7 KB — using fpdf2 2.8.6. cell, multi_cell,
write_html, colour and font calls all executed cleanly.
Combine Chapter 11 with Chapter 5's Response.File to generate-and-deliver in one request:
<%
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
%>
"The job Persits.Jpeg used to do — now free, modern, and cross-platform."
ASPPY.image mirrors Pillow's structure with four sub-namespaces:
ASPPY.image.Image — open(path), new(mode, Array(w,h), [color]); instances expose save, resize, crop, rotate, convert, filter, and width/height/size.ASPPY.image.ImageDraw — Draw(img) returns a drawer with text, rectangle, ellipse, line.ASPPY.image.ImageFilter — GaussianBlur(radius) plus constants like BLUR, SHARPEN, EMBOSS, FIND_EDGES.ASPPY.image.ImageEnhance — Brightness(img), Contrast(img).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.
<%
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
%>
output.jpg (~2.4 KB) and reported 200x200 using Pillow
12.1.1. Drawing, the Gaussian blur filter, and saving all succeeded.
<%
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
%>
Array(100,100,700,500)
produced a 600x400 result saved to disk — exactly the expected geometry.
<%
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")
%>
"Read a mailbox from VBScript without a single third-party COM component."
ASPPY ships built-in POP3 and IMAP clients backed by Python's poplib and
imaplib. Access them through the global object:
<%
Dim pop3, imap
Set pop3 = ASPPY.pop3 ' POP3 client object
Set imap = ASPPY.imap ' IMAP client object
%>
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).
<%
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()
%>
<%
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()
%>
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.
Both GetMessage() calls return a message exposing:
From, To, Cc, Subject, Date, MessageIDBody — extracted plaintext (or HTML) bodyAttachmentCount, AttachmentNamesTextAttachmentName(i), AttachmentSize(i), AttachmentContentType(i), AttachmentBytes(i)Header("Header-Name") — fetch any raw headerUID / Seq — IMAP only"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.
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>
ASPPY.crypto.Hash/Verify calls were tested directly on
localhost (Chapter 9): hashing returned a $2b$... value and verification correctly
matched/rejected.
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 -->
New → add_page → fonts/colours →
cell/multi_cell → output) was tested on localhost and wrote a
valid %PDF file. The timestamped filename trick keeps each generated PDF unique.
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.
<%@ 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>
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.
data/, pdfs/, images/) and
remember that ASPPY.zip needs the physical paths that Server.MapPath
provides.
"Describe the vibe. Let the agent type the syntax. Review the result."
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.
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.
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."
These are the load-bearing conventions from developers.md. Tell your agent to honour them:
www; start new apps from www_starter; never modify ASPPY's own *.py files.localStorage for the same data.default.asp only dispatches; controllers coordinate; models read/write data; views render. No page-specific SQL or giant Response.Write blocks in default.asp.RenderFooter() twice).Response.Status = "404 Not Found".IsNumeric() + CInt(); HTML-encode output with Server.HTMLEncode(); redirect before writing output.And (Chapter 2 / Appendix) when parsing route arrays..asp, restart the server to clear the compile cache.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.
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.
ASPPY Global Object — Quick Reference| Namespace | Key calls | Backed 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/output | fpdf2 |
ASPPY.image | .Image, .ImageDraw, .ImageFilter, .ImageEnhance | Pillow |
ASPPY.pop3 / ASPPY.imap | Connect/Login/... → message objects | poplib / imaplib |
These were caught by running the samples. The book follows the runtime; here is what to watch for:
| Topic | What docs imply | What actually happens | Do 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 |
And/Or do not short-circuit. Guard array indices with nested If blocks, never inline And with a length check (Chapter 2).ASPPY.zip, ASPPY.pdf.output, and ASPPY.image.save all want physical paths — wrap in Server.MapPath. Never use a leading slash in MapPath.MapPath is relative to the executing file. The same string resolves differently in default.asp vs. asp/models/x.asp.Response.Redirect.IIf(...) for nullable SQL values or request logic (it evaluates both branches).ASPPY's built-in coverage is near-complete. Categories include:
Len, UCase/LCase, Trim/LTrim/RTrim, Left/Right/Mid, InStr/InStrRev, Replace, Split/Join, StrReverse, Space, String, Asc/Chr, Hex/Oct.Array, IsArray, LBound, UBound, Filter.CBool, CInt, CLng, CDbl, CSng, CCur, CStr, CDate.Now, Date, Time, Year/Month/Day, DateAdd, DateDiff, DatePart, FormatDateTime, Weekday, MonthName.Abs, Int/Fix, Round, Sqr, Rnd, Sgn, Exp, Log, trig.FormatNumber, FormatCurrency, FormatPercent (locale-sensitive; verify against IIS for critical paths).IsEmpty, IsNull, IsNumeric, IsDate, IsObject, TypeName, VarType.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.
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.