Kyle Conroy's Blog https://conroy.org Kyle Conroy [email protected] Making open source easy with Codespaces https://conroy.org/making-open-source-easy-with-codespaces 2022-02-12T00:00:00Z <p>As of today, if you have a <a href="https://github.com">GitHub</a> account (with access to <a href="https://github.com/features/codespaces">Codespaces</a>), you can start contributing to <a href="https://sqlc.dev">sqlc</a> in three minutes.</p> <ol> <li>Go to <a href="https://github.com/kyleconroy/sqlc">kyleconroy/sqlc</a></li> <li>Click the &quot;Code&quot; dropdown and click &quot;New Codespace&quot;</li> <li>Wait ~75 seconds</li> <li>Run <code>make test</code></li> <li>Wait ~100 seconds</li> <li>Repeat</li> </ol> <p>Are Codespaces perfect? Absolutely not. My personal laptop is about 4x faster than my Codespace VM. Iterating on the codespace configuration is slow, requiring a new codespace on every change. Codespaces, while built on open source, is not open source itself. If this was the only way to develop, you're tied completely to GitHub.</p> <p>But good enough is amazing. A first-time contributor doesn't need to know anything about sqlc's development dependenceis. While there are only two (Go and Docker), a codespace will be ready in time before Docker Desktop finishes downloading. No need to worry about OS, system architecture, or conflicts with existing software.</p> 0001-01-01T00:00:00Z Introducing sqlc - Compile SQL queries to type-safe Go https://conroy.org/introducing-sqlc 2019-12-11T00:00:00Z <p>Ask any Go developer and they will tell you how painful it is to write programs that talk to SQL databases. Go feels like it isn't even half as productive compared to working with toolkits like <a href="https://www.sqlalchemy.org/">SQLAlchemy</a>, <a href="https://diesel.rs/">Diesel</a>, <a href="https://hibernate.org/">Hibernate</a> or <a href="https://github.com/rails/rails/tree/master/activerecord">ActiveRecord</a>. The existing tools in the Go ecosystem force application developers to hand-write mapping functions or litter their code with unsafe empty interfaces.</p> <h3>Introducing sqlc</h3> <p>I've been feeling this pain for years, so six months ago I started developing a solution. Today, I'm excited to announce the result of that work. <a href="https://github.com/kyleconroy/sqlc">sqlc</a> is a new tool that makes working with SQL in Go a joy.</p> <p>It dramatically improves the developer experience of working with relational databases without sacrificing type-safety or runtime performance. It does not use struct tags, hand-written mapper functions, unnecessary reflection or add any new dependencies to your code. In fact, it even provides correctness and safety guarantees that <em>no other SQL toolkit</em> in the Go ecosystem can match.</p> <p>sqlc accomplishes all of this by taking a fundamentally different approach: compiling SQL into fully type-safe, idiomatic Go code. sqlc can take this SQL:</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">CREATE</span> <span style="color:#069;font-weight:bold">TABLE</span> authors ( id BIGSERIAL <span style="color:#069;font-weight:bold">PRIMARY</span> <span style="color:#069;font-weight:bold">KEY</span>, name <span style="color:#366">text</span> <span style="color:#069;font-weight:bold">NOT</span> <span style="color:#069;font-weight:bold">NULL</span>, bio <span style="color:#366">text</span> ); <span style="color:#09f;font-style:italic">-- name: GetAuthor :one </span><span style="color:#09f;font-style:italic"></span><span style="color:#069;font-weight:bold">SELECT</span> <span style="color:#555">*</span> <span style="color:#069;font-weight:bold">FROM</span> authors <span style="color:#069;font-weight:bold">WHERE</span> id <span style="color:#555">=</span> <span style="color:#a00;background-color:#faa">$</span><span style="color:#f60">1</span> <span style="color:#069;font-weight:bold">LIMIT</span> <span style="color:#f60">1</span>; </pre> <p>and generate the following Go code automatically.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">package</span> db <span style="color:#069;font-weight:bold">import</span> ( <span style="color:#c30">&#34;context&#34;</span> <span style="color:#c30">&#34;database/sql&#34;</span> ) <span style="color:#069;font-weight:bold">type</span> Author <span style="color:#069;font-weight:bold">struct</span> { ID <span style="color:#078;font-weight:bold">int64</span> Name <span style="color:#078;font-weight:bold">string</span> Bio sql.NullString } <span style="color:#069;font-weight:bold">const</span> getAuthor = <span style="color:#c30">`-- name: GetAuthor :one </span><span style="color:#c30">SELECT id, name, bio FROM authors </span><span style="color:#c30">WHERE id = $1 LIMIT 1 </span><span style="color:#c30">`</span> <span style="color:#069;font-weight:bold">type</span> Queries <span style="color:#069;font-weight:bold">struct</span> { db <span style="color:#555">*</span>sql.DB } <span style="color:#069;font-weight:bold">func</span> (q <span style="color:#555">*</span>Queries) GetAuthor(ctx context.Context, id <span style="color:#078;font-weight:bold">int64</span>) (Author, <span style="color:#078;font-weight:bold">error</span>) { row <span style="color:#555">:=</span> q.db.QueryRowContext(ctx, getAuthor, id) <span style="color:#069;font-weight:bold">var</span> i Author err <span style="color:#555">:=</span> row.Scan(<span style="color:#555">&amp;</span>i.ID, <span style="color:#555">&amp;</span>i.Name, <span style="color:#555">&amp;</span>i.Bio) <span style="color:#069;font-weight:bold">return</span> i, err } </pre> <p>For years, software engineers have been generating SQL queries from annotated objects in programming languages. <strong>SQL is already a structured, typed language; we should be generating correct, type-safe code in every programming language from the source of truth: SQL itself.</strong></p> <h3>Problems using SQL from Go, today</h3> <p>Working with relational databases in any programming language is <a href="https://en.wikipedia.org/wiki/Object-relational_impedance_mismatch">challenging</a>. Go in particular is especially painful; even with the existing packages and tools, writing and maintaining queries in a Go application is a chore.</p> <h4>Low-level standard library</h4> <p>Go’s standard library offers the <a href="https://golang.org/pkg/database/sql/"><code>database/sql</code></a> package for interacting with relational databases, but most applications outgrow the <code>database/sql</code> package and reach for a full-fledged ORM or a higher-level library abstraction. Why?</p> <p>Using the <code>database/sql</code> package is straightforward. Write a query, pass in the necessary arguments, and scan the results back into fields. Programmers are responsible for explicitly specifying the mapping between a SQL field and its value in the program for both inputs and outputs.</p> <p>Once an application has more than a few queries, maintaining these mappings is cumbersome and severely impacts programmer productivity. Worse, it's trivial to make mistakes that are not caught until runtime. If you switch the order of parameters in your query, the parameter mapping must be updated. If a column is added to a table, all queries must be updated to return that value. If the type in SQL does not match the type in your code, failures will not occur until query execution time.</p> <h4>Higher-level libraries</h4> <p>The Go community has produced higher-level libraries (<a href="https://github.com/jmoiron/sqlx">github.com/jmoiron/sqlx</a>) and ORMs (<a href="https://gorm.io/">github.com/jinzhu/gorm</a>) to solve these issues. However, higher-level libraries still require manual mapping via query text and struct tags that, if incorrect, will only fail at runtime.</p> <p>ORMs do away with much of the manual mapping but require you to write your queries now in a pseudo-sql DSL that basically reinvents SQL in a set of Go function calls.</p> <h4>Invalid SQL</h4> <p>With either approach, it is still trivial to make errors that the compiler can't check. As a Go programmer, have you ever:</p> <ul> <li>Mixed up the order of the arguments when invoking the query so they didn't match up with the SQL text</li> <li>Updated the name of a column in one query both not another</li> <li>Mistyped the name of a column in a query</li> <li>Changed the number of arguments in a query but forgot to pass the additional values</li> <li>Changed the type of a column but forgot to change the type in your code?</li> </ul> <p>All of these errors are <em>impossible</em> with sqlc. Wait, what? How?</p> <h3>How to use sqlc in 3 steps</h3> <ol> <li>You write SQL queries</li> <li>You run <code>sqlc</code> to generate Go code that presents type-safe interfaces to those queries</li> <li>You write application code that calls the methods <code>sqlc</code> generates</li> </ol> <p>Seriously, it's that easy. You don't have to write any boilerplate SQL querying code ever again. sqlc generates fully-type-safe idiomatic Go code from your queries. sqlc also prevents entire classes of common errors in SQL code.</p> <p>During code generation, sqlc parses all of your queries and DDL statements (e.g. <code>CREATE TABLE</code>) so that it knows the names and types of every column in your tables and every expression in your queries. If any of them do not match, sqlc <em>will fail to compile your queries</em>, catching would-be runtime errors before they happen.</p> <p>Likewise, the methods that sqlc generates for you have a strict arity and correct Go type definitions that match your columns. So if you change a query's arguments or a column's type but don't update your code, it will fail to compile.</p> <h3>A guided tour of sqlc</h3> <p>That all sounds great, but what does it look like in practice?</p> <h4>Download and install</h4> <p>To start, <a href="https://github.com/kyleconroy/sqlc#downloads">download the latest version of sqlc</a> and add it to your <code>$PATH</code>. You’ll also want to have Go 1.13 installed on your system. Create a new directory for the example project.</p> <pre style="background-color:#f0f3f3"> $ mkdir sqlc-tour $ <span style="color:#366">cd</span> sqlc-tour $ go mod init github.com/kyleconroy/sqlc-tour </pre> <h4>Configuration file</h4> <p>sqlc uses a <a href="https://github.com/kyleconroy/sqlc#settings">configuration file</a> at the root of your project to store settings. Create <code>sqlc.json</code> with the following contents:</p> <pre style="background-color:#f0f3f3"> { <span style="color:#309;font-weight:bold">&#34;version&#34;</span>: <span style="color:#c30">&#34;1&#34;</span>, <span style="color:#309;font-weight:bold">&#34;packages&#34;</span>: [{ <span style="color:#309;font-weight:bold">&#34;schema&#34;</span>: <span style="color:#c30">&#34;schema.sql&#34;</span>, <span style="color:#309;font-weight:bold">&#34;queries&#34;</span>: <span style="color:#c30">&#34;query.sql&#34;</span>, <span style="color:#309;font-weight:bold">&#34;name&#34;</span>: <span style="color:#c30">&#34;main&#34;</span>, <span style="color:#309;font-weight:bold">&#34;path&#34;</span>: <span style="color:#c30">&#34;.&#34;</span> }] } </pre> <p>sqlc will generate a Go package for each entry in the packages list. Each entry has four required properties:</p> <ul> <li><code>schema</code> <ul> <li>Path to a SQL file that defines database tables (can also be a directory of SQL files)</li> </ul></li> <li><code>queries</code> <ul> <li>Path to a SQL file with application queries (can also be a directory of SQL files)</li> </ul></li> <li><code>name</code> <ul> <li>The package name to use for the generated code. Defaults to <code>path</code> basename</li> </ul></li> <li><code>path</code> <ul> <li>Output directory for generated code</li> </ul></li> </ul> <h4>Write DDL</h4> <p>Let’s build an application for tracking authors. It’s a small application with only a single table. Define the <code>authors</code> table in <code>schema.sql</code>.</p> <pre style="background-color:#f0f3f3"> <span style="color:#09f;font-style:italic">-- schema.sql </span><span style="color:#09f;font-style:italic"></span> <span style="color:#069;font-weight:bold">CREATE</span> <span style="color:#069;font-weight:bold">TABLE</span> authors ( id BIGSERIAL <span style="color:#069;font-weight:bold">PRIMARY</span> <span style="color:#069;font-weight:bold">KEY</span>, name <span style="color:#366">text</span> <span style="color:#069;font-weight:bold">NOT</span> <span style="color:#069;font-weight:bold">NULL</span>, bio <span style="color:#366">text</span> ); </pre> <h4>Write queries</h4> <p>Your application needs a few queries to create, insert, update and delete <code>author</code> records. Queries are annotated with a small comment that includes a Go method name and the <code>database/sql</code> function to use.</p> <pre style="background-color:#f0f3f3"> <span style="color:#09f;font-style:italic">-- query.sql </span><span style="color:#09f;font-style:italic"></span> <span style="color:#09f;font-style:italic">-- name: GetAuthor :one </span><span style="color:#09f;font-style:italic"></span><span style="color:#069;font-weight:bold">SELECT</span> <span style="color:#555">*</span> <span style="color:#069;font-weight:bold">FROM</span> authors <span style="color:#069;font-weight:bold">WHERE</span> id <span style="color:#555">=</span> <span style="color:#a00;background-color:#faa">$</span><span style="color:#f60">1</span> <span style="color:#069;font-weight:bold">LIMIT</span> <span style="color:#f60">1</span>; <span style="color:#09f;font-style:italic">-- name: ListAuthors :many </span><span style="color:#09f;font-style:italic"></span><span style="color:#069;font-weight:bold">SELECT</span> <span style="color:#555">*</span> <span style="color:#069;font-weight:bold">FROM</span> authors <span style="color:#069;font-weight:bold">ORDER</span> <span style="color:#069;font-weight:bold">BY</span> name; <span style="color:#09f;font-style:italic">-- name: CreateAuthor :one </span><span style="color:#09f;font-style:italic"></span><span style="color:#069;font-weight:bold">INSERT</span> <span style="color:#069;font-weight:bold">INTO</span> authors ( name, bio ) <span style="color:#069;font-weight:bold">VALUES</span> ( <span style="color:#a00;background-color:#faa">$</span><span style="color:#f60">1</span>, <span style="color:#a00;background-color:#faa">$</span><span style="color:#f60">2</span> ) RETURNING <span style="color:#555">*</span>; <span style="color:#09f;font-style:italic">-- name: DeleteAuthor :exec </span><span style="color:#09f;font-style:italic"></span><span style="color:#069;font-weight:bold">DELETE</span> <span style="color:#069;font-weight:bold">FROM</span> authors <span style="color:#069;font-weight:bold">WHERE</span> id <span style="color:#555">=</span> <span style="color:#a00;background-color:#faa">$</span><span style="color:#f60">1</span>; </pre> <h4>Generate code</h4> <p>With just the information stored in these two SQL files, sqlc can now generate database access methods for your application. Make sure that you’re in the <code>sqlc-tour</code> directory and run the following command:</p> <pre style="background-color:#f0f3f3"> $ sqlc generate </pre> <p>This will generate three files: <code>db.go</code>, <code>models.go</code>, and <code>query.sql.go</code>.</p> <p><code>db.go</code> defines a shared interface for using a <code>*sql.DB</code> or <code>*sql.Tx</code> to execute queries, as well as the <code>Queries</code> struct which contains the database access methods.</p> <h4><code>db.go</code></h4> <pre style="background-color:#f0f3f3"> <span style="color:#09f;font-style:italic">// db.go </span><span style="color:#09f;font-style:italic"></span><span style="color:#069;font-weight:bold">package</span> main <span style="color:#069;font-weight:bold">import</span> ( <span style="color:#c30">&#34;context&#34;</span> <span style="color:#c30">&#34;database/sql&#34;</span> ) <span style="color:#069;font-weight:bold">type</span> dbtx <span style="color:#069;font-weight:bold">interface</span> { ExecContext(context.Context, <span style="color:#078;font-weight:bold">string</span>, <span style="color:#555">...</span><span style="color:#069;font-weight:bold">interface</span>{}) (sql.Result, <span style="color:#078;font-weight:bold">error</span>) PrepareContext(context.Context, <span style="color:#078;font-weight:bold">string</span>) (<span style="color:#555">*</span>sql.Stmt, <span style="color:#078;font-weight:bold">error</span>) QueryContext(context.Context, <span style="color:#078;font-weight:bold">string</span>, <span style="color:#555">...</span><span style="color:#069;font-weight:bold">interface</span>{}) (<span style="color:#555">*</span>sql.Rows, <span style="color:#078;font-weight:bold">error</span>) QueryRowContext(context.Context, <span style="color:#078;font-weight:bold">string</span>, <span style="color:#555">...</span><span style="color:#069;font-weight:bold">interface</span>{}) <span style="color:#555">*</span>sql.Row } <span style="color:#069;font-weight:bold">func</span> New(db dbtx) <span style="color:#555">*</span>Queries { <span style="color:#069;font-weight:bold">return</span> <span style="color:#555">&amp;</span>Queries{db: db} } <span style="color:#069;font-weight:bold">type</span> Queries <span style="color:#069;font-weight:bold">struct</span> { db dbtx } <span style="color:#069;font-weight:bold">func</span> (q <span style="color:#555">*</span>Queries) WithTx(tx <span style="color:#555">*</span>sql.Tx) <span style="color:#555">*</span>Queries { <span style="color:#069;font-weight:bold">return</span> <span style="color:#555">&amp;</span>Queries{ db: tx, } } </pre> <h4><code>models.go</code></h4> <p><code>models.go</code> contains the structs associated with the database tables. In this example it contains a single struct, <code>Author</code>.</p> <pre style="background-color:#f0f3f3"> <span style="color:#09f;font-style:italic">// models.go </span><span style="color:#09f;font-style:italic"></span><span style="color:#069;font-weight:bold">package</span> main <span style="color:#069;font-weight:bold">import</span> <span style="color:#c30">&#34;database/sql&#34;</span> <span style="color:#069;font-weight:bold">type</span> Author <span style="color:#069;font-weight:bold">struct</span> { ID <span style="color:#078;font-weight:bold">int64</span> Name <span style="color:#078;font-weight:bold">string</span> Bio sql.NullString } </pre> <h4><code>query.sql.go</code></h4> <p><code>query.sql.go</code> contains the data access methods we defined in <code>query.sql</code>. It’s a bit verbose, but this is code you would have had to write yourself!</p> <pre style="background-color:#f0f3f3"> <span style="color:#09f;font-style:italic">// query.sql.go </span><span style="color:#09f;font-style:italic"></span><span style="color:#069;font-weight:bold">package</span> main <span style="color:#069;font-weight:bold">import</span> ( <span style="color:#c30">&#34;context&#34;</span> <span style="color:#c30">&#34;database/sql&#34;</span> ) <span style="color:#069;font-weight:bold">const</span> createAuthor = <span style="color:#c30">`-- name: CreateAuthor :one </span><span style="color:#c30">INSERT INTO authors ( </span><span style="color:#c30"> name, bio </span><span style="color:#c30">) VALUES ( </span><span style="color:#c30"> $1, $2 </span><span style="color:#c30">) </span><span style="color:#c30">RETURNING id, name, bio </span><span style="color:#c30">`</span> <span style="color:#069;font-weight:bold">type</span> CreateAuthorParams <span style="color:#069;font-weight:bold">struct</span> { Name <span style="color:#078;font-weight:bold">string</span> Bio sql.NullString } <span style="color:#069;font-weight:bold">func</span> (q <span style="color:#555">*</span>Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, <span style="color:#078;font-weight:bold">error</span>) { row <span style="color:#555">:=</span> q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio) <span style="color:#069;font-weight:bold">var</span> i Author err <span style="color:#555">:=</span> row.Scan(<span style="color:#555">&amp;</span>i.ID, <span style="color:#555">&amp;</span>i.Name, <span style="color:#555">&amp;</span>i.Bio) <span style="color:#069;font-weight:bold">return</span> i, err } <span style="color:#069;font-weight:bold">const</span> deleteAuthor = <span style="color:#c30">`-- name: DeleteAuthor :exec </span><span style="color:#c30">DELETE FROM authors </span><span style="color:#c30">WHERE id = $1 </span><span style="color:#c30">`</span> <span style="color:#069;font-weight:bold">func</span> (q <span style="color:#555">*</span>Queries) DeleteAuthor(ctx context.Context, id <span style="color:#078;font-weight:bold">int64</span>) <span style="color:#078;font-weight:bold">error</span> { _, err <span style="color:#555">:=</span> q.db.ExecContext(ctx, deleteAuthor, id) <span style="color:#069;font-weight:bold">return</span> err } <span style="color:#069;font-weight:bold">const</span> getAuthor = <span style="color:#c30">`-- name: GetAuthor :one </span><span style="color:#c30">SELECT id, name, bio FROM authors </span><span style="color:#c30">WHERE id = $1 LIMIT 1 </span><span style="color:#c30">`</span> <span style="color:#069;font-weight:bold">func</span> (q <span style="color:#555">*</span>Queries) GetAuthor(ctx context.Context, id <span style="color:#078;font-weight:bold">int64</span>) (Author, <span style="color:#078;font-weight:bold">error</span>) { row <span style="color:#555">:=</span> q.db.QueryRowContext(ctx, getAuthor, id) <span style="color:#069;font-weight:bold">var</span> i Author err <span style="color:#555">:=</span> row.Scan(<span style="color:#555">&amp;</span>i.ID, <span style="color:#555">&amp;</span>i.Name, <span style="color:#555">&amp;</span>i.Bio) <span style="color:#069;font-weight:bold">return</span> i, err } <span style="color:#069;font-weight:bold">const</span> listAuthors = <span style="color:#c30">`-- name: ListAuthors :many </span><span style="color:#c30">SELECT id, name, bio FROM authors </span><span style="color:#c30">ORDER BY name </span><span style="color:#c30">`</span> <span style="color:#069;font-weight:bold">func</span> (q <span style="color:#555">*</span>Queries) ListAuthors(ctx context.Context) ([]Author, <span style="color:#078;font-weight:bold">error</span>) { rows, err <span style="color:#555">:=</span> q.db.QueryContext(ctx, listAuthors) <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> <span style="color:#069;font-weight:bold">nil</span>, err } <span style="color:#069;font-weight:bold">defer</span> rows.Close() <span style="color:#069;font-weight:bold">var</span> items []Author <span style="color:#069;font-weight:bold">for</span> rows.Next() { <span style="color:#069;font-weight:bold">var</span> i Author <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">:=</span> rows.Scan(<span style="color:#555">&amp;</span>i.ID, <span style="color:#555">&amp;</span>i.Name, <span style="color:#555">&amp;</span>i.Bio); err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> <span style="color:#069;font-weight:bold">nil</span>, err } items = <span style="color:#366">append</span>(items, i) } <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">:=</span> rows.Close(); err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> <span style="color:#069;font-weight:bold">nil</span>, err } <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">:=</span> rows.Err(); err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> <span style="color:#069;font-weight:bold">nil</span>, err } <span style="color:#069;font-weight:bold">return</span> items, <span style="color:#069;font-weight:bold">nil</span> } </pre> <h5>Star expansion</h5> <p>Were you surprised to see <code>SELECT *</code> / <code>RETURNING *</code> in <code>query.sql</code>? sqlc replaces <code>*</code> references with the correct columns when generating code. Take a second look at the <code>createAuthor</code>, <code>listAuthor</code> and <code>getAuthor</code> SQL queries in the example above.</p> <h4>Write your application code</h4> <p>It’s now easy to create, delete and fetch <code>author</code> records. Paste the following into <code>main.go</code>. It should build (<code>go build</code>) without any errors.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">package</span> main <span style="color:#069;font-weight:bold">import</span> ( <span style="color:#c30">&#34;context&#34;</span> <span style="color:#c30">&#34;database/sql&#34;</span> <span style="color:#c30">&#34;fmt&#34;</span> ) <span style="color:#069;font-weight:bold">func</span> run(ctx context.Context, db <span style="color:#555">*</span>sql.DB) <span style="color:#078;font-weight:bold">error</span> { q <span style="color:#555">:=</span> <span style="color:#555">&amp;</span>Queries{db: db} insertedAuthor, err <span style="color:#555">:=</span> q.CreateAuthor(ctx, CreateAuthorParams{ Name: <span style="color:#c30">&#34;Brian Kernighan&#34;</span>, Bio: sql.NullString{ String: <span style="color:#c30">&#34;Co-author of The C Programming Language&#34;</span>, Valid: <span style="color:#069;font-weight:bold">true</span>, }, }) <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> err } authors, err <span style="color:#555">:=</span> q.ListAuthors(ctx) <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> err } fmt.Println(authors) err = q.DeleteAuthor(ctx, insertedAuthor.ID) <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> err } <span style="color:#069;font-weight:bold">return</span> <span style="color:#069;font-weight:bold">nil</span> } <span style="color:#069;font-weight:bold">func</span> main() { <span style="color:#09f;font-style:italic">// TODO: Open a connection to your PostgreSQL database </span><span style="color:#09f;font-style:italic"></span> run(context.Background(), <span style="color:#069;font-weight:bold">nil</span>) } </pre> <p>All the above code can be found on in the <a href="https://github.com/kyleconroy/sqlc-tour">sqlc-tour</a> repository. A larger, more complicated example application can be found in the <a href="https://github.com/kyleconroy/sqlc/tree/main/examples/ondeck">ondeck</a> package in the <a href="https://github.com/kyleconroy/sqlc">sqlc</a> repository.</p> <h3>Packed with power</h3> <p>Don’t let the previous example’s simplicity fool you. sqlc has support for complex queries and advanced usage patterns:</p> <ul> <li><a href="https://docs.sqlc.dev/en/stable/howto/transactions.html">Transactions</a> and <a href="https://docs.sqlc.dev/en/stable/howto/prepared_query.html">prepared statements</a></li> <li><a href="https://docs.sqlc.dev/en/stable/howto/query_count.html">Aggregates</a>, case statements, and common table expressions</li> <li><a href="https://docs.sqlc.dev/en/stable/howto/insert.html#returning-columns-from-inserted-rows"><code>RETURNING</code></a> values from INSERT, UPDATE, and DELETE statements</li> <li>PostgreSQL types like <a href="https://docs.sqlc.dev/en/stable/reference/datatypes.html#arrays">arrays</a>, <a href="https://docs.sqlc.dev/en/stable/reference/datatypes.html#enums">enums</a>, <a href="https://docs.sqlc.dev/en/stable/reference/datatypes.html#dates-and-time">timestamps</a>, and <a href="https://docs.sqlc.dev/en/stable/reference/datatypes.html#uuids">UUIDs</a></li> <li>Go <a href="https://docs.sqlc.dev/en/stable/reference/config.html#type-overrides">type overrides</a> for individual columns or PostgreSQL values</li> <li>Generated structs with <a href="https://docs.sqlc.dev/en/stable/reference/config.html">JSON tags</a></li> </ul> <h3>How it works</h3> <p>You might be wondering how this all works. It’s not magic, but it’s close: sqlc parses queries using the <strong>same parser</strong> as your PostgreSQL database.</p> <p>A first pass uses DDL statements to build an in-memory representation of your database. Next, sqlc parses each query and uses the in-memory representation to determine input parameters and output columns.</p> <p>This is only possible thanks to the amazing work by Lukas Fittl on <a href="https://github.com/lfittl/pg_query_go">pg_query_go</a>. If you need help diagnosing PostgreSQL performance issues, his service <a href="https://pganalyze.com/">pganalyze</a> may be exactly what you need.</p> <h3>What’s next</h3> <p>While it’s still early days, sqlc is ready for production. It’s used for all database access in my own projects:</p> <ul> <li><a href="https://equinox.io">https://equinox.io</a> - Go application packaging &amp; distribution</li> <li><a href="https://upcoming.fm">https://upcoming.fm</a> - Spotify playlists for your favorite music venues</li> <li><a href="https://chaincontrol.org">https://chaincontrol.org</a> - SMS notifications for winter road closures in Tahoe</li> </ul> <p>More importantly, it’s seen adoption in larger companies. Both <a href="https://ngrok.com">ngrok</a> and <a href="https://getweave.com">Weave</a> use sqlc to power portions of their stack.</p> <p>sqlc is a young project in active development. It currently only supports PostgreSQL and Go. However, it's designed to support additional language backends in the future. If you'd like sqlc support for your language of choice, create an <a href="https://github.com/kyleconroy/sqlc">issue</a> or send me an email at <code>[email protected]</code>.</p> <p>Lastly, I like to thank the authors of <a href="https://www.hugsql.org/"><code>hugsql</code></a>, <a href="https://pugsql.org/"><code>pugsql</code></a>, and <a href="https://github.com/protocolbuffers/protobuf"><code>protoc</code></a>, which served as inspiration for <code>sqlc</code>. Without these tools, sqlc would not exist.</p> Upcoming: Spotify Playlists for Your Favorite Venues https://conroy.org/upcoming 2019-06-02T00:00:00Z <p>Determined to go to more concerts this year, I wanted a better way to explore local shows. I listen to the majority of my music on Spotify, so I decided to make playlists for each of my favorite venues featuring artists from their upcoming concerts.</p> <p>Each playlists is updated once a week on Monday. The order is determined by concert date. Once an artist has performed, they're removed from the playlist. I cycle through the <a href="https://developer.spotify.com/documentation/web-api/reference/artists/get-artists-top-tracks/">top tracks</a>, which means playlists will repeat every few months.</p> <h2>Venues</h2> <p>You can find a list of all venues, and add your own, at <a href="https://upcoming.fm">https://upcoming.fm</a>.</p> <h2>Implementation</h2> <p>Before writing any code, I manually created a single playlist for the Bill Graham Civic Auditorium. While not much work, I knew this approach wouldn't scale beyond a few venues. Satisfied with the results, I looked into what APIs were available to automate playlist creation.</p> <p>It turns out Spotify has a fantastic <a href="https://developer.spotify.com/documentation/web-api/">API</a> for search and playlist management. One quick sign up later, I had a developer account and a set of credentials. I've been writing the majority of my side projects in Go recently, so I used the excellent <a href="https://github.com/zmb3/spotify">zmb3/spotify</a> package. It didn't take long before I could search for artists by name and create a playlist with a popular song from each artist.</p> <p>However, Spotify does not have an API for upcoming concerts at venues. For that, I needed <a href="https://www.songkick.com/">Songkick</a>. They have an <a href="https://www.songkick.com/developer">API</a>, but sign-up is not self-serve. I applied for a developer account and had my key within in a few days. Using the <a href="https://www.songkick.com/developer/upcoming-events-for-venue">Upcoming events endpoint</a>, I was able to get the names of all upcoming headliners for a given venue. I then plugged that list into my code above and had an automated solution. Spotify.</p> <p>I wasn't happy with the Go clients for the Songkick API, so I wrote <a href="https://github.com/kyleconroy/songkick">one</a> myself. It's deliberately incomplete, as I only needed to access one endpoint.</p> <p>I deployed to code to Heroku and have the script run once a week.</p> PEP 594 Will Break 1 in 30 PyPI Packages https://conroy.org/breaking-python-packages 2019-05-29T00:00:00Z <p><a href="https://www.python.org/dev/peps/pep-0594/">PEP 594</a> outlines the plan to deprecate and remove packages from the Python standard library. If accepted in its current form, PEP 594 will break 3.8% of all Python 3 packages on PyPI.</p> <p>As of May 2019 there are 3,604 Python 3 packages on PyPI (out of 94,680) that import packages deprecated by PEP 594.</p> <pre style="background-color:#f0f3f3"> total packages: 181225 valid packages: 178642 python3 packages: 94680 scanned packages: 92779 broken packages: 3604 </pre> <p>The majority of these packages import the <code>parser</code> or <code>imp</code> package. If those two packages were removed from PEP 594, the number of broken packages drops to 828.</p> <p>Is 3,600 broken packages enough to block adoption of the PEP? I don't know. I just wanted to make sure that the impact the PEP was known before a final decision.</p> <h2>Reproduce the Results</h2> <p>Before you begin, you'll need Python &gt;= 3.6 and a recent version of Go to run the code. All commands should be run inside a cloned checkout of the <a href="https://github.com/kyleconroy/dead-batteries/">dead-batteries</a> repository. First, build the binary.</p> <pre style="background-color:#f0f3f3"> git clone https://github.com/kyleconroy/dead-batteries.git cd dead-batteries go build </pre> <p>Next, you'll download the metadata for every package on PyPI, which will take up about ~500MB of space.</p> <pre style="background-color:#f0f3f3"> ./dead-battery mirror </pre> <ul> <li><code>simple.html</code>: contains a list of all known packages on PyPI<br> </li> <li><code>meta/*.json</code>: contains package metadata for each package</li> <li><code>python3-packages.json</code>: contains a list of packages that support Python3</li> </ul> <p>With a complete mirror, you're now ready to scan packages for imports of deprecated packages. Open up a new shell to install and run the Python service.</p> <pre style="background-color:#f0f3f3"> python3 -m venv venv venv/bin/pip install flask venv/bin/pip install gunicorn venv/bin/pip install gunicorn[gevent] venv/bin/gunicorn -k gevent -w 10 -b 127.0.0.1:4000 imports:app </pre> <p>The service exposes a HTTP/JSON interface to the <code>ast</code> package. It parses a given file and returns any deprecated imports as well as parsing errors.</p> <pre style="background-color:#f0f3f3"> // INPUT { &#34;path&#34;: &#34;/path/to/python/file&#34; } // OUTPUT { &#34;imports&#34;: { &#34;imp&#34;: 2 }, &#34;errors&#34;: { &#34;syntax-error&#34;: 2 } } </pre> <p>With that running, you can now start the scan. On my laptop, the scan took about an hour to complete. The output is continually saved to <code>results.json</code>.</p> <pre style="background-color:#f0f3f3"> ./dead-battery scan </pre> <p>Once the search process is complete, generate the package statistics.</p> <pre style="background-color:#f0f3f3"> ./dead-battery stats </pre> <p>Two new files have been created: <code>import.csv</code> and <code>packages.json</code>. The CSV file contains the total number of imports for each deprecated standard library package. The JSON file contains a list of every package that imports one of these deprecated packages, along with a link to the package on PyPI.</p> <p>A quick jump into the Python interpreter gives us the total number of packages affected by PEP 594.</p> <pre style="background-color:#f0f3f3"> &gt;&gt;&gt; import json &gt;&gt;&gt; len(json.load(open(&#39;packages.json&#39;))) 3604 </pre> <h2>Methodology</h2> <p>Since PEP 594 only affects Python 3, I needed to filter out packages that don't support Python 3. For each package, I first looked for any classifiers with the prefix <code>Programming Language :: Python :: 3</code>. Next, I checked the <code>python_version</code> of the latest release. It's a bit messy, but the code to do so can be found <a href="https://github.com/kyleconroy/dead-batteries/blob/master/mirror.go#L114">here</a>.</p> <pre style="background-color:#f0f3f3"> { <span style="color:#309;font-weight:bold">&#34;info&#34;</span>: { <span style="color:#309;font-weight:bold">&#34;classifiers&#34;</span>: [ <span style="color:#c30">&#34;Programming Language :: Python :: 2.7&#34;</span>, <span style="color:#c30">&#34;Programming Language :: Python :: 3&#34;</span> ], <span style="color:#309;font-weight:bold">&#34;version&#34;</span>: <span style="color:#c30">&#34;2.22.0&#34;</span> }, <span style="color:#309;font-weight:bold">&#34;releases&#34;</span>: { <span style="color:#309;font-weight:bold">&#34;2.22.0&#34;</span>: [ { <span style="color:#309;font-weight:bold">&#34;packagetype&#34;</span>: <span style="color:#c30">&#34;bdist_wheel&#34;</span>, <span style="color:#309;font-weight:bold">&#34;python_version&#34;</span>: <span style="color:#c30">&#34;py2.py3&#34;</span>, <span style="color:#309;font-weight:bold">&#34;url&#34;</span>: <span style="color:#c30">&#34;https://files.pythonhosted.org/.../requests-2.22.0-py2.py3-none-any.whl&#34;</span> } ] } } </pre> <p>If you know a better way to check for Python 3 compatibility, please reach out</p> Downloading Your Twitter Data https://conroy.org/your-twitter-data 2018-07-12T00:00:00Z <p>After the outcry from my <a href="/archiving-twitter">last post</a> reached Jack himself, Twitter launched a new export option that includes all your data in a machine readable format. We did it!</p> <p>Or did we?</p> <p>It turns out Jack didn't read my post. I didn't get a call from the Twitter executive team. Instead, it was <a href="https://github.com/kyleconroy/grain/issues/1">brought to my attention</a> that the account export I've been looking for already exists and has for some time.</p> <p>Now, I’m not sure why Twitter has two ways to download your data, but they do. I was using the Tweet archive, available in <a href="https://twitter.com/settings/account">account settings</a>. The other option, called &quot;Your Twitter Data&quot;, is found on a <a href="https://twitter.com/settings/your_twitter_data">separate settings page</a> behind a password confirmation dialog. Scroll down to the bottom of the page and click the &quot;Download data&quot; button.</p> <p>This archive contains far more information than the Tweet archive. Uncompressed, my Tweet archive is 7MB. My data archive was 45MB. The information contained inside is higher quality and more numerous. Ad impressions, block lists, screen name changes, oh my! It even includes image descriptions. The full list of files is included at the bottom of this post.</p> <h2>&quot;Machine&quot; Readable</h2> <p>That said, the archive is not without its issues. The most glaring problem is that the data isn't machine readable out of the box. For some reason, records are contained in JavaScript files.</p> <pre style="background-color:#f0f3f3"> <span style="color:#366">window</span>.YTD.tweet.part0 <span style="color:#555">=</span> [{ ... }] </pre> <p>Other strange choices abound. Tweets are contained in <code>tweet.js</code>. Here, all numbers are stored as floats, serialized as strings.</p> <pre style="background-color:#f0f3f3"> { <span style="color:#c30">&#34;favorite_count&#34;</span> <span style="color:#555">:</span> <span style="color:#c30">&#34;0.0&#34;</span> } </pre> <p>I wasn't aware you could half-favorite a tweet. This scheme results in corrupted IDs, as large numbers are stored in E-notation, which truncates digits. Notice that the <code>id</code> filed is missing the last digit present in the <code>id_str</code> field.</p> <pre style="background-color:#f0f3f3"> { <span style="color:#c30">&#34;id&#34;</span> <span style="color:#555">:</span> <span style="color:#c30">&#34;1.01456537218219622E18&#34;</span>, <span style="color:#c30">&#34;id_str&#34;</span> <span style="color:#555">:</span> <span style="color:#c30">&#34;1014565372182196224&#34;</span> } </pre> <p>As I update <a href="https://github.com/kyleconroy/grain">Grain</a> to parse the entire archive, I'm sure I'll run into more issues. I sincerely hope that talking about these data quality problems publicly encourages Twitter and other companies to take account exports seriously.</p> <h4>Appendix: Archive Layout</h4> <pre style="background-color:#f0f3f3"> . ├── README.txt ├── account-creation-ip.js ├── account-suspension.js ├── account.js ├── ad-engagements.js ├── ad-impressions.js ├── ad-mobile-conversions-attributed.js ├── ad-mobile-conversions-unattributed.js ├── ad-online-conversions-attributed.js ├── ad-online-conversions-unattributed.js ├── ageinfo.js ├── block.js ├── connected-application.js ├── contact.js ├── direct-message-headers.js ├── direct-message.js ├── direct_message_media/ ├── email-address-change.js ├── facebook-connection.js ├── follower.js ├── following.js ├── ip-audit.js ├── like.js ├── lists-created.js ├── lists-member.js ├── lists-subscribed.js ├── moment.js ├── mute.js ├── ni-devices.js ├── personalization.js ├── profile.js ├── profile_media/ ├── protected-history.js ├── saved-search.js ├── screen-name-change.js ├── tweet.js ├── tweet_media/ └── verified.js </pre> Actually Archiving Your Entire Twitter Account https://conroy.org/archiving-twitter 2018-06-30T00:00:00Z <p>Most social networks give you the option of downloading an archive of all data stored in your account. If you live in the European Union, my understanding is that these services are now required to give you access to this data, thanks in part to GDPR. However, the quality of archives varies from service to service.</p> <h2>Your Twitter Archive</h2> <p>Twitter makes it easy to download an archive of your data. After following <a href="https://help.twitter.com/en/managing-your-account/how-to-download-your-twitter-archive">four steps</a>, a link to your archive will arrive in your inbox. The ZIP file includes an HTML view of your timeline as well as access to your tweets in CSV format.</p> <p>Many services allow you to download a human-browsable archive and a machine-readable archive. This commonly looks like a static HTML site for humans and a set of JSON files for machines. Twitter includes both in its archive, but the included data is laughably incomplete.</p> <p>To me, my Twitter account includes all of the following data:</p> <ul> <li>My tweets, including photos, videos, polls, etc.</li> <li>My favorites</li> <li>My public and private lists</li> <li>The accounts I follow</li> <li>The accounts that follow me</li> <li>My direct messages</li> </ul> <p>Of the above, Twitter's account archive only includes partial Tweet data. That's it! No favorites, no lists, no direct messages. The most surprising thing to me was that Twitter does not give you a copy of any of the photos or videos posted to your account. Instead, that data continues to live on Twitter's servers.</p> <p>When viewing your archive without an internet connection, avatars and images disappear. I guess Twitter hopes you didn't really want to save any of those photos after all.</p> <p><img src="/images/archive-comparison.jpg" alt="Side-by-side archive comparsion with and with internt" /></p> <p>The Twitter archive does include a <code>tweets.csv</code> file with all of your accounts tweets. However, this format losses much of the information returned via the Twitter API. The <a href="https://developer.twitter.com/en/docs/tweets/data-dictionary/overview/tweet-object.html">documentation</a> details the rich information on each tweet that the archive throws away. Why not use the schema already in use by the API? I'm not sure why Twitter thought a CSV would be an easier format to consume.</p> <h2>Building a Better Archive</h2> <p>Unhappy with my archive, I set out to build one that meets my needs. Using a combination of the Twitter API and official archive, I'm able to create a new archive that contains everything I've done on Twitter. It's called <a href="https://github.com/kyleconroy/grain">Grain</a> and you can use it today. Your Grain archive includes the following records:</p> <ul> <li>Direct messages</li> <li>Favorites</li> <li>Followers</li> <li>Friends</li> <li>Lists</li> <li>Tweets</li> </ul> <p>Most importantly, the archive includes all images associated with the above records. For comparison, here's the data in my official archive versus the archive built via Grain.</p> <table> <thead> <tr> <th align="right">Records</th> <th align="right">Twitter</th> <th align="right">Grain</th> </tr> </thead> <tbody> <tr> <td align="right">tweets</td> <td align="right">4,348</td> <td align="right">4,348</td> </tr> <tr> <td align="right">favorites</td> <td align="right"></td> <td align="right">2,281</td> </tr> <tr> <td align="right">friends</td> <td align="right"></td> <td align="right">105</td> </tr> <tr> <td align="right">followers</td> <td align="right"></td> <td align="right">640</td> </tr> <tr> <td align="right">direct messages</td> <td align="right"></td> <td align="right">9</td> </tr> <tr> <td align="right">lists</td> <td align="right"></td> <td align="right">3</td> </tr> <tr> <td align="right">images</td> <td align="right"></td> <td align="right">1,986</td> </tr> </tbody> </table> <p>The records are stored as JSON, backed by <a href="https://github.com/kyleconroy/grain/tree/master/proto/twitter">protocol buffers</a>. I'm hoping this makes it easy to build other tools that interact with your archive.</p> <p>Tweets are stored as the full object returned from the Twitter API, which includes far more information than contained in the official archive. Highlights include a fully hydrated user object and detailed display information.</p> <p>A few months ago I <a href="https://twitter.com/kyle_conroy/status/987812599591878656">tweeted</a> about Waymo testing their self-driving cars in my neighborhood.</p> <p><blockquote class="twitter-tweet" data-lang="en"><p lang="en" dir="ltr">Waymo testing underway in Dogpatch <a href="https://t.co/LaOXpQiLi3">pic.twitter.com/LaOXpQiLi3</a></p>&amp;mdash; Kyle Conroy (@kyle_conroy) <a href="https://twitter.com/kyle_conroy/status/987812599591878656?ref_src=twsrc%5Etfw">April 21, 2018</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p> <p>Here's the data for that tweet from the official archive (CSV converted to YAML for readability):</p> <pre style="background-color:#f0f3f3"> tweet_id: 987812599591878656 in_reply_to_status_id: in_reply_to_user_id: timestamp: 2018-04-21 21:57:31 +0000 source: &lt;a href=&#34;http://tapbots.com/tweetbot&#34; rel=&#34;nofollow&#34;&gt;Tweetbot for iΟS&lt;/a&gt; text: Waymo testing underway in Dogpatch https://t.co/LaOXpQiLi3 retweeted_status_id: retweeted_status_user_id: retweeted_status_timestamp: expanded_urls: https://twitter.com/kyle_conroy/status/987812599591878656/photo/1 </pre> <p>And the same tweet in the Grain archive:</p> <pre style="background-color:#f0f3f3"> { &#34;created_at&#34;: &#34;Sat Apr 21 21:57:31 +0000 2018&#34;, &#34;display_text_range&#34;: [ 0, 34 ], &#34;entities&#34;: { &#34;hashtags&#34;: [], &#34;media&#34;: [ { &#34;display_url&#34;: &#34;pic.twitter.com/LaOXpQiLi3&#34;, &#34;expanded_url&#34;: &#34;https://twitter.com/kyle_conroy/status/987812599591878656/photo/1&#34;, &#34;id&#34;: &#34;987812583188017152&#34;, &#34;indices&#34;: [ &#34;35&#34;, &#34;58&#34; ], &#34;media_url&#34;: &#34;http://pbs.twimg.com/media/DbVqT42VMAAK4r3.jpg&#34;, &#34;media_url_https&#34;: &#34;https://pbs.twimg.com/media/DbVqT42VMAAK4r3.jpg&#34;, &#34;sizes&#34;: { &#34;large&#34;: { &#34;h&#34;: 1024, &#34;resize&#34;: &#34;fit&#34;, &#34;w&#34;: 768 }, &#34;medium&#34;: { &#34;h&#34;: 1024, &#34;resize&#34;: &#34;fit&#34;, &#34;w&#34;: 768 }, &#34;small&#34;: { &#34;h&#34;: 680, &#34;resize&#34;: &#34;fit&#34;, &#34;w&#34;: 510 }, &#34;thumb&#34;: { &#34;h&#34;: 150, &#34;resize&#34;: &#34;crop&#34;, &#34;w&#34;: 150 } }, &#34;type&#34;: &#34;photo&#34;, &#34;url&#34;: &#34;https://t.co/LaOXpQiLi3&#34; } ], &#34;urls&#34;: [], &#34;user_mentions&#34;: [] }, &#34;extended_entities&#34;: { &#34;media&#34;: [ { &#34;display_url&#34;: &#34;pic.twitter.com/LaOXpQiLi3&#34;, &#34;expanded_url&#34;: &#34;https://twitter.com/kyle_conroy/status/987812599591878656/photo/1&#34;, &#34;id&#34;: &#34;987812583188017152&#34;, &#34;indices&#34;: [ &#34;35&#34;, &#34;58&#34; ], &#34;media_url&#34;: &#34;http://pbs.twimg.com/media/DbVqT42VMAAK4r3.jpg&#34;, &#34;media_url_https&#34;: &#34;https://pbs.twimg.com/media/DbVqT42VMAAK4r3.jpg&#34;, &#34;sizes&#34;: { &#34;large&#34;: { &#34;h&#34;: 1024, &#34;resize&#34;: &#34;fit&#34;, &#34;w&#34;: 768 }, &#34;medium&#34;: { &#34;h&#34;: 1024, &#34;resize&#34;: &#34;fit&#34;, &#34;w&#34;: 768 }, &#34;small&#34;: { &#34;h&#34;: 680, &#34;resize&#34;: &#34;fit&#34;, &#34;w&#34;: 510 }, &#34;thumb&#34;: { &#34;h&#34;: 150, &#34;resize&#34;: &#34;crop&#34;, &#34;w&#34;: 150 } }, &#34;type&#34;: &#34;photo&#34;, &#34;url&#34;: &#34;https://t.co/LaOXpQiLi3&#34; } ] }, &#34;full_text&#34;: &#34;Waymo testing underway in Dogpatch https://t.co/LaOXpQiLi3&#34;, &#34;id&#34;: &#34;987812599591878657&#34;, &#34;lang&#34;: &#34;en&#34;, &#34;source&#34;: &#34;&lt;a href=\&#34;http://tapbots.com/tweetbot\&#34; rel=\&#34;nofollow\&#34;&gt;Tweetbot for i\u039fS&lt;/a&gt;&#34;, &#34;user&#34;: { &#34;created_at&#34;: &#34;Sun Jun 27 16:44:20 +0000 2010&#34;, &#34;description&#34;: &#34;Currently increasing GDP of the internet at @stripe. Formerly StackMachine, Twilio and UC Berkeley.&#34;, &#34;entities&#34;: { &#34;url&#34;: {} }, &#34;favourites_count&#34;: &#34;2298&#34;, &#34;followers_count&#34;: &#34;646&#34;, &#34;friends_count&#34;: &#34;105&#34;, &#34;id&#34;: &#34;160248151&#34;, &#34;lang&#34;: &#34;en&#34;, &#34;listed_count&#34;: &#34;28&#34;, &#34;name&#34;: &#34;Kyle Conroy&#34;, &#34;profile_background_color&#34;: &#34;131516&#34;, &#34;profile_background_image_url&#34;: &#34;http://abs.twimg.com/images/themes/theme14/bg.gif&#34;, &#34;profile_background_image_url_https&#34;: &#34;https://abs.twimg.com/images/themes/theme14/bg.gif&#34;, &#34;profile_background_tile&#34;: true, &#34;profile_banner_url&#34;: &#34;https://pbs.twimg.com/profile_banners/160248151/1441003908&#34;, &#34;profile_image_url&#34;: &#34;http://pbs.twimg.com/profile_images/691734524355424256/Jsa2rYyB_normal.jpg&#34;, &#34;profile_image_url_https&#34;: &#34;https://pbs.twimg.com/profile_images/691734524355424256/Jsa2rYyB_normal.jpg&#34;, &#34;profile_link_color&#34;: &#34;009999&#34;, &#34;profile_sidebar_border_color&#34;: &#34;EEEEEE&#34;, &#34;profile_sidebar_fill_color&#34;: &#34;EFEFEF&#34;, &#34;profile_text_color&#34;: &#34;333333&#34;, &#34;profile_use_background_image&#34;: true, &#34;screen_name&#34;: &#34;kyle_conroy&#34;, &#34;statuses_count&#34;: &#34;4086&#34;, &#34;url&#34;: &#34;https://t.co/qOYJRRAPQu&#34; } } </pre> <p>Sadly, it's not a perfect tool due to the limitations of the Twitter API. Direct messages older than 30 days are not available in the API. Without access to the official archive, the tool can only retrieve and store that last 2,300 tweets from your account. I have no idea why Twitter picked such a arbitrary limit.</p> <p>The archive is also missing some data. I haven't figured out how to archive videos, moments, polls, or saved searches.</p> <p>I'm hoping that my work will convince Twitter to improve its own archive and make Grain obsolete. If you feel like Twitter should provide you all your data without jumping through hoops, send a message to <a href="https://twitter.com/TwitterSupport">@TwitterSupport</a>. If you work or worked at Twitter, please reach out if you can put me in contact with the team that owns account archives. I'd love to give them my feedback directly.</p> Per-Test Database Isolation in Postgres https://conroy.org/per-test-database-isolation-in-postgres 2017-09-20T00:00:00Z <p>Over the years, I've tried many different strategies for isolating individual tests at the database layer.</p> <p><strong>One shared database, but all data is sharded by an account</strong>: Works in the short term, but eventually you'll have global tables that aren't scoped to an account.</p> <p><strong>Creating a new database for each test</strong>: Can be slow and error prone. Difficult to ensure all connections have been closed before dropping the database. Some CI providers only provide one logical test database.</p> <p><strong>Use an in-memory SQLite database:</strong> Only works if your application avoids Postgres-specific features. Even then you may still run into runtime differences between database engines.</p> <p><strong>Truncating the tables between tests</strong>: Test parallelization is tricky, if not impossible. No command to truncate all tables, drop all indexes, etc.</p> <p><strong>Wrap each test in a transaction</strong>: Works fine as long as none of your code uses transactions or parallel queries. While Postgres can't nest transactions, some SQL layers <a href="http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#module-ActiveRecord::Transactions::ClassMethods-label-Nested+transactions">simulate support</a> using savepoints.</p> <p><strong>Give each test a single connection, scoped to temporary tables</strong>: Supports individual transactions, but not two open transactions at once. Temporary tables can't be shared across connections.</p> <p>After trying all these different solutions, I think I've found a winning strategy. Postgres schemas and the search path allow you to isolate tests from each other while using the same logical database. You can think of a schema as a namespace for tables. From the docs:</p> <blockquote> <p>A database contains one or more named schemas, which in turn contain tables. […] The same object name can be used in different schemas without conflict; for example, both <code>schema1</code> and <code>myschema</code> can contain tables named <code>mytable</code>.</p> <p>There are several reasons why one might want to use schemas: […] To allow many users to use one database without interfering with each other</p> </blockquote> <p>Establish a connection to your database and create a new <a href="https://www.postgresql.org/docs/9.6/static/ddl-schemas.html">schema</a>. Here I'm using <code>foo</code> as a placeholder name. In the real world, you'd want to randomly generate a name for each individual test.</p> <pre style="background-color:#f0f3f3"> CREATE SCHEMA foo </pre> <p>Create a new connection pool to the database and set the <a href="https://www.postgresql.org/docs/9.6/static/sql-set.html">search path</a> to only include the new schema.</p> <pre style="background-color:#f0f3f3"> postgres:<span style="color:#09f;font-style:italic">//localhost/chalkbag?sslmode=disable&amp;search_path=foo </span><span style="color:#09f;font-style:italic"></span></pre> <p>Now you can create as many tables, functions, enums, and indexes as you want, in complete isolation. Once the test is finished, drop the schema. This step is optional if you're using a CI provider that destroys the database at the end of a test run.</p> <pre style="background-color:#f0f3f3"> DROP SCHEMA foo CASCADE </pre> <p>This strategy only works if you aren't using schemas for other purposes in your application. Here's an example in Go of the above steps.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">package</span> main <span style="color:#069;font-weight:bold">import</span> ( <span style="color:#c30">&#34;database/sql&#34;</span> _ <span style="color:#c30">&#34;github.com/lib/pq&#34;</span> ) <span style="color:#069;font-weight:bold">func</span> main() { dburl <span style="color:#555">:=</span> <span style="color:#c30">&#34;postgres://localhost/db?sslmode=disable&#34;</span> db, err <span style="color:#555">:=</span> sql.Open(<span style="color:#c30">&#34;postgres&#34;</span>, dburl) <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#366">panic</span>(err) } <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">:=</span> provision(db); err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#366">panic</span>(err) } } <span style="color:#069;font-weight:bold">func</span> provision(db <span style="color:#555">*</span>sql.DB) <span style="color:#078;font-weight:bold">error</span> { <span style="color:#09f;font-style:italic">// For each test, pick a new schema name at random. </span><span style="color:#09f;font-style:italic"></span> <span style="color:#09f;font-style:italic">// `foo` is used here only as an example </span><span style="color:#09f;font-style:italic"></span> <span style="color:#069;font-weight:bold">if</span> _, err <span style="color:#555">:=</span> db.Exec(<span style="color:#c30">&#34;CREATE SCHEMA foo&#34;</span>); err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> err } <span style="color:#069;font-weight:bold">defer</span> <span style="color:#069;font-weight:bold">func</span>() { db.Exec(<span style="color:#c30">&#34;DROP SCHEMA foo CASCADE&#34;</span>) }() surl <span style="color:#555">:=</span> <span style="color:#c30">&#34;postgres://localhost/db?sslmode=disable&amp;search_path=foo&#34;</span> sdb, err <span style="color:#555">:=</span> sql.Open(<span style="color:#c30">&#34;postgres&#34;</span>, surl) <span style="color:#069;font-weight:bold">if</span> err <span style="color:#555">!=</span> <span style="color:#069;font-weight:bold">nil</span> { <span style="color:#069;font-weight:bold">return</span> err } <span style="color:#09f;font-style:italic">// Run tests </span><span style="color:#09f;font-style:italic"></span> _, err = sdb.Exec(<span style="color:#c30">&#34;SELECT 1&#34;</span>) <span style="color:#069;font-weight:bold">return</span> err } </pre> <p>Using schemas avoids race conditions, makes clean up easy, and requires only a single logical database.</p> Writing API Clients in Go https://conroy.org/writing-api-clients-in-go 2017-09-17T00:00:00Z <p>The humble API client. A deceptively simple piece of software. Make a request and parse a response. Repeat.</p> <p>With the rise of public web services, API clients have become the main integration point. However, many of the Go clients I encounter are obtuse. Here's my guide for writing boring API clients that age well.</p> <p>We'll use the Dropbox Paper API as our example. First, start with a client struct.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">type</span> client <span style="color:#069;font-weight:bold">struct</span> { <span style="color:#09f;font-style:italic">// unexported fields </span><span style="color:#09f;font-style:italic"></span>} </pre> <p>Each API method should be represented as an exported method on the client. We'll use the <a href="https://www.dropbox.com/developers/documentation/http/documentation#paper-docs-download">document download</a> method as our example.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">func</span> (c <span style="color:#555">*</span>client) DownloadDoc() {} </pre> <p>The method should have two arguments: a context and a method-specific argument struct. It may feel awkward passing around structs instead of positional arguments, but structs allow you to make additions to methods without making breaking changes to the methods signatures.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">type</span> DocExport <span style="color:#069;font-weight:bold">struct</span> { DocID <span style="color:#078;font-weight:bold">string</span> Format ExportFormat } <span style="color:#069;font-weight:bold">func</span> (c <span style="color:#555">*</span>client) DownloadDoc(ctx context.Context, in <span style="color:#555">*</span>DocExport) { <span style="color:#09f;font-style:italic">// implemetation </span><span style="color:#09f;font-style:italic"></span>} </pre> <p>Take special care in avoiding stringly-typed APIs. If a struct field is limited to a set of values, create a new type that encapsulates those values. In this case, the <code>Format</code> field can only have two values: <code>markdown</code> and <code>html</code>.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">type</span> ExportFormat <span style="color:#078;font-weight:bold">string</span> <span style="color:#069;font-weight:bold">const</span> ( ExportFormatMarkdown ExportFormat = <span style="color:#c30">&#34;markdown&#34;</span> ExportFormatHTML = <span style="color:#c30">&#34;html&#34;</span> ) </pre> <p>As an added bonus, these types work natively with the <code>encoding/json</code> package.</p> <p>The method should return two results: a method-specific return value struct and an error.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">type</span> DocExportResult <span style="color:#069;font-weight:bold">struct</span> { Owner <span style="color:#078;font-weight:bold">string</span> Title <span style="color:#078;font-weight:bold">string</span> Revision <span style="color:#078;font-weight:bold">int64</span> MIME <span style="color:#078;font-weight:bold">string</span> Content []<span style="color:#078;font-weight:bold">byte</span> } <span style="color:#069;font-weight:bold">func</span> (c <span style="color:#555">*</span>client) DownloadDoc(ctx context.Context, in <span style="color:#555">*</span>DocExport) (<span style="color:#555">*</span>DocExportResult, <span style="color:#078;font-weight:bold">error</span>) { <span style="color:#09f;font-style:italic">// implemetation </span><span style="color:#09f;font-style:italic"></span>} </pre> <p>You may have noticed that our client struct isn't exported. Instead of exporting your concrete type, export an interface. Using an interface makes it easier for downstream consumers to mock out calls to your package.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">type</span> Client <span style="color:#069;font-weight:bold">interface</span> { DownloadDoc(context.Context, <span style="color:#555">*</span>DocExport) ( <span style="color:#555">*</span>DocExportResult, <span style="color:#078;font-weight:bold">error</span>) } </pre> <p>For increased flexibility, allow users to pass in their own <code>http.Client</code>, as many use cases require a customized HTTP transport.</p> <pre style="background-color:#f0f3f3"> <span style="color:#069;font-weight:bold">func</span> NewClient(token <span style="color:#078;font-weight:bold">string</span>, hclient <span style="color:#555">*</span>http.Client) Client { <span style="color:#09f;font-style:italic">// implementation </span><span style="color:#09f;font-style:italic"></span>} </pre> <p>This isn't the only way to design API clients in Go. Have another style you like more? I'd love to know about it.</p> Blogging on Paper https://conroy.org/blogging-on-paper 2017-08-29T00:00:00Z <p>I recently published my <a href="/progressively-worse-apps">first blog post of 2017</a>. The fact that it was posted exactly one year after my last post was mere coincidence. Looking over the the last six years of my blog, it's hard to really call it a blog at all.</p> <table> <thead> <tr> <th>Year</th> <th>2011</th> <th>2012</th> <th>2013</th> <th>2014</th> <th>2015</th> <th>2016</th> <th>2017</th> </tr> </thead> <tbody> <tr> <td>Number of Posts</td> <td>0</td> <td>1</td> <td>1</td> <td>2</td> <td>1</td> <td>1</td> <td>1</td> </tr> </tbody> </table> <p>I think I may change the title to &quot;Kyle Conroy's Personal Yearly Post&quot;. I'd love to pretend my posts are so meticulously researched that they take a year to write, but that's not the case. It took me more time to deploy my site than it did to write &quot;<a href="/blog-post">I should write a blog post</a>&quot;.</p> <p>One of the reasons for the slow output is my current workflow. I write posts in Markdown using vim, generate HTML using Jekyll, and serve the content via a small web app in Go on Heroku.</p> <p>There are a few problems with this setup. Starting a new post involves firing up vim and creating a new Markdown file, a process which isn't easy to replicate on mobile. When I'm finished with a draft, I can't easily get feedback from peers unless I open a PR on Github and invite them to my private repository.</p> <p>At work, we use <a href="https://hackpad.com/">Hackpad</a> as one of our tools for collaborating on documents. Dropbox bought Hackpad, shut it down, and turned it into <a href="/">Dropbox Paper</a>. Paper feels like Hackpad with all the bugs ironed out. Once I found out that Paper supported exporting documents in Markdown, I decided to try it out for blogging. After writing two posts in Paper, I'm glad I've switched.</p> <p>I found myself in some long lines at a museum over the weekend. While I was waiting, I worked on few posts using the Paper mobile app. I'm sure there are mobile apps out there for editing Markdown, but I'd need to figure out how to integrate them into my workflow.</p> <p>Paper also supports inline comments. I was able to ask a few friends for input on the previous post, and they could post comments inline. Many document editing platforms support comments, but I haven't used one before for blogging.</p> <p>My previous setup is unchanged save for an extra step at the start where I pull down posts from Paper using the API. While the API works, it was some serious short comings:</p> <ul> <li>The markdown export doesn't support fenced code blocks.</li> <li>The API doesn't return the date a document was created or updated.</li> <li>Changes to Paper documents don't generate webhooks.</li> <li>I can't create a read-only API key.</li> </ul> <p>These are minor annoyances now, but may prove problematic in the future. I've also open-sourced <a href="https://github.com/kyleconroy/paper/">my Paper API client written in Go</a>. It's incomplete, but implements enough of the API to export documents.</p> <p>I've now written two posts within the span of a week. We'll see how long it lasts, but using Paper has made writing more enjoyable.</p> Progressively Worse Apps https://conroy.org/progressively-worse-apps 2017-08-28T00:00:00Z <p>As <a href="https://developers.google.com/web/progressive-web-apps/">progress</a><a href="https://developers.google.com/web/progressive-web-apps/">ive web apps</a> become more popular, navigation has become fraught with peril. It's common for apps to make tens of requests to render a single page. Parts of the application load one by one until the entire page is finished. Poor network connections and slow endpoints expose intermediate states to users. <strong>Asynchronously loaded page elements shift click targets, resulting in a usability nightmare.</strong></p> <p>Let's take a look at the how an example application, the Heroku dashboard, suffers from these issues. Here's how the applications list loads.</p> <p><img src="/images/s_720837EE43E334638A99FF92C11432D9DA3F83171CEE7E593D7CAA4BE5F1C760_1503596154737_heroku-apps.png" alt="Loading stages for application list" /></p> <p>There are four phases before the page is finished loading. I find the gray layout unnecessary. Just continue to show me the loader, as I can't interact with the page. Without the loader, I'm not sure if the page is still loading.</p> <p>Besides the fake grey list, the application list page works. I can interact with the page before application metrics load. Once the metrics do load, nothing on the page shifts around or changes place.</p> <p>The application overview isn't as lucky.</p> <p><img src="/images/s_720837EE43E334638A99FF92C11432D9DA3F83171CEE7E593D7CAA4BE5F1C760_1503596147789_heroku-app.png" alt="Loading stages for application overview" /></p> <p>The first problem is the application metrics pane. Like the application list view, metrics are fetched asynchronously. However, here the placeholder is the wrong size. When the metrics load, the pane grows by a small amount. While the change is minimal, it's big enough to disrupt click targets.</p> <p>The second, larger problem is the add-ons pane. A Heroku application can have unlimited add-ons, making it difficult to size a placeholder element. When the add-ons do load, the dyno pane is pushed further down the page.</p> <p>The inspiration for this post came from the frustration of having this happen to me. I went to select the worker dyno, but right before I clicked, the add-ons finished loading, causing me to select the web dyno instead.</p> <p>There a few solutions to this problem.</p> <p>First, ask yourself if you really need a dynamically-loaded page in the first place. Static pages still work surprisingly well, not matter what Google and Facebook say.</p> <p>If you do decide to load parts of your page dynamically, make sure your placeholders are correctly sized, both vertically and horizontally. The container shouldn't grow or shrink after the load.</p> <p>When building and testing your application, throttle your connection to simulate a slow network. This feature is built in to most modern browsers.</p> <p>Don't cause users to second guess your app. Make navigation consistent under all scenarios.</p> Generating Code Examples https://conroy.org/generating-code-examples 2016-08-28T00:00:00Z <p>I've been thinking about a specific problem when writing technical documentation: code examples. In most cases, code examples are written by hand in a single language. If you want to add more languages, you'll need to write more examples. As you write more examples, typos creep in. The code no longer compiles. How can you fix this problem? I have a couple of ideas.</p> <h2>Requirements</h2> <p>For my purposes, I'm looking to write a single code snippet and then generate sample code in a variety of languages. Right now I'm targeting Ruby, Python, Go, Java, Node, C#, PHP, C++, C and Swift. At a minimum, I need support for:</p> <ul> <li>Importing modules</li> <li>Control flow (if, else, while, for, etc.)</li> <li>Lists and maps</li> <li>Calling functions and methods</li> </ul> <p>I don't have a functional language in my list. I'm not sure how you'd translate a code example written in an imperative language to one in a functional language.</p> <h2>Approaches</h2> <p>These are some of the approaches I thought through.</p> <h3>Templates</h3> <p>The first idea that came to mind was templates. Apparently this is how most code generators work. Note that when I say &quot;code generator&quot; here, I'm not talking about machine code. Instead, I'm talking about projects like <a href="http://swagger.io/">swagger</a> or <a href="https://grpc.io">gRPC</a>. They take some input and generate a large amount of source code.</p> <p>I think templates work for these projects because the input is constrained. For example, Swagger ingests a JSON document which describes an API. <code>gRPC</code> and <code>protoc</code> ingest <code>.proto</code> files. These inputs aren't turing-complete programming languages.</p> <p>We're trying to translate one sample program into many languages.</p> <h3>A DSL</h3> <p>I thought, alright, let's build a DSL. However, most DSLs I've seen are glorified configuration languages. Ansible uses YAML to encode a psuedo-programming language. There are a million gems for creating DSLs in Ruby, but I don't think those are up for the task (would love to be proven wrong).</p> <p>When writing code examples, we want to be able to use the full power of a turing-complete programming language, but one that has features that cleanly map to all languages we wish to support. This would be a &quot;lowest-common denominator&quot; language, one that can easily be correctly transpiled into many languages.</p> <p>It wouldn't be a particularly powerful language, but it would allow you to write the basic code you'd need. You could then include macros (or some type of extensions) to generate specific code sections. An example would be initializing a <code>gRPC</code> client. Each language does it in a separate way, but you could abstract it behind a macro.</p> <h3>Use a subset of an existing language</h3> <p>Instead of creating a new language, use a subset of an existing language. For example, Go has a parser built into the standard library. I could write examples in a subset of Go (no goroutines, no type casting, etc.) and generate my code examples that way. The macros I mentioned above aren't super easy to implement, but I could just hardcode them into the &quot;language&quot;.</p> <h2>Wrapping up</h2> <p>I'm not particularly happy with any of these ideas.</p> <p>I can't be the first person to run into this issue. If you've solved this problem (no matter the context) I'd love to hear how you did it.</p> What if Last Week Tonight aired on PBS? https://conroy.org/pbs-presents-last-week-tonight 2016-04-24T00:00:00Z <p>That isn't to say that Last Week Tonight doesn't report on serious news. It does, but every segment includes plenty of humor (and usually a dick joke). Here's what Last Week Tonight would look like if it aired on the Public Broadcasting Service.</p> <p><a data-flickr-embed="true" href="https://www.flickr.com/photos/140518731@N02/26605211036/in/dateposted-public/" title="PBS presents: Last Week Tonight"><img src="https://farm2.staticflickr.com/1608/26605211036_6b2f9fda56_b.jpg" width="1024" height="576" alt="PBS presents: Last Week Tonight"></a><script async src="//embedr.flickr.com/assets/client-code.js" charset="utf-8"></script></p> <p class="footnote">Why Flickr? YouTube marked it as stolen content. Vimeo has a weekly upload limit of 500MB. Dropbox will disable files that get viewed too many times. It's 2016 and sharing video content is still difficult.</p> I should write a blog post https://conroy.org/blog-post 2015-08-14T00:00:00Z <p>Wow, I haven’t written a post in a while.</p> <p>This one will need to be really good, then.</p> <p>Huh, my blog sure looks outdated.</p> <p>I can’t write a post with it looking like that.</p> <p>I need to update the design.</p> <p>How do I deploy this thing again?</p> <p>Oh, it’s a full-blown web application.</p> <p>All blogs today use static site generators.</p> <p>I should look up the best static site generator and switch to that.</p> <p>There are a lot of choices, I should figure out how I’m going to migrate my blog.</p> <p>I’m sure other people have this problem all the time!</p> <p>I should write a blog post.</p> HTML ROMs: Self-contained web applications https://conroy.org/self-contained-web-apps 2014-11-07T00:00:00Z <p>About a year ago, my friends found out that I had never played Pokemon Red or Blue. After some prodding, I agreed to play, filling a supposed void in my childhood. I was on my way to be the best there ever was.</p> <p>Thanks to <a href="http://openemu.org/">OpenEMU</a>, playing classic video games is easier than ever. All you need is a ROM, which are easy to find via Google. They're even legal if you own the original game. ROMs are self-contained time-capsules. Hours of game play, packed into less than 500k of data.</p> <p>What if we took the idea of ROMs and applied it to the web? A single HTML file containing a full application, no network connectivity required. <a href="http://gabrielecirulli.github.io/2048/">2048</a>, <a href="http://game.notch.net/drowning/">Drowning</a>, and <a href="http://adarkroom.doublespeakgames.com/">A Dark Room</a> are a few examples that come to mind. These games are great, but how long will they be around? Servers die, domain names expire, and files disappear. The archive team can't always be called upon to ride in and save the day.</p> <p>The answer is to turn them into single-file web applications: HTML ROMs.</p> <h2>The browser as an emulator</h2> <p>HTML ROMs are self contained applications that live in a single HTML file. They can't include any other files and must work without a network connection. A user should be able to save the file, open it locally, and have everything Just Work.</p> <p>To see how feasible this idea was, I took some time to make some to convert a few HTML games into single-file applications. Behold, <a href="/roms/2048.html">2048</a> and <a href="/roms/drowning.html">Drowning</a> as self-contained single-file applications, that you can enjoy whenever and where ever you have access to a web browser.</p> <h2>Development considerations</h2> <p>First, don't compress or minify JavaScript or CSS. This allows other people to easily remix and change the original application. Drowning is a good example of a game that's hard to change, as the original Dart source-code isn't available.</p> <p>Images, audio, and video should be base64 encoded into data URIs. The increase in file size is worth the increase in portability. You never need to worry about sending or saving more than one file.</p> <p>Some tooling will be required to make the above easy. A CSS preprocessor that <a href="https://github.com/heldr/datauri">turns URLs into data URIs</a>. A JavaScript <a href="https://github.com/esnext/es6-module-transpiler">ES6 transpiler</a> to compile individual modules into one larger JavaScript file. Tie them together with your favorite build tool, and your on your way.</p> <p>Sadly, mobile browsers don't make it easy to save and load HTML files from the file system. An application that wraps a web view with easy loading from the file system wouldn't be too hard to right, and would ensure that mobile users can take advantage of permanent offline access, without developers having to worry about the <a href="http://alistapart.com/article/application-cache-is-a-douchebag">headache that is AppCache</a>.</p> <h2>Go forth and create</h2> <p>Transform your favorite web game into an HTML ROM. Send me a link and I'll add it to the list below:</p> <ul> <li><a href="/roms/2048.html">2048</a></li> <li><a href="/roms/drowning.html">Drowning</a></li> </ul> The Need for an Open Barcode Database https://conroy.org/open-barcode-database 2014-01-06T00:00:00Z <p>Next time you're in the store, grab a random product and look turn it around. You'll probably see one of these:</p> <p><img src="/images/gtin.jpg" alt="GTIN Code" /></p> <p>Encoded in that bar code is the product's <a href="http://en.wikipedia.org/wiki/Global_Trade_Item_Number">Global Trade Item Number (GTIN)</a>. It's 8-14 digits long and almost every product sold in the world has one of these numbers. They are managed by the <a href="http://en.wikipedia.org/wiki/GS1">GS1</a>, a non-profit organization that manages standards for supply chains. GS1 provides (and controls) the <a href="http://en.wikipedia.org/wiki/GEPIR">Global Electronic Party Information Registry (GEPIR)</a>, the authoritative source for GTINs. The two interfaces to GEPIR are a web portal and a SOAP web service, both of which restrict a user to 20-30 <strong>daily</strong> requests. There is no option to download the dataset, submit missing item numbers, or correct inaccuracies.</p> <p>Compare this to OpenStreetMaps, where I can download an entire map of the earth with a single <a href="http://planet.openstreetmap.org/">click</a> or add a missing building using their web-based editor.</p> <p>I'm not the first person to realize how bad the situation is. There are other options out there, but none are the open database that's needed. Most options are just as closed as GEPIR.</p> <ul> <li><a href="http://www.upcdatabase.com/">UPC Database</a></li> <li><a href="https://www.datakick.org">Datakick</a></li> <li><a href="http://www.scandit.com/product-api"><del>Scandit</del></a></li> <li><a href="http://eandata.com/">EANdata</a></li> <li><a href="http://www.product-open-data.com/">Open Product Data (POD)</a></li> </ul> <p>The last service on the list, POD, is of interest. It has almost a million indexed items and has a free dump of the data. The data is licensed under the <a href="http://opendatacommons.org/licenses/odbl/">Open Data Commons Open Database License</a>, which is the same license as Open Street Maps. The only issue is that POD doesn't support the crowd-sourcing of its data.</p> <p>I think OpenStreetMaps solved this problem the right way: get people contributing data and make that data available under an open license.</p> <p>My idea is an application that let's you scan bar codes and upload GTINs. If the product isn't in the database, just attach a photo and add a short description. The database will be small at first, but once a few hundred people start tagging, it will be the next <a href="http://en.wikipedia.org/wiki/CueCat">CueCat</a>.</p> The Website That Never Was https://conroy.org/website-that-never-was 2013-04-03T00:00:00Z <p>Expiring domain names make me sad. Those expiry emails are a constant reminder that I had an idea I didn't execute on (sometimes for the better). After my most recent expiry, I thought I'd take a trip through my expired domains and see what could have been.</p> <h2>scrapebook.org</h2> <p>I've toyed with the idea of deleting my Facebook account many times, but I'm always worried about losing the data I've contributed. I wrote a <a href="https://github.com/kyleconroy/scrapebook">tool</a> to pull that data from Facebook. I discontinued the project (which was only ever half done) once Facebook added the ability to <a href="https://www.facebook.com/help/131112897028467/">download your data</a>.</p> <h2>kandrandyou.org</h2> <p>After Daniel Ritchie passed, I setup a tumblr for people to submit photos of themselves with <a href="http://en.wikipedia.org/wiki/C_%28programming_language%29">The C Programming Language</a>. 23 people submitted images. While I let the domain expire, the <a href="http://kandrandyou.tumblr.com"><del>tumblr is still up</del></a></p> <h2>adoptarepo.org</h2> <p>This one hurts. Adopt a Repo was going to the be the best place to find maintainers for your old, abandoned projects. It was going to look just like a pet adoption website, complete with <a href="http://www.youtube.com/watch?feature=player_embedded&amp;v=ao2A-eEIkA4">funny commercials</a>. You'd sign in with you Github account, identify your projects, and then the site would add a banner to the README announcing the search for a new maintainer. I still think there is a need for a solution to abandoned projects on Github.</p> <h2>areyouresponsive.com</h2> <p>Are You Responsive served as a developer tool for testing responsive websites. It's <a href="http://areyouresponsive.herokuapp.com/"><del>still alive</del></a>, but I let the name expire due to low usage and better tools being <a href="https://developer.mozilla.org/en-US/docs/Tools/Responsive_Design_View">built into browsers</a>.</p> <h2>dothingstellpeople.com</h2> <p>After reading Carl Lange's great post <a href="http://carl.flax.ie/dothingstellpeople.html">Do Things, Tell People</a>, I wanted to create a community based around people sharing their most recent creations and getting feedback. Imagine Hacker News but only Show HN posts. I decided against the idea because I didn't like the current state of online community software. I may reboot the idea using <a href="http://www.discourse.org/">Discourse</a>.</p> <h2>stockorshop.com</h2> <p>After my <a href="/2010/04/apple-stock">Apple products or stock</a> post, I thought it would be fun to generalize the concept and do it for other products. Game consoles, laptops, cars, and maybe even soda. Decided that I didn't want to be a one trick pony.</p> <h2>anyonecancode.org</h2> <p>Inspired by <a href="http://www.youtube.com/watch?v=thkDWSUzQDo">Anyone Can Cook</a> from Pixar's <a href="http://www.imdb.com/title/tt0382932/">Ratatouille</a>, Anyone Can Code was going to be a website for teaching beginners how to code. Never got past the idea phase.</p> <h2>castinghawk.com</h2> <p>A site for aspiring actors managing extras work. These actors are inundated with text messages and emails for extra work everyday, with few of these messages relevant. I built a prototype version, but never released it because it relied on email forwarding. I also wanted to use machine learning (via <a href="http://scikit-learn.org/stable/">scikit</a>) so that when a user flagged enough job offers, we could get an idea of what items they liked.</p> <h2>codingwithfriends.com</h2> <p>When Github launched their game jam, I wanted to make a crappy social game version of Github. You'd have to buy coins to open pull requests and create repos, and opening issues allowed you to level up as a developer.</p> Circles of Parity: A History https://conroy.org/circles-of-parity 2011-12-03T00:00:00Z <p>About two weeks ago reddit user <a href="http://www.reddit.com/user/AZxWildcat">AZxWildcat</a> submitted this awesome image to the <a href="http://www.reddit.com/r/CFB/">College Football subreddit</a>.</p> <p><img src="/images/circle.jpg" alt="PAC 12 Circle of Parity" /></p> <p>Instantly after seeing this, I wanted to know more. How often does this happen? What year did it happen the most? When did this first happen?</p> <h2>Science Time</h2> <p>My initial hypothesis was that circles of parity occur once or twice a season. To test this statement, I needed every college football game score. Ever. Thankfully, the Internet is a wonderful place.</p> <h2>James Howell, College Football Data King</h2> <p>One Google search later and I landed on <a href="http://homepages.cae.wisc.edu/~dwilson/rfsc/history/howell/">James Howell's College Football Scores</a> page. This guy has the score of every college football game going back to 1869. Yes, 142 years of football scores. He also maintains a similar data set of conference affliations. I wrote a fabric script to save all the results locally for further processing.</p> <h2>Analysis</h2> <p>After downloading and transforming the data, it was time to start analyzing. For each year I had a JSON representation of the conference affiliations and a CSV file of football scores. The first step was to create a a directed graph for all the games that year. Each node in the graph is a team. Connections to another node inditcate victory.</p> <p>Next, for each conference, I took a subset of the overall games graph involving only the teams from that conference. In the graph, a circle of parity manifests itself as a cycle which touches every node one time. In graph theory, this is called a <a href="http://en.wikipedia.org/wiki/Hamiltonian_path">Hamiltonian Cycle</a>. To find these cycles, I first enumerated all paths in the graph. Since I made sure to only visit a given node once within a path, a path is a Hamiltonian cycle if and only if its length is equal to the number of nodes in the graph. With just under <a href="https://github.com/kyleconroy/circle-of-parity/blob/master/analyze.py">a hundred lines of Python</a> my solution was complete.</p> <h2>Results</h2> <p>It turns out my initial hypothesis was correct. A circle of parity will likely form once a season. However, after 142 years of football, there are some really interesting data points.</p> <ul> <li><strong>Total circles:</strong> 134 in 142 years</li> <li><strong>Average per year:</strong> 0.94</li> <li><strong>First circle:</strong> 1869 between Rutgers and Princeton</li> <li><strong>Conference with most circles:</strong> 16 in the Big Ten</li> <li><strong>Most in a season:</strong> 6 in 2006 with Big 12, Big East, CUSA, MAC, Pac 10, SEC</li> <li><strong>Largest circle:</strong> 16 teams, WAC in 1997</li> </ul> <p>In 1997 there was a circle of parity between all <strong>16 teams</strong> in the WAC.</p> <table class="pretty"> <thead> <tr> <th>Winner</th> <th>Loser</th> <th>Score</th> </tr> </thead> <tbody> <tr> <td>Fresno State</td> <td>Nevada-Las Vegas</td> <td>46-28</td> </tr> <tr> <td>Nevada-Las Vegas</td> <td>Texas Christian</td> <td>21-19</td> </tr> <tr> <td>Texas Christian</td> <td>Southern Methodist</td> <td>21-18</td> </tr> <tr> <td>Southern Methodist</td> <td>Wyoming</td> <td>22-17</td> </tr> <tr> <td>Wyoming</td> <td>San Diego State</td> <td>41-17</td> </tr> <tr> <td>San Diego State</td> <td>San Jose State</td> <td>48-21</td> </tr> <tr> <td>San Jose State</td> <td>Air Force</td> <td>25-22</td> </tr> <tr> <td>Air Force</td> <td>Colorado State</td> <td>24-0</td> </tr> <tr> <td>Colorado State</td> <td>Tulsa</td> <td>44-8</td> </tr> <tr> <td>Tulsa</td> <td>Utah</td> <td>21-13</td> </tr> <tr> <td>Utah</td> <td>Rice</td> <td>31-14</td> </tr> <tr> <td>Rice</td> <td>New Mexico</td> <td>35-23</td> </tr> <tr> <td>New Mexico</td> <td>Texas-El Paso</td> <td>38-20</td> </tr> <tr> <td>Texas-El Paso</td> <td>Brigham Young</td> <td>14-3</td> </tr> <tr> <td>Brigham Young</td> <td>Hawaii</td> <td>17-3</td> </tr> <tr> <td>Hawaii</td> <td>Fresno State</td> <td>28-16</td> </tr> </tbody> </table> <p>Absolutely insane.</p> <p>If you're interested, the <a href="https://raw.github.com/kyleconroy/circle-of-parity/master/report.txt">full report</a> lists all 134 circles.</p> <h2>Making it Faster</h2> <p>It turns out that finding Hamiltonian cycles is hard. Not just hard, NP-complete hard. Thankfully, my graphs are very limited in size so the report only takes about a minute to generate on my laptop.</p> <p>Even so, I thought I would give <a href="http://pypy.org/">PyPy</a> a chance to work its magic. PyPy is a &quot;fast, compliant alternative implementation of Python&quot; written in Python itself. They aren't lying when they say its fast.</p> <pre style="background-color:#f0f3f3"> $ time python analyze.py real 0m52.537s user 0m51.050s sys 0m0.066s $ time pypy analyze.py real 0m22.758s user 0m18.952s sys 0m0.131s </pre> <p>Without changing a line of code, my code saw a <strong>56.6%</strong> percent improvement in running time. If you haven't checked out PyPy yet, I suggest you head over the homepage right now and get reading.</p> <h2>Wrapping Up</h2> <p>All my data and code can be found in the <a href="https://github.com/kyleconroy/circle-of-parity">circle-of-parity</a> Github repo. Another huge thanks to AZxWildcat, James Howell, and the website where I got some of the cycle detection code (which I have now forgotton).</p> Blocking that Annoying Bar on Websites https://conroy.org/annoying-bar 2010-09-20T00:00:00Z <p>More and more websites are popping up toolbars when you scroll down a certain distance on a page.</p> <p><img src="/images/apture/slashfilm.jpg" alt="Apture on Slashfilm"/></p> <p>I can't stand these annoying bars, some of which are powered by Apture.com</p> <p><img src="/images/apture/apture.jpg" alt="Apture Logo"/></p> <p>I think <strong>Annoy.</strong> should be added to that list. If you want to block the Apture bar, simply add</p> <pre style="background-color:#f0f3f3"> 127.0.0.1 www.apture.com </pre> <p>to your hosts file (usually /etc/hosts)</p> VoiceForms 4000: Wufoo + Twilio = Serious Business https://conroy.org/voiceforms-4000 2010-08-31T00:00:00Z <p>Just finished up hacking on my submission for the Wufoo API contest. VoiceForms 4000 takes any existing Wufoo form and turns it into a phone survey. The app is written in Python, runs on App Engine, and uses Twilio for all the phone magic.</p> <p><a href="https://voiceforms4000.appspot.com/">https://voiceforms4000.appspot.com/</a></p> <p>Go ahead and fill one of the sample forms and tell me what you think.</p> jQueried: A Safari 5 Extension https://conroy.org/jqueried-safari-extension 2010-06-08T00:00:00Z <p>jQueried is a Safari 5 extension in the same vein as the jQuerify bookmarklet. jQueried adds jQuery to the current webpage, a task which can be very useful for debugging.</p> <p>After finishing my first extension, simple as it is, I am impressed with the simplicity of Safari's extension API. Coupled with Firefox's Jetpack and Chrome's extension system, it looks like the future of browser extensions is all JS + CSS + HTML.</p> <p>Source is on github: <a href="https://github.com/kyleconroy/jqueried">https://github.com/kyleconroy/jqueried</a></p> <p><del>Download the extension: <a href="http://dl.dropbox.com/u/40773/jqueried.safariextz">http://dl.dropbox.com/u/40773/jqueried.safariextz</a></del></p> What if I had bought Apple stock instead? https://conroy.org/apple-stock 2010-04-24T00:00:00Z <link type="text/css" rel="stylesheet" href="/css/table-style.css" /> <script type="text/javascript" src="/js/jquery.js"></script> <script type="text/javascript" src="/js/jquery.tablesorter.min.js"></script> <script type="text/javascript"> $(document).ready(function(){ $("#apple").tablesorter({ sortList: [[3,1]] }); }); </script> <p> Currently, Apple's stock is at an all time high. A share today is worth over 40 times its value seven years ago. So, how much would you have today if you purchased stock instead of an Apple product? See for yourself in the table below. A huge thanks to <a href="http://www.everymac.com">everymac.com</a> for the original prices and release dates. All values are calculated using Apple's current stock price according to <a href="http://brivierestockquotes.appspot.com/">http://brivierestockquotes.appspot.com</a>. </p> <p> I have also released the data set behind the calculations, which include Apple's stock price since 1997 as well as almost all Apple products released since 1997. Code at <a href="http://www.github.com/kyleconroy/apple-stock">github.com/kyleconroy/apple-stock</a>. </p> <p> Big thanks to <a href="http://news.ycombinator.com/user?id=gojomo">gojomo</a> at <a href="http://news.ycombinator.com/item?id=1291809">http://news.ycombinator.com/item?id=1291809</a> for the idea. </p> <!-- EXCERPT --> <h3>Last updated April 1, 2012</h3> <table cellpadding="0" cellspacing="1" class="tablesorter" id="apple"><thead><tr><th>Product</th><th>Release Date</th><th>Original Price</th><th>Stock Value Today</th></tr></thead><tbody><tr><td>Apple Power Macintosh G3 233 Desktop</td><td>1997-11-10</td><td>$2400.0</td><td>$308119</td></tr><tr><td>Apple Power Macintosh G3 233 Minitower</td><td>1997-11-10</td><td>$2400.0</td><td>$308119</td></tr><tr><td>Apple Power Macintosh G3 266 Desktop</td><td>1997-11-10</td><td>$2400.0</td><td>$308119</td></tr><tr><td>Apple Power Macintosh G3 266 Minitower</td><td>1997-11-10</td><td>$3000.0</td><td>$385149</td></tr><tr><td>Apple Power Macintosh G3 300 Desktop</td><td>1998-03-17</td><td>$2750.0</td><td>$250191</td></tr><tr><td>Apple Power Macintosh G3 300 Minitower</td><td>1998-03-17</td><td>$2399.0</td><td>$218258</td></tr><tr><td>Apple Power Macintosh G3 333 Minitower</td><td>1998-08-12</td><td>$2999.0</td><td>$179446</td></tr><tr><td>Apple Power Macintosh G3 233 All-in-One</td><td>1998-03-31</td><td>$1499.0</td><td>$130628</td></tr><tr><td>Apple Power Macintosh G3 266 All-in-One</td><td>1998-03-31</td><td>$1799.0</td><td>$156771</td></tr><tr><td>Apple Power Macintosh G3 300 (Blue &amp; White)</td><td>1999-01-05</td><td>$1599.0</td><td>$88520</td></tr><tr><td>Apple Power Macintosh G3 350 (Blue &amp; White)</td><td>1999-01-05</td><td>$1599.0</td><td>$88520</td></tr><tr><td>Apple Power Macintosh G3 400 (Blue &amp; White)</td><td>1999-01-05</td><td>$1999.0</td><td>$110664</td></tr><tr><td>Apple Power Macintosh G3 450 (Blue &amp; White)</td><td>1999-06-01</td><td>$2999.0</td><td>$160540</td></tr><tr><td>Apple Mac Server G3 233 Minitower</td><td>1998-03-02</td><td>$3349.0</td><td>$352881</td></tr><tr><td>Apple Mac Server G3 266 Minitower</td><td>1998-03-02</td><td>$4499.0</td><td>$474055</td></tr><tr><td>Apple Mac Server G3 300 Minitower</td><td>1998-03-17</td><td>$4999.0</td><td>$454802</td></tr><tr><td>Apple Mac Server G3 333 Minitower</td><td>1998-09-01</td><td>$4599.0</td><td>$323250</td></tr><tr><td>Apple Mac Server G3 350 (Blue &amp; White)</td><td>1999-01-05</td><td>$3299.0</td><td>$182633</td></tr><tr><td>Apple Mac Server G3 400 (Blue &amp; White)</td><td>1999-01-05</td><td>$4999.0</td><td>$276745</td></tr><tr><td>Apple Mac Server G3 450 (Blue &amp; White)</td><td>1999-06-01</td><td>$4999.0</td><td>$267602</td></tr><tr><td>Apple PowerBook G3 250 (Original/Kanga/3500)</td><td>1997-11-10</td><td>$5700.0</td><td>$731784</td></tr><tr><td>Apple PowerBook G3 233 (Wallstreet)</td><td>1998-05-06</td><td>$2299.0</td><td>$181842</td></tr><tr><td>Apple PowerBook G3 250 (Wallstreet)</td><td>1998-05-06</td><td>$2979.0</td><td>$235627</td></tr><tr><td>Apple PowerBook G3 292 (Wallstreet)</td><td>1998-05-06</td><td>$4599.0</td><td>$363763</td></tr><tr><td>Apple PowerBook G3 233 (PDQ - Late 1998)</td><td>1998-09-01</td><td>$2799.0</td><td>$196733</td></tr><tr><td>Apple PowerBook G3 266 (PDQ - Late 1998)</td><td>1998-09-01</td><td>$3499.0</td><td>$245934</td></tr><tr><td>Apple PowerBook G3 300 (PDQ - Late 1998)</td><td>1998-09-01</td><td>$4999.0</td><td>$351365</td></tr><tr><td>Apple PowerBook G3 333 (Bronze KB/Lombard)</td><td>1999-05-10</td><td>$2499.0</td><td>$132473</td></tr><tr><td>Apple PowerBook G3 400 (Bronze KB/Lombard)</td><td>1999-05-10</td><td>$3499.0</td><td>$185484</td></tr><tr><td>Apple PowerBook G3 400 (Firewire/Pismo)</td><td>2000-02-16</td><td>$2499.0</td><td>$52515</td></tr><tr><td>Apple PowerBook G3 500 (Firewire/Pismo)</td><td>2000-02-16</td><td>$3499.0</td><td>$73530</td></tr><tr><td>Apple Power Macintosh G4 400 (PCI)</td><td>1999-08-31</td><td>$1599.0</td><td>$58778</td></tr><tr><td>Apple Power Macintosh G4 450 (AGP)</td><td>1999-08-31</td><td>$2499.0</td><td>$91862</td></tr><tr><td>Apple Power Macintosh G4 500 (AGP)</td><td>1999-08-31</td><td>$3499.0</td><td>$128622</td></tr><tr><td>Apple Power Macintosh G4 350 (PCI)</td><td>1999-10-13</td><td>$1599.0</td><td>$59880</td></tr><tr><td>Apple Power Macintosh G4 400 (AGP)</td><td>1999-10-13</td><td>$2499.0</td><td>$93583</td></tr><tr><td>Apple Power Macintosh G4 350 (AGP)</td><td>1999-12-02</td><td>$1599.0</td><td>$34797</td></tr><tr><td>Apple Power Macintosh G4 400 (Gigabit)</td><td>2000-07-19</td><td>$1599.0</td><td>$36396</td></tr><tr><td>Apple Power Macintosh G4 450 DP (Gigabit)</td><td>2000-07-19</td><td>$2499.0</td><td>$56882</td></tr><tr><td>Apple Power Macintosh G4 500 DP (Gigabit)</td><td>2000-07-19</td><td>$3199.0</td><td>$72815</td></tr><tr><td>Apple Power Macintosh G4 450 Cube</td><td>2000-07-19</td><td>$1799.0</td><td>$40948</td></tr><tr><td>Apple Power Macintosh G4 500 Cube</td><td>2000-07-19</td><td>$2299.0</td><td>$52329</td></tr><tr><td>Apple Power Macintosh G4 466 (Digital Audio)</td><td>2001-01-09</td><td>$1699.0</td><td>$118445</td></tr><tr><td>Apple Power Macintosh G4 533 (Digital Audio)</td><td>2001-01-09</td><td>$2199.0</td><td>$153303</td></tr><tr><td>Apple Power Macintosh G4 667 (Digital Audio)</td><td>2001-01-09</td><td>$2799.0</td><td>$195132</td></tr><tr><td>Apple Power Macintosh G4 733 (Digital Audio)</td><td>2001-01-09</td><td>$3499.0</td><td>$243933</td></tr><tr><td>Apple Power Macintosh G4 733 (Quicksilver)</td><td>2001-07-18</td><td>$1699.0</td><td>$97945</td></tr><tr><td>Apple Power Macintosh G4 867 (Quicksilver)</td><td>2001-07-18</td><td>$2499.0</td><td>$144064</td></tr><tr><td>Apple Power Macintosh G4 800 DP (Quicksilver)</td><td>2001-07-18</td><td>$3499.0</td><td>$201713</td></tr><tr><td>Apple Power Macintosh G4 800 (QS 2002)</td><td>2002-01-28</td><td>$1599.0</td><td>$82360</td></tr><tr><td>Apple Power Macintosh G4 933 (QS 2002)</td><td>2002-01-28</td><td>$2299.0</td><td>$118416</td></tr><tr><td>Apple Power Macintosh G4 1.0 DP (QS 2002)</td><td>2002-01-28</td><td>$2999.0</td><td>$154471</td></tr><tr><td>Apple Power Macintosh G4 867 DP (MDD)</td><td>2002-08-13</td><td>$1699.0</td><td>$139539</td></tr><tr><td>Apple Power Macintosh G4 1.0 DP (MDD)</td><td>2002-08-13</td><td>$2499.0</td><td>$205243</td></tr><tr><td>Apple Power Macintosh G4 1.25 DP (MDD)</td><td>2002-08-13</td><td>$3299.0</td><td>$270947</td></tr><tr><td>Apple Power Macintosh G4 1.0 (FW 800)</td><td>2003-01-28</td><td>$1499.0</td><td>$123281</td></tr><tr><td>Apple Power Macintosh G4 1.25 DP (FW 800)</td><td>2003-01-28</td><td>$1999.0</td><td>$164403</td></tr><tr><td>Apple Power Macintosh G4 1.42 DP (FW 800)</td><td>2003-01-28</td><td>$2699.0</td><td>$221973</td></tr><tr><td>Apple Power Macintosh G4 1.25 (MDD 2003)</td><td>2003-06-23</td><td>$1299.0</td><td>$81722</td></tr><tr><td>Apple Mac Server G4 350 (AGP)</td><td>1999-12-02</td><td>$2999.0</td><td>$65264</td></tr><tr><td>Apple Mac Server G4 400 (AGP)</td><td>1999-12-02</td><td>$2999.0</td><td>$65264</td></tr><tr><td>Apple Mac Server G4 450 (AGP)</td><td>1999-12-02</td><td>$3999.0</td><td>$87027</td></tr><tr><td>Apple Mac Server G4 500 (AGP)</td><td>1999-12-02</td><td>$4499.0</td><td>$97908</td></tr><tr><td>Apple Mac Server G4 450 DP (Gigabit)</td><td>2000-07-19</td><td>$3999.0</td><td>$91025</td></tr><tr><td>Apple Mac Server G4 500 DP (Gigabit)</td><td>2000-07-19</td><td>$4999.0</td><td>$113787</td></tr><tr><td>Apple Mac Server G4 533 (Digital Audio)</td><td>2001-01-09</td><td>$2999.0</td><td>$209075</td></tr><tr><td>Apple Mac Server G4 533 DP (Digital Audio)</td><td>2001-01-09</td><td>$3999.0</td><td>$278790</td></tr><tr><td>Apple Mac Server G4 733 (Quicksilver)</td><td>2001-09-08</td><td>$2799.0</td><td>$193111</td></tr><tr><td>Apple Mac Server G4 800 DP (Quicksilver)</td><td>2001-09-08</td><td>$3799.0</td><td>$262104</td></tr><tr><td>Apple Mac Server G4 933 (QS 2002)</td><td>2002-01-28</td><td>$2799.0</td><td>$144170</td></tr><tr><td>Apple Mac Server G4 1.0 DP (QS 2002)</td><td>2002-01-28</td><td>$3299.0</td><td>$169924</td></tr><tr><td>Apple Mac Server G4 1.0 DP (MDD)</td><td>2002-08-27</td><td>$2999.0</td><td>$241998</td></tr><tr><td>Apple Mac Server G4 1.25 DP (MDD)</td><td>2002-08-27</td><td>$3499.0</td><td>$282345</td></tr><tr><td>Apple PowerBook G4 400 (Original - Ti)</td><td>2001-01-09</td><td>$2599.0</td><td>$181189</td></tr><tr><td>Apple PowerBook G4 500 (Original - Ti)</td><td>2001-01-09</td><td>$3499.0</td><td>$243933</td></tr><tr><td>Apple PowerBook G4 550 (Gigabit - Ti)</td><td>2001-10-16</td><td>$2199.0</td><td>$146327</td></tr><tr><td>Apple PowerBook G4 667 (Gigabit - Ti)</td><td>2001-10-16</td><td>$2999.0</td><td>$199561</td></tr><tr><td>Apple PowerBook G4 667 (DVI - Ti)</td><td>2002-04-29</td><td>$2499.0</td><td>$125064</td></tr><tr><td>Apple PowerBook G4 800 (DVI - Ti)</td><td>2002-04-29</td><td>$3199.0</td><td>$160096</td></tr><tr><td>Apple PowerBook G4 867 (Ti)</td><td>2002-11-06</td><td>$2299.0</td><td>$160088</td></tr><tr><td>Apple PowerBook G4 1.0 (Ti)</td><td>2002-11-06</td><td>$2999.0</td><td>$208832</td></tr><tr><td>Apple PowerBook G4 867 12" (Al)</td><td>2003-01-07</td><td>$1799.0</td><td>$145166</td></tr><tr><td>Apple PowerBook G4 1.0 17" (Al)</td><td>2003-01-07</td><td>$3299.0</td><td>$266206</td></tr><tr><td>Apple PowerBook G4 1.0 12" (DVI - Al)</td><td>2003-09-16</td><td>$1599.0</td><td>$85749</td></tr><tr><td>Apple PowerBook G4 1.0 15" (FW800 - Al)</td><td>2003-09-16</td><td>$1999.0</td><td>$107200</td></tr><tr><td>Apple PowerBook G4 1.25 15" (FW800 - Al)</td><td>2003-09-16</td><td>$2599.0</td><td>$139376</td></tr><tr><td>Apple PowerBook G4 1.33 17" (Al)</td><td>2003-09-16</td><td>$2999.0</td><td>$160827</td></tr><tr><td>Apple PowerBook G4 1.33 12" (Al)</td><td>2004-04-19</td><td>$1599.0</td><td>$67607</td></tr><tr><td>Apple PowerBook G4 1.33 15" (Al)</td><td>2004-04-19</td><td>$1999.0</td><td>$84520</td></tr><tr><td>Apple PowerBook G4 1.5 15" (Al)</td><td>2004-04-19</td><td>$2499.0</td><td>$105661</td></tr><tr><td>Apple PowerBook G4 1.5 17" (Al)</td><td>2004-04-19</td><td>$2799.0</td><td>$118345</td></tr><tr><td>Apple PowerBook G4 1.5 12" (Al)</td><td>2005-01-31</td><td>$1499.0</td><td>$23373</td></tr><tr><td>Apple PowerBook G4 1.5 15" (SMS/BT2 - Al)</td><td>2005-01-31</td><td>$1999.0</td><td>$31170</td></tr><tr><td>Apple PowerBook G4 1.67 15" (Al)</td><td>2005-01-31</td><td>$2299.0</td><td>$35848</td></tr><tr><td>Apple PowerBook G4 1.67 17" (Al)</td><td>2005-01-31</td><td>$2699.0</td><td>$42085</td></tr><tr><td>Apple PowerBook G4 1.67 15" (DLSD/HR - Al)</td><td>2005-10-19</td><td>$1999.0</td><td>$21814</td></tr><tr><td>Apple PowerBook G4 1.67 17" (DLSD/HR - Al)</td><td>2005-10-19</td><td>$2499.0</td><td>$27271</td></tr><tr><td>Apple Power Macintosh G5 1.6 (PCI)</td><td>2003-06-23</td><td>$1999.0</td><td>$125760</td></tr><tr><td>Apple Power Macintosh G5 1.8 (PCI-X)</td><td>2003-06-23</td><td>$2399.0</td><td>$150925</td></tr><tr><td>Apple Power Macintosh G5 2.0 DP (PCI-X)</td><td>2003-06-23</td><td>$2999.0</td><td>$188672</td></tr><tr><td>Apple Power Macintosh G5 1.8 DP (PCI-X)</td><td>2003-11-18</td><td>$2499.0</td><td>$146889</td></tr><tr><td>Apple Power Macintosh G5 1.8 DP (PCI)</td><td>2004-06-09</td><td>$1999.0</td><td>$79370</td></tr><tr><td>Apple Power Macintosh G5 2.0 DP (PCI-X 2)</td><td>2004-06-09</td><td>$2499.0</td><td>$99223</td></tr><tr><td>Apple Power Macintosh G5 2.5 DP (PCI-X)</td><td>2004-06-09</td><td>$2999.0</td><td>$119076</td></tr><tr><td>Apple Power Macintosh G5 1.8 (PCI)</td><td>2004-10-19</td><td>$1499.0</td><td>$37904</td></tr><tr><td>Apple Power Macintosh G5 2.0 DP (PCI)</td><td>2005-04-27</td><td>$1999.0</td><td>$33337</td></tr><tr><td>Apple Power Macintosh G5 2.3 DP (PCI-X)</td><td>2005-04-27</td><td>$2499.0</td><td>$41676</td></tr><tr><td>Apple Power Macintosh G5 2.7 DP (PCI-X)</td><td>2005-04-27</td><td>$2999.0</td><td>$50015</td></tr><tr><td>Apple Power Macintosh G5 Dual Core (2.0)</td><td>2005-10-19</td><td>$1999.0</td><td>$21814</td></tr><tr><td>Apple Power Macintosh G5 Dual Core (2.3)</td><td>2005-10-19</td><td>$2499.0</td><td>$27271</td></tr><tr><td>Apple Power Macintosh G5 "Quad Core" (2.5)</td><td>2005-10-19</td><td>$3299.0</td><td>$36001</td></tr><tr><td>Apple Mac Pro "Quad Core" 2.0 (Original)</td><td>2006-08-07</td><td>$2199.0</td><td>$19616</td></tr><tr><td>Apple Mac Pro "Quad Core" 2.66 (Original)</td><td>2006-08-07</td><td>$2499.0</td><td>$22292</td></tr><tr><td>Apple Mac Pro "Quad Core" 3.0 (Original)</td><td>2006-08-07</td><td>$3299.0</td><td>$29428</td></tr><tr><td>Apple Mac Pro "Eight Core" 3.0 (2,1)</td><td>2007-04-04</td><td>$3999.0</td><td>$25433</td></tr><tr><td>Apple Mac Pro "Quad Core" 2.8 (2008)</td><td>2008-01-08</td><td>$2299.0</td><td>$8048</td></tr><tr><td>Apple Mac Pro "Eight Core" 2.8 (2008)</td><td>2008-01-08</td><td>$2799.0</td><td>$9799</td></tr><tr><td>Apple Mac Pro "Eight Core" 3.0 (2008)</td><td>2008-01-08</td><td>$3599.0</td><td>$12600</td></tr><tr><td>Apple Mac Pro "Eight Core" 3.2 (2008)</td><td>2008-01-08</td><td>$4399.0</td><td>$15400</td></tr><tr><td>Apple Mac Pro "Quad Core" 2.66 (2009/Nehalem)</td><td>2009-03-03</td><td>$2499.0</td><td>$16954</td></tr><tr><td>Apple Mac Pro "Quad Core" 2.93 (2009/Nehalem)</td><td>2009-03-03</td><td>$2999.0</td><td>$20346</td></tr><tr><td>Apple Mac Pro "Quad Core" 3.33 (2009/Nehalem)</td><td>2009-12-04</td><td>$3699.0</td><td>$11471</td></tr><tr><td>Apple Mac Pro "Eight Core" 2.26 (2009/Nehalem)</td><td>2009-03-03</td><td>$3299.0</td><td>$22382</td></tr><tr><td>Apple Mac Pro "Eight Core" 2.66 (2009/Nehalem)</td><td>2009-03-03</td><td>$4699.0</td><td>$31880</td></tr><tr><td>Apple Mac Pro "Eight Core" 2.93 (2009/Nehalem)</td><td>2009-03-03</td><td>$5899.0</td><td>$40022</td></tr><tr><td>Apple Mac Pro "Quad Core" 2.8 (2010/Nehalem)</td><td>2010-07-27</td><td>$2499.0</td><td>$5673</td></tr><tr><td>Apple Mac Pro "Quad Core" 3.2 (2010/Nehalem)</td><td>2010-07-27</td><td>$2899.0</td><td>$6581</td></tr><tr><td>Apple Mac Pro "Six Core" 3.33 (2010/Westmere)</td><td>2010-07-27</td><td>$3699.0</td><td>$8397</td></tr><tr><td>Apple Mac Pro "Eight Core" 2.4 (2010/Westmere)</td><td>2010-07-27</td><td>$3499.0</td><td>$7943</td></tr><tr><td>Apple Mac Pro "Twelve Core" 2.66 (2010/Westmere)</td><td>2010-07-27</td><td>$4999.0</td><td>$11349</td></tr><tr><td>Apple Mac Pro "Twelve Core" 2.93 (2010/Westmere)</td><td>2010-07-27</td><td>$6199.0</td><td>$14073</td></tr><tr><td>Apple Mac Pro "Quad Core" 2.8 (Server)</td><td>2010-11-05</td><td>$2999.0</td><td>$5669</td></tr><tr><td>Apple Mac Pro "Quad Core" 3.2 (Server)</td><td>2010-11-05</td><td>$3399.0</td><td>$6425</td></tr><tr><td>Apple Mac Pro "Six Core" 3.33 (Server)</td><td>2010-11-05</td><td>$4199.0</td><td>$7938</td></tr><tr><td>Apple Mac Pro "Eight Core" 2.4 (Server)</td><td>2010-11-05</td><td>$3774.0</td><td>$7134</td></tr><tr><td>Apple Mac Pro "Twelve Core" 2.66 (Server)</td><td>2010-11-05</td><td>$5274.0</td><td>$9970</td></tr><tr><td>Apple Mac Pro "Twelve Core" 2.93 (Server)</td><td>2010-11-05</td><td>$6474.0</td><td>$12239</td></tr><tr><td>Apple Xserve G4/1.0</td><td>2002-05-14</td><td>$2999.0</td><td>$140363</td></tr><tr><td>Apple Xserve G4/1.0 DP</td><td>2002-05-14</td><td>$3999.0</td><td>$187166</td></tr><tr><td>Apple Xserve G4/1.33 (Slot Load)</td><td>2003-02-10</td><td>$2799.0</td><td>$233724</td></tr><tr><td>Apple Xserve G4/1.33 DP (Slot Load)</td><td>2003-02-10</td><td>$3799.0</td><td>$317227</td></tr><tr><td>Apple Xserve G4/1.33 DP Cluster Node</td><td>2003-03-01</td><td>$2799.0</td><td>$229254</td></tr><tr><td>Apple Xserve G5/2.0 (PCI-X)</td><td>2004-01-06</td><td>$2999.0</td><td>$162719</td></tr><tr><td>Apple Xserve G5/2.0 DP (PCI-X)</td><td>2004-01-06</td><td>$3999.0</td><td>$216977</td></tr><tr><td>Apple Xserve G5/2.0 DP Cluster Node (PCI-X)</td><td>2004-01-06</td><td>$2999.0</td><td>$162719</td></tr><tr><td>Apple Xserve G5/2.3 DP (PCI-X)</td><td>2005-01-04</td><td>$3999.0</td><td>$74995</td></tr><tr><td>Apple Xserve G5/2.3 DP Cluster Node (PCI-X)</td><td>2005-01-04</td><td>$2999.0</td><td>$56241</td></tr><tr><td>Apple Xserve Xeon 2.0 "Quad Core" (Late 2006)</td><td>2006-08-07</td><td>$2999.0</td><td>$26752</td></tr><tr><td>Apple Xserve Xeon 2.66 "Quad Core" (Late 2006)</td><td>2006-08-07</td><td>$0.0</td><td>$0</td></tr><tr><td>Apple Xserve Xeon 3.0 "Quad Core" (Late 2006)</td><td>2006-08-07</td><td>$0.0</td><td>$0</td></tr><tr><td>Apple Xserve Xeon 2.8 "Quad Core" (Early 2008)</td><td>2008-01-08</td><td>$2999.0</td><td>$10499</td></tr><tr><td>Apple Xserve Xeon 2.8 "Eight Core" (Early 2008)</td><td>2008-01-08</td><td>$3499.0</td><td>$12250</td></tr><tr><td>Apple Xserve Xeon 3.0 "Eight Core" (Early 2008)</td><td>2008-01-08</td><td>$4299.0</td><td>$15050</td></tr><tr><td>Apple Xserve Xeon Nehalem 2.26 "Quad Core"</td><td>2009-04-07</td><td>$2999.0</td><td>$15635</td></tr><tr><td>Apple Xserve Xeon Nehalem 2.26 "Eight Core"</td><td>2009-04-07</td><td>$3599.0</td><td>$18763</td></tr><tr><td>Apple Xserve Xeon Nehalem 2.66 "Eight Core"</td><td>2009-04-07</td><td>$4999.0</td><td>$26062</td></tr><tr><td>Apple Xserve Xeon Nehalem 2.93 "Eight Core"</td><td>2009-04-07</td><td>$6199.0</td><td>$32318</td></tr><tr><td>Apple Xserve RAID</td><td>2003-02-10</td><td>$5999.0</td><td>$500933</td></tr><tr><td>Apple Xserve RAID (SFP)</td><td>2004-01-06</td><td>$5999.0</td><td>$325493</td></tr><tr><td>Apple iMac G3/233 Original - Bondi (Rev. A &amp; B)</td><td>1998-05-06</td><td>$1299.0</td><td>$102746</td></tr><tr><td>Apple iMac G3/266 (Fruit Colors)</td><td>1999-01-05</td><td>$1199.0</td><td>$66376</td></tr><tr><td>Apple iMac G3/333 (Fruit Colors)</td><td>1999-04-14</td><td>$1199.0</td><td>$80952</td></tr><tr><td>Apple iMac G3/350 (Slot Loading - Blueberry)</td><td>1999-10-05</td><td>$999.0</td><td>$35253</td></tr><tr><td>Apple iMac G3/400 DV (Slot Loading - Fruit)</td><td>1999-10-05</td><td>$1299.0</td><td>$45839</td></tr><tr><td>Apple iMac G3/400 DV SE (Slot Loading)</td><td>1999-10-05</td><td>$1499.0</td><td>$52897</td></tr><tr><td>Apple iMac G3/350 (Summer 2000 - Indigo)</td><td>2000-07-19</td><td>$799.0</td><td>$18186</td></tr><tr><td>Apple iMac G3/400 DV (Summer 2000 - I/R)</td><td>2000-07-19</td><td>$999.0</td><td>$22739</td></tr><tr><td>Apple iMac G3/450 DV+ (Summer 2000)</td><td>2000-07-19</td><td>$1299.0</td><td>$29567</td></tr><tr><td>Apple iMac G3/500 DV SE (Summer 2000)</td><td>2000-07-19</td><td>$1499.0</td><td>$34120</td></tr><tr><td>Apple iMac G3/400 (Early 2001 - Indigo)</td><td>2001-02-22</td><td>$899.0</td><td>$57339</td></tr><tr><td>Apple iMac G3/500 (Early 2001 - Flower/Blue)</td><td>2001-02-22</td><td>$1199.0</td><td>$76474</td></tr><tr><td>Apple iMac G3/600 SE (Early 2001)</td><td>2001-02-22</td><td>$1499.0</td><td>$95609</td></tr><tr><td>Apple iMac G3/500 (Summer 2001 - I/S)</td><td>2001-07-18</td><td>$799.0</td><td>$46061</td></tr><tr><td>Apple iMac G3/600 (Summer 2001)</td><td>2001-07-18</td><td>$999.0</td><td>$57591</td></tr><tr><td>Apple iMac G3/700 SE (Summer 2001)</td><td>2001-07-18</td><td>$1499.0</td><td>$86415</td></tr><tr><td>Apple iMac G4/700 (Flat Panel)</td><td>2002-01-07</td><td>$1299.0</td><td>$68018</td></tr><tr><td>Apple iMac G4/800 (Flat Panel)</td><td>2002-01-07</td><td>$1799.0</td><td>$94200</td></tr><tr><td>Apple iMac G4/800 17-Inch (Flat Panel)</td><td>2002-07-17</td><td>$1999.0</td><td>$153260</td></tr><tr><td>Apple iMac G4/800 - X Only (Flat Panel)</td><td>2003-02-04</td><td>$1299.0</td><td>$106687</td></tr><tr><td>Apple iMac G4/1.0 17-Inch (Flat Panel)</td><td>2003-02-04</td><td>$1799.0</td><td>$147752</td></tr><tr><td>Apple iMac G4/1.0 15-Inch "FP" (USB 2.0)</td><td>2003-09-08</td><td>$1299.0</td><td>$68497</td></tr><tr><td>Apple iMac G4/1.25 17-Inch "FP" (USB 2.0)</td><td>2003-09-08</td><td>$1799.0</td><td>$94862</td></tr><tr><td>Apple iMac G4/1.25 20-Inch "FP" (USB 2.0)</td><td>2003-11-18</td><td>$2199.0</td><td>$129255</td></tr><tr><td>Apple iMac G5/1.6 17-Inch</td><td>2004-08-31</td><td>$1299.0</td><td>$45148</td></tr><tr><td>Apple iMac G5/1.8 17-Inch</td><td>2004-08-31</td><td>$1499.0</td><td>$52100</td></tr><tr><td>Apple iMac G5/1.8 20-Inch</td><td>2004-08-31</td><td>$1899.0</td><td>$66002</td></tr><tr><td>Apple iMac G5/1.8 17-Inch (ALS)</td><td>2005-05-03</td><td>$1299.0</td><td>$21508</td></tr><tr><td>Apple iMac G5/2.0 17-Inch (ALS)</td><td>2005-05-03</td><td>$1499.0</td><td>$24819</td></tr><tr><td>Apple iMac G5/2.0 20-Inch (ALS)</td><td>2005-05-03</td><td>$1799.0</td><td>$29787</td></tr><tr><td>Apple iMac G5/1.9 17-Inch (iSight)</td><td>2005-10-12</td><td>$1299.0</td><td>$15813</td></tr><tr><td>Apple iMac G5/2.1 20-Inch (iSight)</td><td>2005-10-12</td><td>$1699.0</td><td>$20682</td></tr><tr><td>Apple iMac "Core Duo" 1.83 17-Inch</td><td>2006-01-10</td><td>$1299.0</td><td>$9631</td></tr><tr><td>Apple iMac "Core Duo" 2.0 20-Inch</td><td>2006-01-10</td><td>$1699.0</td><td>$12597</td></tr><tr><td>Apple iMac "Core Duo" 1.83 17-Inch (IG)</td><td>2006-07-05</td><td>$899.0</td><td>$9456</td></tr><tr><td>Apple iMac "Core 2 Duo" 1.83 17-Inch (IG)</td><td>2006-09-06</td><td>$999.0</td><td>$8552</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.0 17-Inch</td><td>2006-09-06</td><td>$1199.0</td><td>$10265</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.16 17-Inch</td><td>2006-09-06</td><td>$1299.0</td><td>$11121</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.16 20-Inch</td><td>2006-09-06</td><td>$1499.0</td><td>$12833</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.33 20-Inch</td><td>2006-09-06</td><td>$1749.0</td><td>$14973</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.16 24-Inch</td><td>2006-09-06</td><td>$1999.0</td><td>$17114</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.33 24-Inch</td><td>2006-09-06</td><td>$2249.0</td><td>$19254</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.0 20-Inch (Al)</td><td>2007-08-07</td><td>$1199.0</td><td>$5323</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.4 20-Inch (Al)</td><td>2007-08-07</td><td>$1499.0</td><td>$6655</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.4 24-Inch (Al)</td><td>2007-08-07</td><td>$1799.0</td><td>$7987</td></tr><tr><td>Apple iMac "Core 2 Extreme" 2.8 24-Inch (Al)</td><td>2007-08-07</td><td>$2299.0</td><td>$10207</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.4 20-Inch (Early 2008)</td><td>2008-04-28</td><td>$1199.0</td><td>$4173</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.66 20-Inch (Early 2008)</td><td>2008-04-28</td><td>$1499.0</td><td>$5217</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.8 24-Inch (Early 2008)</td><td>2008-04-28</td><td>$1799.0</td><td>$6262</td></tr><tr><td>Apple iMac "Core 2 Duo" 3.06 24-Inch (Early 2008)</td><td>2008-04-28</td><td>$2199.0</td><td>$7654</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.66 20-Inch (Early 2009)</td><td>2009-03-03</td><td>$1199.0</td><td>$8134</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.66 24-Inch (Early 2009)</td><td>2009-03-03</td><td>$1499.0</td><td>$10170</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.93 24-Inch (Early 2009)</td><td>2009-03-03</td><td>$1799.0</td><td>$12205</td></tr><tr><td>Apple iMac "Core 2 Duo" 3.06 24-Inch (Early 2009)</td><td>2009-03-03</td><td>$2199.0</td><td>$14919</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.0 20-Inch (Mid-2009)</td><td>2009-04-07</td><td>$899.0</td><td>$4686</td></tr><tr><td>Apple iMac "Core 2 Duo" 2.26 20-Inch (Mid-2009)</td><td>2010-03-04</td><td>$899.0</td><td>$2557</td></tr><tr><td>Apple iMac "Core 2 Duo" 3.06 21.5-Inch (Late 2009)</td><td>2009-10-20</td><td>$1199.0</td><td>$3616</td></tr><tr><td>Apple iMac "Core 2 Duo" 3.33 21.5-Inch (Late 2009)</td><td>2009-10-20</td><td>$1399.0</td><td>$4220</td></tr><tr><td>Apple iMac "Core 2 Duo" 3.06 27-Inch (Late 2009)</td><td>2009-10-20</td><td>$1699.0</td><td>$5124</td></tr><tr><td>Apple iMac "Core 2 Duo" 3.33 27-Inch (Late 2009)</td><td>2009-10-20</td><td>$1899.0</td><td>$5728</td></tr><tr><td>Apple iMac "Core i5" 2.66 27-Inch (Late 2009)</td><td>2009-10-20</td><td>$1999.0</td><td>$6029</td></tr><tr><td>Apple iMac "Core i7" 2.8 27-Inch (Late 2009)</td><td>2009-10-20</td><td>$2199.0</td><td>$6633</td></tr><tr><td>Apple iMac "Core i3" 3.06 21.5-Inch (Mid-2010)</td><td>2010-07-27</td><td>$1199.0</td><td>$2722</td></tr><tr><td>Apple iMac "Core i3" 3.2 21.5-Inch (Mid-2010)</td><td>2010-07-27</td><td>$1499.0</td><td>$3403</td></tr><tr><td>Apple iMac "Core i5" 3.6 21.5-Inch (Mid-2010)</td><td>2010-07-27</td><td>$1699.0</td><td>$3857</td></tr><tr><td>Apple iMac "Core i3" 3.2 27-Inch (Mid-2010)</td><td>2010-07-27</td><td>$1699.0</td><td>$3857</td></tr><tr><td>Apple iMac "Core i5" 2.8 27-Inch (Mid-2010)</td><td>2010-07-27</td><td>$1999.0</td><td>$4538</td></tr><tr><td>Apple iMac "Core i5" 3.6 27-Inch (Mid-2010)</td><td>2010-07-27</td><td>$1899.0</td><td>$4311</td></tr><tr><td>Apple iMac "Core i7" 2.93 27-Inch (Mid-2010)</td><td>2010-07-27</td><td>$2199.0</td><td>$4992</td></tr><tr><td>Apple iMac "Core i5" 2.5 21.5-Inch (Mid-2011)</td><td>2011-05-03</td><td>$1199.0</td><td>$2064</td></tr><tr><td>Apple iMac "Core i5" 2.7 21.5-Inch (Mid-2011)</td><td>2011-05-03</td><td>$1499.0</td><td>$2581</td></tr><tr><td>Apple iMac "Core i7" 2.8 21.5-Inch (Mid-2011)</td><td>2011-05-03</td><td>$1699.0</td><td>$2925</td></tr><tr><td>Apple iMac "Core i5" 2.7 27-Inch (Mid-2011)</td><td>2011-05-03</td><td>$1699.0</td><td>$2925</td></tr><tr><td>Apple iMac "Core i5" 3.1 27-Inch (Mid-2011)</td><td>2011-05-03</td><td>$1999.0</td><td>$3441</td></tr><tr><td>Apple iMac "Core i7" 3.4 27-Inch (Mid-2011)</td><td>2011-05-03</td><td>$2199.0</td><td>$3786</td></tr><tr><td>Apple iMac "Core i3" 3.1 21.5-Inch (Late 2011)</td><td>2011-08-08</td><td>$999.0</td><td>$1695</td></tr><tr><td>Apple eMac G4/700</td><td>2002-04-29</td><td>$999.0</td><td>$49995</td></tr><tr><td>Apple eMac G4/800</td><td>2002-08-13</td><td>$1499.0</td><td>$123113</td></tr><tr><td>Apple eMac G4/800 (ATI)</td><td>2003-05-06</td><td>$799.0</td><td>$54747</td></tr><tr><td>Apple eMac G4/1.0 (ATI)</td><td>2003-05-06</td><td>$999.0</td><td>$68451</td></tr><tr><td>Apple eMac G4/1.25 (USB 2.0)</td><td>2004-04-13</td><td>$799.0</td><td>$35563</td></tr><tr><td>Apple eMac G4/1.42 (2005)</td><td>2005-05-03</td><td>$799.0</td><td>$13229</td></tr><tr><td>Apple Mac mini G4/1.25</td><td>2005-01-11</td><td>$499.0</td><td>$9268</td></tr><tr><td>Apple Mac mini G4/1.42</td><td>2005-01-11</td><td>$599.0</td><td>$11125</td></tr><tr><td>Apple Mac mini G4/1.33</td><td>2005-09-27</td><td>$499.0</td><td>$5598</td></tr><tr><td>Apple Mac mini G4/1.5</td><td>2005-09-27</td><td>$599.0</td><td>$6720</td></tr><tr><td>Apple Mac mini "Core Solo" 1.5</td><td>2006-02-28</td><td>$599.0</td><td>$5243</td></tr><tr><td>Apple Mac mini "Core Duo" 1.66</td><td>2006-02-28</td><td>$799.0</td><td>$6994</td></tr><tr><td>Apple Mac mini "Core Duo" 1.83</td><td>2006-09-06</td><td>$799.0</td><td>$6840</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 1.83</td><td>2007-08-07</td><td>$599.0</td><td>$2659</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.0</td><td>2007-08-07</td><td>$799.0</td><td>$3547</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.0 (Early 2009)</td><td>2009-03-03</td><td>$599.0</td><td>$4063</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.26 (Early 2009)</td><td>2009-03-03</td><td>$749.0</td><td>$5081</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.26 (Late 2009)</td><td>2009-10-20</td><td>$599.0</td><td>$1806</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.53 (Late 2009)</td><td>2009-10-20</td><td>$799.0</td><td>$2410</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.66 (Late 2009)</td><td>2009-10-20</td><td>$849.0</td><td>$2560</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.53 (Server)</td><td>2009-10-20</td><td>$999.0</td><td>$3013</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.4 (Mid-2010)</td><td>2010-06-15</td><td>$699.0</td><td>$1613</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.66 (Mid-2010)</td><td>2010-06-15</td><td>$849.0</td><td>$1960</td></tr><tr><td>Apple Mac mini "Core 2 Duo" 2.66 (Server)</td><td>2010-06-15</td><td>$999.0</td><td>$2306</td></tr><tr><td>Apple Mac mini "Core i5" 2.3 (Mid-2011)</td><td>2011-07-20</td><td>$599.0</td><td>$928</td></tr><tr><td>Apple Mac mini "Core i5" 2.5 (Mid-2011)</td><td>2011-07-20</td><td>$799.0</td><td>$1238</td></tr><tr><td>Apple Mac mini "Core i7" 2.7 (Mid-2011)</td><td>2011-07-20</td><td>$899.0</td><td>$1393</td></tr><tr><td>Apple Mac mini "Core i7" 2.0 (Mid-2011/Server)</td><td>2011-07-20</td><td>$999.0</td><td>$1548</td></tr><tr><td>Apple iBook G3/300 (Original/Clamshell)</td><td>1999-07-21</td><td>$1599.0</td><td>$70908</td></tr><tr><td>Apple iBook G3/366 SE (Original/Clamshell)</td><td>2000-02-16</td><td>$1799.0</td><td>$37805</td></tr><tr><td>Apple iBook G3/366 (Firewire/Clamshell)</td><td>2000-09-13</td><td>$1499.0</td><td>$30990</td></tr><tr><td>Apple iBook G3/466 SE (Firewire/Clamshell)</td><td>2000-09-13</td><td>$1799.0</td><td>$37192</td></tr><tr><td>Apple iBook G3/500 (Dual USB - Tr)</td><td>2001-05-01</td><td>$1299.0</td><td>$60047</td></tr><tr><td>Apple iBook G3/500 (Late 2001 - Tr)</td><td>2001-10-16</td><td>$1199.0</td><td>$79784</td></tr><tr><td>Apple iBook G3/600 (Late 2001 - Tr)</td><td>2001-10-16</td><td>$1499.0</td><td>$99747</td></tr><tr><td>Apple iBook G3/600 14-Inch (Early 2002 - Tr)</td><td>2002-01-07</td><td>$1799.0</td><td>$94200</td></tr><tr><td>Apple iBook G3/600 (16 VRAM - Tr)</td><td>2002-05-20</td><td>$1199.0</td><td>$58113</td></tr><tr><td>Apple iBook G3/700 (16 VRAM - Tr)</td><td>2002-05-20</td><td>$1499.0</td><td>$72653</td></tr><tr><td>Apple iBook G3/700 14-Inch (16 VRAM - Tr)</td><td>2002-05-20</td><td>$1799.0</td><td>$87194</td></tr><tr><td>Apple iBook G3/700 (16 VRAM - Op)</td><td>2002-11-06</td><td>$999.0</td><td>$69564</td></tr><tr><td>Apple iBook G3/800 (32 VRAM - Tr)</td><td>2002-11-06</td><td>$1299.0</td><td>$90454</td></tr><tr><td>Apple iBook G3/800 14-Inch (32 VRAM - Tr)</td><td>2002-11-06</td><td>$1599.0</td><td>$111345</td></tr><tr><td>Apple iBook G3/800 (Early 2003 - Op)</td><td>2003-04-22</td><td>$999.0</td><td>$88602</td></tr><tr><td>Apple iBook G3/900 (Early 2003 - Op)</td><td>2003-04-22</td><td>$1299.0</td><td>$115209</td></tr><tr><td>Apple iBook G3/900 14-Inch (Early 2003 - Op)</td><td>2003-04-22</td><td>$1499.0</td><td>$132947</td></tr><tr><td>Apple iBook G4/800 12-Inch (Original - Op)</td><td>2003-10-22</td><td>$1099.0</td><td>$57900</td></tr><tr><td>Apple iBook G4/933 14-Inch (Original - Op)</td><td>2003-10-22</td><td>$1299.0</td><td>$68437</td></tr><tr><td>Apple iBook G4/1.0 14-Inch (Original - Op)</td><td>2003-10-22</td><td>$1499.0</td><td>$78974</td></tr><tr><td>Apple iBook G4/1.0 12-Inch (Early 2004 - Op)</td><td>2004-04-19</td><td>$1099.0</td><td>$46467</td></tr><tr><td>Apple iBook G4/1.0 14-Inch (Early 2004 - Op)</td><td>2004-04-19</td><td>$1299.0</td><td>$54923</td></tr><tr><td>Apple iBook G4/1.2 14-Inch (Early 2004 - Op)</td><td>2004-04-19</td><td>$1499.0</td><td>$63379</td></tr><tr><td>Apple iBook G4/1.2 12-Inch (Late 2004 - Op)</td><td>2004-10-19</td><td>$999.0</td><td>$25261</td></tr><tr><td>Apple iBook G4/1.33 14-Inch (Late 2004 - Op)</td><td>2004-10-19</td><td>$1299.0</td><td>$32847</td></tr><tr><td>Apple iBook G4/1.33 12-Inch (Mid-2005 - Op)</td><td>2005-07-26</td><td>$999.0</td><td>$13727</td></tr><tr><td>Apple iBook G4/1.42 14-Inch (Mid-2005 - Op)</td><td>2005-07-26</td><td>$1299.0</td><td>$17850</td></tr><tr><td>Apple MacBook "Core Duo" 1.83 13"</td><td>2006-05-16</td><td>$1099.0</td><td>$10140</td></tr><tr><td>Apple MacBook "Core Duo" 2.0 13" (White)</td><td>2006-05-16</td><td>$1299.0</td><td>$11985</td></tr><tr><td>Apple MacBook "Core Duo" 2.0 13" (Black)</td><td>2006-05-16</td><td>$1499.0</td><td>$13830</td></tr><tr><td>Apple MacBook "Core 2 Duo" 1.83 13"</td><td>2006-11-08</td><td>$1099.0</td><td>$7991</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.0 13" (White/06)</td><td>2006-11-08</td><td>$1299.0</td><td>$9445</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.0 13" (Black)</td><td>2006-11-08</td><td>$1499.0</td><td>$10900</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.0 13" (White/07)</td><td>2007-05-15</td><td>$1099.0</td><td>$6128</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.16 13" (White)</td><td>2007-05-15</td><td>$1299.0</td><td>$7243</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.16 13" (Black)</td><td>2007-05-15</td><td>$1499.0</td><td>$8358</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.0 13" (White-SR)</td><td>2007-11-01</td><td>$1099.0</td><td>$3515</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.2 13" (White-SR)</td><td>2007-11-01</td><td>$1299.0</td><td>$4155</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.2 13" (Black-SR)</td><td>2007-11-01</td><td>$1499.0</td><td>$4794</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.1 13" (White-08)</td><td>2008-02-26</td><td>$1099.0</td><td>$5530</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.4 13" (White-08)</td><td>2008-02-26</td><td>$1299.0</td><td>$6536</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.4 13" (Black-08)</td><td>2008-02-26</td><td>$1499.0</td><td>$7542</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.0 13" (Unibody)</td><td>2008-10-14</td><td>$1299.0</td><td>$7482</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.4 13" (Unibody)</td><td>2008-10-14</td><td>$1599.0</td><td>$9210</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.0 13" (White-09)</td><td>2009-01-20</td><td>$999.0</td><td>$7659</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.13 13" (White-09)</td><td>2009-05-27</td><td>$999.0</td><td>$4501</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.26 13" (Uni/Late 09)</td><td>2009-10-20</td><td>$999.0</td><td>$3013</td></tr><tr><td>Apple MacBook "Core 2 Duo" 2.4 13" (Mid-2010)</td><td>2010-05-18</td><td>$999.0</td><td>$2373</td></tr><tr><td>Apple MacBook Pro "Core Duo" 1.67 15"</td><td>2006-01-10</td><td>$1999.0</td><td>$14821</td></tr><tr><td>Apple MacBook Pro "Core Duo" 1.83 15"</td><td>2006-01-10</td><td>$2499.0</td><td>$18529</td></tr><tr><td>Apple MacBook Pro "Core Duo" 2.0 15"</td><td>2006-02-14</td><td>$2499.0</td><td>$22150</td></tr><tr><td>Apple MacBook Pro "Core Duo" 2.16 15"</td><td>2006-02-14</td><td>$2799.0</td><td>$24809</td></tr><tr><td>Apple MacBook Pro "Core Duo" 2.16 17"</td><td>2006-04-24</td><td>$2799.0</td><td>$25523</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.16 15"</td><td>2006-10-24</td><td>$1999.0</td><td>$14787</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.33 15"</td><td>2006-10-24</td><td>$2499.0</td><td>$18485</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.33 17"</td><td>2006-10-24</td><td>$2799.0</td><td>$20705</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.2 15" (SR)</td><td>2007-06-05</td><td>$1999.0</td><td>$9770</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.4 15" (SR)</td><td>2007-06-05</td><td>$2499.0</td><td>$12213</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.6 15" (SR)</td><td>2007-11-01</td><td>$2749.0</td><td>$8793</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.4 17" (SR)</td><td>2007-06-05</td><td>$2799.0</td><td>$13680</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.6 17" (SR)</td><td>2007-11-01</td><td>$3049.0</td><td>$9752</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.4 15" (08)</td><td>2008-02-26</td><td>$1999.0</td><td>$10058</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.5 15" (08)</td><td>2008-02-26</td><td>$2499.0</td><td>$12574</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.6 15" (08)</td><td>2008-02-26</td><td>$2749.0</td><td>$13832</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.5 17" (08)</td><td>2008-02-26</td><td>$2799.0</td><td>$14084</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.6 17" (08)</td><td>2008-02-26</td><td>$3049.0</td><td>$15342</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.4 15" (Unibody)</td><td>2008-10-14</td><td>$1999.0</td><td>$11515</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.53 15" (Unibody)</td><td>2008-10-14</td><td>$2499.0</td><td>$14395</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.8 15" (Unibody)</td><td>2008-10-14</td><td>$2799.0</td><td>$16123</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.66 17" (Unibody)</td><td>2009-01-06</td><td>$2799.0</td><td>$18040</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.93 17" (Unibody)</td><td>2009-01-06</td><td>$3099.0</td><td>$19974</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.66 15" (Unibody)</td><td>2009-03-03</td><td>$2499.0</td><td>$16954</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.93 15" (Unibody)</td><td>2009-03-03</td><td>$2799.0</td><td>$18989</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.26 13" (SD/FW)</td><td>2009-06-08</td><td>$1199.0</td><td>$4997</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.53 13" (SD/FW)</td><td>2009-06-08</td><td>$1499.0</td><td>$6247</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.53 15" (SD)</td><td>2009-06-08</td><td>$1699.0</td><td>$7081</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.66 15" (SD)</td><td>2009-06-08</td><td>$1999.0</td><td>$8331</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.8 15" (SD)</td><td>2009-06-08</td><td>$2299.0</td><td>$9581</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 3.06 15" (SD)</td><td>2009-06-08</td><td>$2599.0</td><td>$10832</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.8 17" Mid-2009</td><td>2009-06-08</td><td>$2499.0</td><td>$10415</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 3.06 17" Mid-2009</td><td>2009-06-08</td><td>$2799.0</td><td>$11665</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.4 13" Mid-2010</td><td>2010-04-13</td><td>$1199.0</td><td>$2965</td></tr><tr><td>Apple MacBook Pro "Core 2 Duo" 2.66 13" Mid-2010</td><td>2010-04-13</td><td>$1499.0</td><td>$3707</td></tr><tr><td>Apple MacBook Pro "Core i5" 2.4 15" Mid-2010</td><td>2010-04-13</td><td>$1799.0</td><td>$4449</td></tr><tr><td>Apple MacBook Pro "Core i5" 2.53 15" Mid-2010</td><td>2010-04-13</td><td>$1999.0</td><td>$4943</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.66 15" Mid-2010</td><td>2010-04-13</td><td>$2199.0</td><td>$5438</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.8 15" Mid-2010</td><td>2010-10-20</td><td>$2399.0</td><td>$4631</td></tr><tr><td>Apple MacBook Pro "Core i5" 2.53 17" Mid-2010</td><td>2010-04-13</td><td>$2299.0</td><td>$5685</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.66 17" Mid-2010</td><td>2010-04-13</td><td>$2499.0</td><td>$6180</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.8 17" Mid-2010</td><td>2010-10-20</td><td>$2699.0</td><td>$5211</td></tr><tr><td>Apple MacBook Pro "Core i5" 2.3 13" Early 2011</td><td>2011-02-24</td><td>$1199.0</td><td>$2096</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.7 13" Early 2011</td><td>2011-02-24</td><td>$1499.0</td><td>$2621</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.0 15" Early 2011</td><td>2011-02-24</td><td>$1799.0</td><td>$3145</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.2 15" Early 2011</td><td>2011-02-24</td><td>$2199.0</td><td>$3845</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.3 15" Early 2011</td><td>2011-02-24</td><td>$2449.0</td><td>$4282</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.2 17" Early 2011</td><td>2011-02-24</td><td>$2499.0</td><td>$4369</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.3 17" Early 2011</td><td>2011-02-24</td><td>$2749.0</td><td>$4806</td></tr><tr><td>Apple MacBook Pro "Core i5" 2.4 13" Late 2011</td><td>2011-10-24</td><td>$1199.0</td><td>$1771</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.8 13" Late 2011</td><td>2011-10-24</td><td>$1499.0</td><td>$2214</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.2 15" Late 2011</td><td>2011-10-24</td><td>$1799.0</td><td>$2658</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.4 15" Late 2011</td><td>2011-10-24</td><td>$2199.0</td><td>$3249</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.5 15" Late 2011</td><td>2011-10-24</td><td>$2449.0</td><td>$3618</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.4 17" Late 2011</td><td>2011-10-24</td><td>$2499.0</td><td>$3692</td></tr><tr><td>Apple MacBook Pro "Core i7" 2.5 17" Late 2011</td><td>2011-10-24</td><td>$2749.0</td><td>$4061</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 1.6 13" (Original)</td><td>2008-01-15</td><td>$1799.0</td><td>$6380</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 1.8 13" (Original)</td><td>2008-01-15</td><td>$3098.0</td><td>$10987</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 1.6 13" (NVIDIA)</td><td>2008-10-14</td><td>$1799.0</td><td>$10363</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 1.86 13" (NVIDIA)</td><td>2008-10-14</td><td>$2499.0</td><td>$14395</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 1.86 13" (Mid-09)</td><td>2009-06-08</td><td>$1499.0</td><td>$6247</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 2.13 13" (Mid-09)</td><td>2009-06-08</td><td>$1799.0</td><td>$7498</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 1.4 11" (Late '10)</td><td>2010-10-20</td><td>$999.0</td><td>$1928</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 1.6 11" (Late '10)</td><td>2010-10-20</td><td>$1299.0</td><td>$2508</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 1.86 13" (Late '10)</td><td>2010-10-20</td><td>$1299.0</td><td>$2508</td></tr><tr><td>Apple MacBook Air "Core 2 Duo" 2.13 13" (Late '10)</td><td>2010-10-20</td><td>$1699.0</td><td>$3280</td></tr><tr><td>Apple MacBook Air "Core i5" 1.6 11" (Mid-2011)</td><td>2011-07-20</td><td>$999.0</td><td>$1548</td></tr><tr><td>Apple MacBook Air "Core i7" 1.8 11" (Mid-2011)</td><td>2011-07-20</td><td>$1349.0</td><td>$2090</td></tr><tr><td>Apple MacBook Air "Core i5" 1.7 13" (Mid-2011)</td><td>2011-07-20</td><td>$1299.0</td><td>$2012</td></tr><tr><td>Apple MacBook Air "Core i7" 1.8 13" (Mid-2011)</td><td>2011-07-20</td><td>$1699.0</td><td>$2632</td></tr><tr><td>Apple MacBook Air "Core i5" 1.6 13" (Early 2012)</td><td>2012-02-10</td><td>$999.0</td><td>$1213</td></tr><tr><td>Apple iPod (Original/Scroll) 5 GB, 10 GB</td><td>2001-10-23</td><td>$399.0</td><td>$26374</td></tr><tr><td>Apple iPod 2nd Gen (Touch Wheel) 5, 10, 20 GB</td><td>2002-03-21</td><td>$299.0</td><td>$14766</td></tr><tr><td>Apple iPod 3rd Gen (10/15/30) 10, 15, 30 GB</td><td>2003-04-28</td><td>$299.0</td><td>$25868</td></tr><tr><td>Apple iPod 3rd Gen (10/20/40) 10, 20, 40 GB</td><td>2003-09-08</td><td>$299.0</td><td>$15766</td></tr><tr><td>Apple iPod 3rd Gen (15/20/40) 15, 20, 40 GB</td><td>2004-01-06</td><td>$299.0</td><td>$16223</td></tr><tr><td>Apple iPod 4th Gen (ClickWheel) 20 GB, 40 GB</td><td>2004-07-19</td><td>$299.0</td><td>$11218</td></tr><tr><td>Apple iPod U2 Edition (4th Gen) 20 GB</td><td>2004-10-26</td><td>$349.0</td><td>$8722</td></tr><tr><td>Apple iPod photo (30) 30 GB</td><td>2005-02-23</td><td>$349.0</td><td>$4742</td></tr><tr><td>Apple iPod photo (40/60) 40 GB, 60 GB</td><td>2004-10-26</td><td>$499.0</td><td>$12470</td></tr><tr><td>Apple iPod Color Display 20 GB, 60 GB</td><td>2005-06-28</td><td>$299.0</td><td>$4804</td></tr><tr><td>Apple iPod U2 Edition (Color) 20 GB</td><td>2005-06-28</td><td>$329.0</td><td>$5286</td></tr><tr><td>Apple iPod 5th Gen (with Video) 30 GB, 60 GB</td><td>2005-10-12</td><td>$299.0</td><td>$3639</td></tr><tr><td>Apple iPod U2 Edition 5th Gen 30 GB</td><td>2006-06-06</td><td>$329.0</td><td>$3302</td></tr><tr><td>Apple iPod 5th Gen - Enhanced 30 GB, 80 GB</td><td>2006-09-12</td><td>$249.0</td><td>$2055</td></tr><tr><td>Apple iPod U2 Edition 5th Gen Enhanced 30 GB</td><td>2006-09-12</td><td>$279.0</td><td>$2303</td></tr><tr><td>Apple iPod classic ("Original"/6th Gen) 80 GB, 160 GB</td><td>2007-09-05</td><td>$249.0</td><td>$1091</td></tr><tr><td>Apple iPod classic (Late 2008/7th Gen) 120 GB, 160 GB</td><td>2008-09-09</td><td>$249.0</td><td>$984</td></tr><tr><td>Apple iPod mini 4 GB</td><td>2004-01-06</td><td>$249.0</td><td>$13510</td></tr><tr><td>Apple iPod mini (2nd Gen) 4 GB, 6 GB</td><td>2005-02-23</td><td>$199.0</td><td>$2704</td></tr><tr><td>Apple iPod nano 1 GB, 2 GB, 4 GB</td><td>2005-09-07</td><td>$149.0</td><td>$1835</td></tr><tr><td>Apple iPod nano (2nd Gen) 2 GB, 4 GB, 8 GB</td><td>2006-09-12</td><td>$149.0</td><td>$1229</td></tr><tr><td>Apple iPod nano 2nd Gen (RED) 4 GB, 8 GB</td><td>2006-10-13</td><td>$199.0</td><td>$1590</td></tr><tr><td>Apple iPod nano (3rd Gen/Fat) 4 GB, 8 GB</td><td>2007-09-05</td><td>$149.0</td><td>$653</td></tr><tr><td>Apple iPod nano (4th Gen) 8 GB, 16 GB*</td><td>2008-09-09</td><td>$149.0</td><td>$588</td></tr><tr><td>Apple iPod nano (5th Gen/Camera) 8 GB, 16 GB</td><td>2009-09-09</td><td>$149.0</td><td>$521</td></tr><tr><td>Apple iPod nano (6th Gen/Multitouch) 8 GB, 16 GB</td><td>2010-09-01</td><td>$149.0</td><td>$356</td></tr><tr><td>Apple iPod shuffle (White) 512 MB, 1 GB</td><td>2005-01-11</td><td>$99.0</td><td>$1838</td></tr><tr><td>Apple iPod shuffle 2nd Gen (Silver) 1 GB</td><td>2006-09-12</td><td>$79.0</td><td>$652</td></tr><tr><td>Apple iPod shuffle 2nd Gen (Colors/Early 2007) 1 GB</td><td>2007-01-30</td><td>$79.0</td><td>$553</td></tr><tr><td>Apple iPod shuffle 2nd Gen (Late 2007) 1 GB, 2 GB</td><td>2007-09-05</td><td>$79.0</td><td>$346</td></tr><tr><td>Apple iPod shuffle 2nd Gen (Late 2008) 1 GB, 2 GB</td><td>2008-09-09</td><td>$49.0</td><td>$193</td></tr><tr><td>Apple iPod shuffle 3rd Gen 4 GB</td><td>2009-03-11</td><td>$79.0</td><td>$511</td></tr><tr><td>Apple iPod shuffle 3rd Gen (Colors) 2 GB, 4 GB</td><td>2009-09-09</td><td>$59.0</td><td>$206</td></tr><tr><td>Apple iPod shuffle 4th Gen (Wheel/VoiceOver) 2 GB</td><td>2010-09-01</td><td>$49.0</td><td>$117</td></tr><tr><td>Apple iPod touch (Original) 8, 16, 32 GB</td><td>2007-09-05</td><td>$299.0</td><td>$1310</td></tr><tr><td>Apple iPod touch (2nd Gen) 8, 16, 32 GB</td><td>2008-09-09</td><td>$229.0</td><td>$905</td></tr><tr><td>Apple iPod touch (3rd Gen/8 GB) 8 GB</td><td>2009-09-09</td><td>$199.0</td><td>$697</td></tr><tr><td>Apple iPod touch (3rd Gen/32 &amp; 64 GB) 32, 64 GB</td><td>2009-09-09</td><td>$299.0</td><td>$1047</td></tr><tr><td>Apple iPod touch (4th Gen/FaceTime) 8, 32, 64 GB</td><td>2010-09-01</td><td>$229.0</td><td>$548</td></tr><tr><td>Apple iPod touch (4.5/5th Gen/White) 8, 32, 64 GB</td><td>2011-10-04</td><td>$199.0</td><td>$320</td></tr><tr><td>Apple TV (Original)</td><td>2007-01-09</td><td>$299.0</td><td>$1936</td></tr><tr><td>Apple TV (2nd Generation)</td><td>2010-09-01</td><td>$99.0</td><td>$237</td></tr><tr><td>Apple TV (3rd Generation)</td><td>2012-03-07</td><td>$99.0</td><td>$111</td></tr><tr><td>Apple iPhone (Original/EDGE) 4, 8, 16 GB</td><td>2007-01-09</td><td>$499.0</td><td>$3231</td></tr><tr><td>Apple iPhone 3G 8, 16 GB</td><td>2008-06-09</td><td>$199.0</td><td>$656</td></tr><tr><td>Apple iPhone 3GS 8, 16, 32 GB*</td><td>2009-06-08</td><td>$199.0</td><td>$829</td></tr><tr><td>Apple iPhone 3G (China/No Wi-Fi) 8 GB</td><td>2009-10-30</td><td>$0.0</td><td>$0</td></tr><tr><td>Apple iPhone 3GS (China/No Wi-Fi) 16, 32 GB</td><td>2009-10-30</td><td>$0.0</td><td>$0</td></tr><tr><td>Apple iPhone 4 (GSM) 8, 16, 32 GB</td><td>2010-06-07</td><td>$199.0</td><td>$475</td></tr><tr><td>Apple iPhone 4 (CDMA/Verizon/Sprint) 8, 16, 32 GB</td><td>2011-01-11</td><td>$199.0</td><td>$349</td></tr><tr><td>Apple iPhone 4S 16, 32, 64 GB</td><td>2011-10-04</td><td>$199.0</td><td>$320</td></tr><tr><td>Apple iPad Wi-Fi (Original) 16, 32, 64 GB</td><td>2010-01-27</td><td>$499.0</td><td>$1439</td></tr><tr><td>Apple iPad Wi-Fi/3G/GPS (Original) 16, 32, 64 GB</td><td>2010-01-27</td><td>$629.0</td><td>$1814</td></tr><tr><td>Apple iPad 2 (Wi-Fi Only) 16, 32, 64 GB</td><td>2011-03-02</td><td>$499.0</td><td>$849</td></tr><tr><td>Apple iPad 2 (Wi-Fi/GSM/GPS) 16, 32, 64 GB</td><td>2011-03-02</td><td>$629.0</td><td>$1070</td></tr><tr><td>Apple iPad 2 (Wi-Fi/CDMA/GPS) 16, 32, 64 GB</td><td>2011-03-02</td><td>$629.0</td><td>$1070</td></tr><tr><td>Apple iPad 3rd Gen (Wi-Fi Only) 16, 32, 64 GB</td><td>2012-03-07</td><td>$499.0</td><td>$563</td></tr><tr><td>Apple iPad 3rd Gen (Wi-Fi/4G LTE AT&amp;T/GPS) 16, 32, 64 GB</td><td>2012-03-07</td><td>$629.0</td><td>$710</td></tr><tr><td>Apple iPad 3rd Gen (Wi-Fi/4G LTE Verizon/GPS) 16, 32, 64 GB</td><td>2012-03-07</td><td>$629.0</td><td>$710</td></tr></tbody></table> Starfighter https://conroy.org/starfighter 2009-12-23T00:00:00Z <p>The objective of StarFighter is to fly and blast through asteroids and space debris, making it to the end of the level with a high score. The ship is controlled entirely with the mouse. Lasers are fired by clicking the mouse button. The ship can also launch bombs by holding down the mouse button and releasing it after at least half a second. The longer the mouse button is held down, the further the bomb travels. Bombs are detonated by pressing the space bar.</p> <h2>Features</h2> <ul> <li>animation and UI using OpenGL and GLUT</li> <li>detailed collision detection, optimized using object-aligned bounding boxes/spheres and kd-trees</li> <li>ray tracing technique used to fire lasers/missiles</li> <li>gravity-affected, bounce-able projectile simulation with balls</li> <li>mouse controlled x/y thrust</li> <li>level input file parsing to construct world, using OBJ output of objects designed in Maya</li> </ul> <h2>Libraries Used</h2> <ul> <li>Eigen vector/matrix library</li> <li>kdtree (<a class="wiki_link_ext" href="http://code.google.com/p/kdtree/" rel="nofollow"><a href="http://code.google.com/p/kdtree/">http://code.google.com/p/kdtree/</a></a>)</li> <li>FreeImage for writing images</li> <li>SOIL (Simple OpenGL Image Library) for texture input</li> </ul> <!-- EXCERPT --> <style type="text/css" media="screen"> article #images img { width: 145px; padding: 10px; display: inline-block; } article object{ margin: 5px; } </style> <h2>Group:</h2> <ul> <li>Long Cheng cs184-cy</li> <li>Kyle Conroy cs184-cz</li> <li>Jillian Moore cs184-cx</li> <li>Wei Yeh cs184-df</li> </ul> <p>Hosted on Github <a href="http://github.com/kyleconroy/starfighter">http://github.com/kyleconroy/starfighter</a></p> <h2><del>Binary</del></h2> <p><del>Snow Leopard Only: <a href="/data/starfighter">/data/starfighter</a></del></p> <h2>Video</h2> <iframe width="560" height="315" src="https://www.youtube.com/embed/04mtErvEBG0" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <iframe width="560" height="315" src="https://www.youtube.com/embed/ELPBt9aioNc" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <iframe width="560" height="315" src="https://www.youtube.com/embed/vjVHya7seKE" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <p>Quicktime had some problems. It didn't record the sound effects and created some bad flashing artifacts (seen in the first video)</p> <h2>Images</h2> <div id="images"> <a href="/images/starfighter/image-01.png"> <img src="/images/starfighter/image-01.png" alt="image01" /> </a> <a href="/images/starfighter/image-06.png"> <img src="/images/starfighter/image-06.png" alt="image01" /> </a> <a href="/images/starfighter/image-09.png"> <img src="/images/starfighter/image-09.png" alt="image01" /> </a> <a href="/images/starfighter/image-13.png"> <img src="/images/starfighter/image-13.png" alt="image01" /> </a> <a href="/images/starfighter/image-29.png"> <img src="/images/starfighter/image-29.png" alt="image01" /> </a> <a href="/images/starfighter/image-34.png"> <img src="/images/starfighter/image-34.png" alt="image01" /> </a> <a href="/images/starfighter/image-54.png"> <img src="/images/starfighter/image-54.png" alt="image01" /> </a> </div> <h2>Compilation</h2> <p>Use <code>make</code> resulting executable is named <code>starfighter</code></p> <h2>Usage</h2> <pre style="background-color:#f0f3f3"> ./starfighter -f levelListFile </pre> <h2>Input File Spec</h2> <p>The input file is a level list with pairs</p> <pre style="background-color:#f0f3f3"> level highScore </pre> <p>separated by newlines. &amp;lt;level&amp;gt;.level refers to the level file in the same directory.</p> <p>A level file consists of geometry definitions and model definitions.</p> <h2>Level File Spec</h2> <p>Some entries have options [option1 | option2]</p> <h3>Loading a .obj mesh</h3> <pre> { objgeometry # name unique-name # must be a unique name path path/to/file # relative path bs [box | sphere] # sets the bounding shape, default is box boundingsphere x y z r # explicitly sets the bounding sphere boundingsphere x y z x y z # explicitly sets the bounding box } </pre> <p>A sample level file can be found <a href="http://github.com/derferman/starfighter/blob/master/levels/hittest.level">here</a></p> <h2>Other Credits</h2> <ul> <li>Ship model from sender pinarci, on TurboSquid</li> <ul> <li><a href="http://www.turbosquid.com/3d-models/sendercorp-sender-ma-free/429728" rel="nofollow">http://www.turbosquid.com/3d-models/sendercorp-sender-ma-free/429728</a></li> <li><a href="http://www.turbosquid.com/FullPreview/Index.cfm/ID/433482" rel="nofollow">http://www.turbosquid.com/FullPreview/Index.cfm/ID/433482</a></li> </ul> <li>Asteroid models from Setro on turbo squid</li> <ul><li><a href="http://www.turbosquid.com/3d-models/free-3ds-model-stones/497091" rel="nofollow">http://www.turbosquid.com/3d-models/free-3ds-model-stones/497091</a></li></ul> <li>Sound effects from <a href="http://freesound.org" rel="nofollow">http://freesound.org</a></li> </ul> Bezier Curves And The Utah Teapot https://conroy.org/bezier-curves-and-the-utah-teapot 2009-11-14T00:00:00Z <p>My latest CS184 project is now complete and ready for viewing. We had to take in Bezier patches and use tessellation to create triangles using OpenGL. But I know what you really want to see: videos.</p> <iframe width="480" height="385" src="https://www.youtube.com/embed/XJL0nQYLZ-Y" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> <iframe width="480" height="385" src="https://www.youtube.com/embed/DzizQn51l5A" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> CS184 C++ Raytracer https://conroy.org/cs184-c++-raytracer 2009-10-10T00:00:00Z <p>We were tasked with creating a raytracer. Images are followed by the command line which produced them, as well as the scene file describing the setup. All times were calculated using the linux time program. All images on this page were output using my raytracer.</p> <!-- EXCERPT --> <style> .raytraced { width: 100%; } table { margin: 10px; } table td{ padding: 4px; } table td:first-child{ font-weight: bold; } table.aa td{ } table.aa img{ width: 380px; } pre { display: none; } </style> <h2>Three spheres and a Triangle</h2> <p><img class="raytraced" src="/images/raytracer/spheretri.png" alt="reflection"/></p> <table> <tr> <td>Command:</td> <td>./raytrace scenes/spherestri.scene</td> </tr> <tr> <td>Time:</td> <td>6.030s</td> </tr> <tr> <td>Features:</td> <td>Reflections, Shadows, Phong Shading, Antialiasing</td> </tr> <tr> <td>Comments:</td> <td>This photo was inspired by the Raytracing Implementation Journal. It shows off shadows quite well</td> </tr> <tr> <td>Scene File</td> <td> <a href="/txt/raytracer/spherestri.scene"> spherestri.scene</a> </td> </tr> </table> <h2>Five spheres with Reflection</h2> <p><img class="raytraced" src="/images/raytracer/reflection.png" alt="reflection"/></p> <table> <tr> <td>Command:</td> <td>./raytrace scenes/reflections.scene</td> </tr> <tr> <td>Time:</td> <td>7.714s</td> </tr> <tr> <td>Features:</td> <td>Reflections, Shadows, Phong Shading, Antialiasing</td> </tr> <tr> <td>Comments:</td> <td>This photo was inspired by the Raytracing Implementation Journal</td> </tr> <tr> <td>Scene File</td> <td> <a href="/txt/raytracer/reflections.scene"> reflections.scene</a> </td> </tr> </table> <h2>Three Ellipsoids</h2> <p><img class="raytraced" src="/images/raytracer/mirrors.png" alt="reflection"/></p> <table> <tr> <td>Command:</td> <td>./raytrace scenes/mirrors.scene</td> </tr> <tr> <td>Time:</td> <td>32.797s</td> </tr> <tr> <td>Features:</td> <td>Linear Transformations, Reflections, Shadows</td> </tr> <tr> <td>Comments:</td> <td></td> </tr> <tr> <td>Scene File</td> <td> <a href="/txt/raytracer/mirrors.scene"> mirrors.scene</a> </td> </tr> </table> <h2>Five Ellipsoids</h2> <p><img class="raytraced" src="/images/raytracer/elipse.png" alt="reflection"/></p> <table> <tr> <td>Command:</td> <td>./raytrace scenes/elipse.scene</td> </tr> <tr> <td>Time:</td> <td>11.506s</td> </tr> <tr> <td>Features:</td> <td>Linear Transformations, Reflections, Shadows</td> </tr> <tr> <td>Comments:</td> <td>This photo was inspired by the Raytracing Implementation Journal</td> </tr> <tr> <td>Scene File</td> <td> <a href="/txt/raytracer/elipse.scene"> elipse.scene</a> </td> </tr> </table> <h2>Antialiasing</h2> <table class="aa"> <tr> <td>Resolution</td> <td>Time</td> <td>Image</td> </tr> <tr> <td>1</td> <td>0.354s</td> <td> <img src="/images/raytracer/aa1.png" alt="reflection"/> </td> </tr> <tr> <td>2</td> <td>1.067s</td> <td> <img src="/images/raytracer/aa2.png" alt="reflection"/> </td> </tr> <tr> <td>3</td> <td>2.305s</td> <td> <img src="/images/raytracer/aa3.png" alt="reflection"/> </td> </tr> <tr> <td>4</td> <td>4.008s</td> <td> <img src="/images/raytracer/aa4.png" alt="reflection"/> </td> </tr> <tr> <td>5</td> <td>6.093s</td> <td> <img src="/images/raytracer/aa5.png" alt="reflection"/> </td> </tr> </table> <table> <tr> <td>Command:</td> <td>./raytrace scenes/antialias.scene</td> </tr> <tr> <td>Features:</td> <td>Antialiasing</td> </tr> <tr> <td>Comments:</td> <td> The number of rays sent out per pixel is the resolution value squared. As we can see in the first image, we apply a small amount of jitter to each ray, whih further reduces aliasing. </td> </tr> <tr> <td>Scene File</td> <td> <a href="/txt/raytracer/antialias.scene"> antialias.scene</a> </td> </tr> </table> <h2>Rendering .obj</h2> <p><img class="raytraced" src="/images/raytracer/teapot.png" alt="reflection"/> <table> <tr> <td>Time:</td> <td>5m45.038s</td> </tr> <tr> <td>Command:</td> <td>./raytrace scenes/teapot.scene</td> </tr> <tr> <td>Features:</td> <td>Loading and rendering .obj files</td> </tr> <tr> <td>Comments:</td> <td> My .obj parser does not support vector normals </td> </tr> <tr> <td>Scene File</td> <td> <a href="/txt/raytracer/teapot.scene"> teapot.scene</a> </td> </tr> </table></p> <h2>Angel</h2> <p><img class="raytraced" src="/images/raytracer/angel.png" alt="reflection"/> <table> <tr> <td>Time:</td> <td>66m38.493s</td> </tr> <tr> <td>Command:</td> <td>./raytrace scenes/angel.scene</td> </tr> <tr> <td>Comments:</td> <td> Rendering this file is pretty slow without acceleration structures </td> </tr> <tr> <td>Scene File</td> <td> <a href="/txt/raytracer/angel.scene"> angel.scene</a> </td> </tr> </table></p>