https://forgebuddy.com/images/app/menubar-2.pngLaravel News Links
Forge Buddy – a menu bar utility for Laravel Forge
https://forgebuddy.com/images/app/menubar-2.pngLaravel News Links
You Can Get Almost-New Camera Gear for a Fraction of the Cost. Here’s How.
https://cdn.thewirecutter.com/wp-content/media/2023/11/buyingusedcameragear-2048px-mirrorlesscameras-3×2-1.jpg?auto=webp&quality=60&width=630&dpr=2

Let’s be real—camera gear is expensive. And the hit to your wallet can feel most acute as you’re starting to build a system from scratch, when a new entry-level camera costs you north of $500, and it may not even come with a lens. Buying used, however, can get you 90% of the quality and longevity of new gear at 60% of the price. That’s a deal that’s hard to turn down.
Wirecutter: Reviews for the Real World
xTool Laser Screenprinting System
https://theawesomer.com/photos/2023/11/xtool_laser_screenprinting_system_t.jpg
| Pledge
Screenprinting involves coating screens with chemicals, exposing the areas you want to print to light, then washing them to allow ink to flow through. xTool’s screenprinting system uses a laser to expose pre-coated screens for crisp screens in hours, not days. Price shown includes an xTool 5-watt laser. A $200 basic kit is available for other laser engravers.

The Awesomer
Simplifying API Integration with Laravel’s Http Facade
https://hashnode.com/utility/r?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1699585660555%2F4345ca45-14b3-45cd-ac5a-824e7fe8ee60.png%3Fw%3D1200%26h%3D630%26fit%3Dcrop%26crop%3Dentropy%26auto%3Dcompress%2Cformat%26format%3Dwebp%26fm%3Dpng
I’ve been working a lot lately integrating third-party APIs. There are several different approaches to this such as using the third-party provided SDK. However, I feel sticking to Laravel’s Http facade is often a better choice. By using the Http facade, all third-party integrations can have a similar structure, and testing and mocking becomes a lot easier. Also, your application will have fewer dependencies. You won’t have to worry about keeping the SDK up to date or figuring out what to do if the SDK is no longer supported.
In this post, we will explore integrating the Google Books API. I will create a reusable client and request class to make using the API very simple. In future posts, I will go into more detail about testing, mocking, as well as creating API resources.
Let’s get started!
Add Google Books Configuration to Laravel
Now that we have an API key, we can add it to the .env along with the API URL.
GOOGLE_BOOKS_API_URL=https://www.googleapis.com/books/v1
GOOGLE_BOOKS_API_KEY=[API KEY FROM GOOGLE]
For this example, I am storing an API key that I obtained from the Google Cloud console, though it is not needed for the parts of the API we will be accessing. For more advanced API usage, you would need to integrate with Google’s OAuth 2.0 server and create a client ID and secret that could also be stored in the
.envfile. This is beyond the scope of this post.
With the environment variables in place, open the config/services.php file and add a section for Google Books.
'google_books' => [
'base_url' => env('GOOGLE_BOOKS_API_URL'),
'api_key' => env('GOOGLE_BOOKS_API_KEY'),
],
Create an ApiRequest Class
When making requests to the API, I find it easiest to use a simple class to be able to set any request properties I need.
Below is an example of an ApiRequest class that I use to pass in URL information along with the body, headers, and any query parameters. This class can easily be modified or extended to add additional functionality.
<?php
namespace App\Support;
class ApiRequest
{
protected array $headers = [];
protected array $query = [];
protected array $body = [];
public function __construct(protected HttpMethod $method = HttpMethod::GET, protected string $uri = '')
{
}
public function setHeaders(array|string $key, string $value = null): static
{
if (is_array($key)) {
$this->headers = $key;
} else {
$this->headers[$key] = $value;
}
return $this;
}
public function clearHeaders(string $key = null): static
{
if ($key) {
unset($this->headers[$key]);
} else {
$this->headers = [];
}
return $this;
}
public function setQuery(array|string $key, string $value = null): static
{
if (is_array($key)) {
$this->query = $key;
} else {
$this->query[$key] = $value;
}
return $this;
}
public function clearQuery(string $key = null): static
{
if ($key) {
unset($this->query[$key]);
} else {
$this->query = [];
}
return $this;
}
public function setBody(array|string $key, string $value = null): static
{
if (is_array($key)) {
$this->body = $key;
} else {
$this->body[$key] = $value;
}
return $this;
}
public function clearBody(string $key = null): static
{
if ($key) {
unset($this->body[$key]);
} else {
$this->body = [];
}
return $this;
}
public function getHeaders(): array
{
return $this->headers;
}
public function getQuery(): array
{
return $this->query;
}
public function getBody(): array
{
return $this->body;
}
public function getUri(): string
{
if (empty($this->query) || $this->method === HttpMethod::GET) {
return $this->uri;
}
return $this->uri.'?'.http_build_query($this->query);
}
public function getMethod(): HttpMethod
{
return $this->method;
}
public static function get(string $uri = ''): static
{
return new static(HttpMethod::GET, $uri);
}
public static function post(string $uri = ''): static
{
return new static(HttpMethod::POST, $uri);
}
public static function put(string $uri = ''): static
{
return new static(HttpMethod::PUT, $uri);
}
public static function delete(string $uri = ''): static
{
return new static(HttpMethod::DELETE, $uri);
}
}
The class constructor takes an HttpMethod, which is just a simple enum with the various HTTP methods, and a URI.
enum HttpMethod: string
{
case GET = 'get';
case POST = 'post';
case PUT = 'put';
case DELETE = 'delete';
}
There are helper methods to create the request using the HTTP method name and passing a URI. Finally, there are methods to add and clear headers, query parameters, and body data.
Create an API Client
Now that we have the request, we need an API client to send it. This is where we can use the Http facade.
Abstract ApiClient
First, we’ll create an abstract ApiClient class that will be extended by our various APIs.
<?php
namespace App\Support;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
abstract class ApiClient
{
public function send(ApiRequest $request): Response
{
return $this->getBaseRequest()
->withHeaders($request->getHeaders())
->{$request->getMethod()->value}(
$request->getUri(),
$request->getMethod() === HttpMethod::GET
? $request->getQuery()
: $request->getBody()
);
}
protected function getBaseRequest(): PendingRequest
{
$request = Http::acceptJson()
->contentType('application/json')
->throw()
->baseUrl($this->baseUrl());
return $this->authorize($request);
}
protected function authorize(PendingRequest $request): PendingRequest
{
return $request;
}
abstract protected function baseUrl(): string;
}
This class has a getBaseRequest method that sets up some sane defaults using the Http facade to create a PendingRequest. It calls the authorize method which we can override in our Google Books implementation to set our API key.
The baseUrl method is just a simple abstract method that our Google Books class will set to use the Google Books API URL we set earlier.
Finally, the send method is what sends the request to the API. It takes an ApiRequest parameter to build up the request, then returns the response.
GoogleBooksApiClient
With the abstract client created, we can now create a GoogleBooksApiClient to extend it.
<?php
namespace App\Support;
use Illuminate\Http\Client\PendingRequest;
class GoogleBooksApiClient extends ApiClient
{
protected function baseUrl(): string
{
return config('services.google_books.base_url');
}
protected function authorize(PendingRequest $request): PendingRequest
{
return $request->withQueryParameters([
'key' => config('services.google_books.api_key'),
]);
}
}
In this class, we just need to set the base URL and configure the authorization. For the Google Books API, that means passing the API key as a URL parameter and setting an empty Authorization header.
If we had an API that used a bearer authorization, we could have an authorize method like the following:
protected function authorize(PendingRequest $request): PendingRequest
{
return $request->withToken(config(services.someApi.token));
}
The nice part about having this authorize method is the flexibility it offers to support a variety of API authorization methods.
Query Books By Title
Now that we have our ApiRequest class and GoogleBooksApiClient, we can create an action to query books by title. It would look something like this:
<?php
namespace App\Actions;
use App\Support\ApiRequest;
use App\Support\GoogleBooksApiClient;
use Illuminate\Http\Client\Response;
class QueryBooksByTitle
{
public function __invoke(string $title): Response
{
$client = app(GoogleBooksApiClient::class);
$request = ApiRequest::get('volumes')
->setQuery('q', 'intitle:'.$title)
->setQuery('printType', 'books');
return $client->send($request);
}
}
Then, to call the action, if I wanted to find information about the book The Ferryman, which I just read and highly recommend, use the following snippet:
use App\Actions\QueryBooksByTitle;
$response = app(QueryBooksByTitle::class)("The Ferryman");
$response->json();
Bonus: Tests
Below, I added some examples for testing the request and client classes. For the tests, I am using Pest PHP which provides a clean syntax and additional features on top of PHPUnit.
ApiRequest
<?php
use App\Support\ApiRequest;
use App\Support\HttpMethod;
it('sets request data properly', function () {
$request = (new ApiRequest(HttpMethod::GET, '/'))
->setHeaders(['foo' => 'bar'])
->setQuery(['baz' => 'qux'])
->setBody(['quux' => 'quuz']);
expect($request)
->getHeaders()->toBe(['foo' => 'bar'])
->getQuery()->toBe(['baz' => 'qux'])
->getBody()->toBe(['quux' => 'quuz'])
->getMethod()->toBe(HttpMethod::GET)
->getUri()->toBe('/');
});
it('sets request data properly with a key->value', function () {
$request = (new ApiRequest(HttpMethod::GET, '/'))
->setHeaders('foo', 'bar')
->setQuery('baz', 'qux')
->setBody('quux', 'quuz');
expect($request)
->getHeaders()->toBe(['foo' => 'bar'])
->getQuery()->toBe(['baz' => 'qux'])
->getBody()->toBe(['quux' => 'quuz'])
->getMethod()->toBe(HttpMethod::GET)
->getUri()->toBe('/');
});
it('clears request data properly', function () {
$request = (new ApiRequest(HttpMethod::GET, '/'))
->setHeaders(['foo' => 'bar'])
->setQuery(['baz' => 'qux'])
->setBody(['quux' => 'quuz']);
$request->clearHeaders()
->clearQuery()
->clearBody();
expect($request)
->getHeaders()->toBe([])
->getQuery()->toBe([])
->getBody()->toBe([])
->getUri()->toBe('/');
});
it('clears request data properly with a key', function () {
$request = (new ApiRequest(HttpMethod::GET, '/'))
->setHeaders('foo', 'bar')
->setQuery('baz', 'qux')
->setBody('quux', 'quuz');
$request->clearHeaders('foo')
->clearQuery('baz')
->clearBody('quux');
expect($request)
->getHeaders()->toBe([])
->getQuery()->toBe([])
->getBody()->toBe([])
->getUri()->toBe('/');
});
it('creates instance with correct method', function (HttpMethod $method) {
$request = ApiRequest::{$method->value}('/');
expect($request->getMethod())->toBe($method);
})->with([
[HttpMethod::GET],
[HttpMethod::POST],
[HttpMethod::PUT],
[HttpMethod::DELETE],
]);
The ApiRequest tests check that the correct request data is being set and the correct methods are being used.
ApiClient
Testing for the ApiClient will be a little more complex. Since it is an abstract class, we will use an anonymous class in the beforeEach function to create a client to use that extends ApiClient.
Notice, that we also use the Http::fake() method. This creates mocks on the Http facade that we can make assertions against and prevent making API requests in the tests.
<?php
use App\Support\ApiClient;
use App\Support\ApiRequest;
use App\Support\HttpMethod;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;
beforeEach(function () {
Http::fake();
$this->client = new class extends ApiClient
{
protected function baseUrl(): string
{
return 'https://example.com';
}
};
});
it('sends a get request', function () {
$request = ApiRequest::get('foo')
->setHeaders(['X-Foo' => 'Bar'])
->setQuery(['baz' => 'qux']);
$this->client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)
->url()->toBe('https://example.com/foo?baz=qux')
->method()->toBe(HttpMethod::GET->name)
->header('X-Foo')->toBe(['Bar']);
return true;
});
});
it('sends a post request', function () {
$request = ApiRequest::post('foo')
->setBody(['foo' => 'bar'])
->setHeaders(['X-Foo' => 'Bar'])
->setQuery(['baz' => 'qux']);
$this->client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)
->url()->toBe('https://example.com/foo?baz=qux')
->method()->toBe(HttpMethod::POST->name)
->data()->toBe(['foo' => 'bar'])
->header('X-Foo')->toBe(['Bar']);
return true;
});
});
it('sends a put request', function () {
$request = ApiRequest::put('foo')
->setBody(['foo' => 'bar'])
->setHeaders(['X-Foo' => 'Bar'])
->setQuery(['baz' => 'qux']);
$this->client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)
->url()->toBe('https://example.com/foo?baz=qux')
->method()->toBe(HttpMethod::PUT->name)
->data()->toBe(['foo' => 'bar'])
->header('X-Foo')->toBe(['Bar']);
return true;
});
});
it('sends a delete request', function () {
$request = ApiRequest::delete('foo')
->setBody(['foo' => 'bar'])
->setHeaders(['X-Foo' => 'Bar'])
->setQuery(['baz' => 'qux']);
$this->client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)
->url()->toBe('https://example.com/foo?baz=qux')
->method()->toBe(HttpMethod::DELETE->name)
->data()->toBe(['foo' => 'bar'])
->header('X-Foo')->toBe(['Bar']);
return true;
});
});
it('handles authorization', function () {
$client = new class extends ApiClient
{
protected function baseUrl(): string
{
return 'https://example.com';
}
protected function authorize(PendingRequest $request): PendingRequest
{
return $request->withHeaders(['Authorization' => 'Bearer foo']);
}
};
$request = ApiRequest::get('foo');
$client->send($request);
Http::assertSent(static function (Request $request) {
expect($request)->header('Authorization')->toBe(['Bearer foo']);
return true;
});
});
For the tests, we are confirming that the request properties are being set correctly on the various request methods. We also confirm the baseUrl and authorize methods are being called correctly. To make these assertions, we are using the Http::assertSent method which expects a callback with a $request that we can test against. Notice that I am using the PestPHP expectations and then returning true. We could just use a normal comparison and return that, but by using the expectations, we get much cleaner error messages when the tests fail. Read this excellent article for more information.
GoogleBooksApiClientTest
The test for the GoogleBooksApiClient is similar to the ApiClient test where we just want to make sure our custom implementation details are being handled properly, like setting the base URL and adding a query parameter with the API key.
Also, not the config helper in the beforeEach method. By using the helper, we can set test values for the Google Books service config that will be used in each of our tests.
<?php
use App\Support\ApiRequest;
use App\Support\GoogleBooksApiClient;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Request;
beforeEach(function () {
Http::fake();
config([
'services.google_books.base_url' => 'https://example.com',
'services.google_books.api_key' => 'foo',
]);
});
it('sets the base url', function () {
$request = ApiRequest::get('foo');
app(GoogleBooksApiClient::class)->send($request);
Http::assertSent(static function (Request $request) {
expect($request)->url()->toStartWith('https://example.com/foo');
return true;
});
});
it('sets the api key as a query parameter', function () {
$request = ApiRequest::get('foo');
app(GoogleBooksApiClient::class)->send($request);
Http::assertSent(static function (Request $request) {
expect($request)->url()->toContain('key=foo');
return true;
});
});
Summary
In this article, we covered some helpful steps for integrating third-party APIs in Laravel. By using these simple custom classes, along with the Http facade, we can ensure all integrations function similarly, are easier to test, and don’t require any project dependencies. In a later post, I will expand on these integration tips by covering DTOs, testing with mock responses, and using API resources.
Thanks for reading!
Laravel News Links
How to use the Basilisk II System 7 emulator on macOS
https://photos5.appleinsider.com/gallery/56786-116140-lede-xl.jpg
The Basilisk II emulator can be used to run old versions of Mac OS 8 and System 7 on a modern Mac. Here’s how to get started using it.
Before macOS and OS X, there was Mac OS 9, and before that were its precursors: Mac OS 8, and System 7.x.
System 7 was released in 1991 and was the first version of Mac OS to feature color. Prior to that, the Mac was black and white only.
Early Macs used a CPU from Motorola called the 68000, better known simply as the 68K.
Emulating Mac OS 8 and System 7
Today there are a number of emulators you can run on your modern Mac to emulate Mac OS 8 and System 7 running on a vintage 68K Mac. Among these are Basilisk II, Sheepshaver, macintosh.js, and others.
There are also many online Mac OS 9 emulators.
In this article, we’ll cover Basilisk II written by Christian Bauer.
Getting Basilisk II
To download Basilisk II, head over to its page which has links to acquire the executable. There is also a GitHub repo which contains source code written in C++.
Note that the download links are on the Basilisk II forums and can be a bit confusing. The most recent post from the forums page as of this writing reads:
“SDL2 port, 10 August 2023, universal (x86_64 and arm64) from github.com/kanjitalk755/macemu source Universal, will run natively on both Intel and Apple Silicon
Recommended for macOS 10.13 (High Sierra) through 13 (Ventura)
Download: https://www.emaculation.com/basilisk/Ba … 230810.zip”
SDL2 is the Simple DirectMedia Layer – a code library framework that handles graphics, sound, and user input. You can install SDL2 from its website as a macOS framework in /Library/Frameworks, but Basilisk II has a copy of the framework built in.
Basilisk II has been ported to run on macOS, older versions of Windows, most X11-based versions of Unix, and some older vintage operating systems.
Once Basilisk II has been downloaded, you’ll need to do some configuration first. Along with Basilisk II you’ll also need copies of original Macintosh ROMs from a 512K or 1MB classic Mac, Mac II, and the original version of Mac OS you wish to run (System 7.1, 7.5.3, Mac OS 8, or 8.1).
Also from the forums page:
“For a new BasiliskII setup, a BasiliskIIGUI application and a keycodes file are needed”
Keycode files are only needed if you’re not using the US-English keyboard.
To get the original Mac ROM files you’ll need to own one of the mentioned original Macs and dump its ROM to a file. For copyright reasons, we won’t get into how to do that here.
Apple does provide a free download of MacOS 7.5.3, one of the last commercially released versions of System 7, online.
The original Macs had much of their OS and APIs in hardware in ROMs on the motherboards. The OS could be extended later by installing software but all the old Mac OS versions and apps made calls to the ROM APIs to standardize many software functions.
Be sure to read the Basilisk II README file for additional details before you continue. You’ll also want to read the Technical Manual which is linked at the top of the main Basilisk II page.
The Basilisk II GitHub page also has a Mac OS 9 emulator called SheepShaver which emulates later PowerPC CPU-based Macs. Mac OS 9.0, 9.1, and 9.2.1 which ran on PowerPC Macs were the final versions of Apple’s classic OS before Apple released Mac OS X in 2000.
Note that when Apple released new revolutionary Macs in 1998 (iMac, PowerMac G3, and the first iBook), it switched to a new ROM format called “NewWorld” ROMs. Depending on whether you want to emulate a pre-1998 or post-1998 Mac, you’ll need either “Old-world” or “New-World” ROMs.
Macs from 1998 or later also switched to new firmware called Open Firmware. You can boot any 1998 or later classic PowerPC-based Mac into Open Firmware by holding down Command-Option-O-F on the Mac’s keyboard after restarting.
Installing Basilisk II
Basilisk II emulates either a Macintosh Classic, Classic II (the last of the black and white “compact Macs”), or a Macintosh II running MacOS 7.x, 8.0, or 8.1, or the successor to the Mac II line called Quadra. The Macintosh II was Apple’s first color Macintosh and came in a large, wide desktop form factor without a built-in display.
The original Mac II models were, in order:
- Mac II
- Mac IIcx
- Mac IIci
- Mac IIvx
- Mac IIsi
Original Mac II. Note the two 3.5-inch floppy drives at the right, top.
The IIcx and IIvx were short-lived models. By far the most popular of the Mac II models was the Mac Iici which was much more compact and easier to deal with than the original Mac II.
Later Apple replaced the Mac II line with the new Quadra line because it was based on the next evolution of the 68000 – the 68040. These models included the Quadra 700 which looked like the IIci, and two huge desktop tower models: the 900 and 950.
The 700 became one of Apple’s most popular models.
The IIsi was a low-end, more compact version of the Mac IIci with more limited features and only one expansion slot.
Macintosh IIsi – utilizing what Apple called the “Snow White” design language.
For Basilisk II installation instructions, you’ll need to clone the macemu GitHub repository on Christian’s GitHub page, dive into the /macemu/BasiliskII/ folder, and read the file INSTALL.
If you are running on a non-macOS UNIX system, you will need to compile Basilisk II from source on that platform, which is detailed in the INSTALL file.
As mentioned in the INSTALL file:
“The ROM file has to be named “ROM” and put in the same directory as the Basilisk II executable but you can specify a different location for the ROM file with the “rom” option in the preferences file (or with the preferences GUI).”
If you try to run Basilisk II without the ROM file, nothing happens.
According to that page, you’ll need to create folder in your user’s home folder named “BasiliskII”, and put the app, any keyboard keycode files, the ROM file, and other mentioned associated files into it.
You’ll also need the mentioned Basilisk II GUI beta application which is used to set up and configure the emulator.
Note that the GUI config app can only be downloaded from another linked forum post. Current version as of this writing is 0.20.
There is also another utility mentioned from a third party called “Basilisk II Disk Image Chooser” which helps select and configure disk images.
Basilisk II GUI has a popup menu for setting the Mac model, but currently only two models are enabled from the popup: Mac Iici (System 7.x) and Quadra 900 (for Mac OS 8.x).
The Basilisk II GUI also has a Browse button for selecting the ROM file using a standard macOS file Open pane, which allows you to choose a ROM file located in another location.
Select a Mac model, ROM file, and CPU type, then click “Save”.
The first time you run Basilisk II, it will create an invisible prefs file at the root of your user folder named “.basilisk_ii_prefs”. Although this file doesn’t have any file extension, it’s a plain text file which you can open with any text editor to change some of Basilisk II’s settings.
Some important values in the prefs file are:
- displaycolordepth
- ramsize
- fpu (Floating point unit)
- nosound
- rom
- disk
The ‘rom’ value is critical – it points to the Mac ROM file to use. But we’ll get to that in a moment below.
You can also set or add classic 1.44MB floppy disk drives to the prefs file from the GUI or by adding “floppy” keys with paths to the floppy diskette images you want to use.
Original Macs used standard 3.5-inch floppy diskettes designed by Sony which were in a sealed plastic shell with a sliding metal door to expose the disk media when a diskette was inserted into a floppy disk drive. You can make images of these disks and load them into Basilisk II.
They were called “floppies” because the disk media itself was just a thin plastic film with a magnetic recording surface bonded to it. If you removed the outer hard plastic shell, the disk would “flop” over if you picked it up.
A stack of 3.5-inch floppy diskettes.
You will also need to set a disk (volume) to boot from. This is different than setting the ROM file.
The ROM file contains only the Mac OS ROM code. You’ll still need a bootable disk containing the actual Mac OS operating system in order to boot the emulator.
If you haven’t set both, the emulator won’t start. Back in Basilisk II GUI, you can set up your disk volumes in the Volumes tab:
Set disks, volumes, and a shared folder in the Volumes tab.
If you’re not sure what to add here, add the DiskTools_MacOS8.image as stated on the forums setup page.
A better way is to run the third-party Basilisk II Disk Image Chooser app. This app detects if you don’t have any startup disk set in your emulator configuration – and if you don’t it will prompt you with options to set one up:
Set a startup disk in Disk Image Chooser.
You may want to quit Basilisk II GUI first before running Basilisk II Disk Image Chooser, then restart it after you’ve set a startup disk.
If you click the Select button in Basilisk II Disk Image Chooser, you can choose a disk image to use and the utility will set it for you in the emulator’s prefs file.
Once you try to exit Basilisk II Disk Image Chooser, it will ask you if you want to write the prefs file. If you do, it will then prompt whether you want to launch Basilisk II, return to the main menu, or exit.
Select Launch Basilisk II to start the emulator, or Exit to quit Basilisk II Disk Image Chooser so you can go back to Basilisk II GUI to finish configuration.
If you reopen the .basilisk_ii_prefs file, you’ll note several entries have been added. Most notably the ‘rom’ and ‘disk’ entries which point to the ROM file and startup disk to use when the emulator starts.
rom /Volumes/Virtualization/BasiliskII/9779D2C4 – MacII (800k v2).ROM
disk /Volumes/Virtualization/BasiliskII/DiskTools_MacOS8.image
Be sure the startup disk image file you select matches which Mac model (ROM) you’ve chosen to use. Some older Macs can’t run newer versions of the Mac OS, and vice versa.
If you reopen Basilisk II GUI, under the Volumes tab you’ll now see the startup disk image Basilisk II Disk Image Chooser set for you:
Shared files
As the instructions note, the UNIX root under the Volumes tab in Basilisk II GUI can be set to point to a shared folder on your modern Mac’s filesystem – any files you put in the UNIX root folder will appear on the emulated Mac OS desktop as a “UNIX” disk volume.
You can copy files from your modern Mac to the emulated Mac’s desktop by placing them in this folder. Note you need to restart the emulator for any changes to be noticed.
Starting the emulator
Once you’re satisfied with all the settings in Basilisk II GUI, click the Save button, then click Start. This starts the emulator from the startup disk you set.
When you’re done using Basilisk II and want to quit the emulator, select the Shut Down menu item from the Finder’s Special menu in the old (emulated) Mac OS you are running.
Unlike in modern macOS, the Shut Down menu item is on the right side of the menu bar in the old Mac OS under the Special menu.
While Basilisk II is a bit confusing to set up and configure at first, it’s a nice piece of software. Once you get everything installed, it’s easy to use.
Being able to run both System 7.5.3 and Mac OS 8 on modern Macs opens your computer up to a huge library of vintage software used on the Mac in decades past.
AppleInsider News
OpenAI’s GPT Builder: A Game Changer for Exponential Entrepreneurs
https://blog.finxter.com/wp-content/uploads/2023/11/image-35.png
The digital landscape is on the brink of a revolutionary shift, thanks to OpenAI’s latest offering: the GPT Builder.
Anyone can leverage GPT Builder to create personalized AI models and potentially generate significant revenue with the launch of the OpenAI GPT store!
Step 1: Accessing GPT Builder

To begin with, access to GPT Builder is exclusive to ChatGPT Plus subscribers. Once you’re in, navigate to the Explore tab and select “Create a GPT”. This is your gateway to entering the world of custom AI creation.
Step 2: Conversational Drafting

The process starts with a simple conversation within the “Create” tab to create your desired AI model through a casual chat with ChatGPT. This interaction forms the foundation of your custom GPT. Simply describe the behavior you want your GPT to have in natural language. Focus on value creation for the user or fun projects!
Step 3: Configuring Your GPT

Now click the “Configure” tab to adjust and modify your GPT. Here, you can also connect with custom “Actions”, i.e., APIs that call external services. You can also set whether your GPT will allow web browsing or DALL-E Image Generation.
You can also fine-tune your GPT such as renaming, changing the profile photo, and most importantly, infusing the GPT with specific knowledge by uploading relevant data.
Step 4: Bringing Your GPT to Life
Now you can click “Save” and publish your GPT to the world — or just keep it private for now. Make sure to test your GPT properly.
Here’s an example of my first GPT:
Recommended: I Made a Dan Peña GPT with OpenAI and Learned This
The Upcoming GPT Store: A World of Opportunities
The launch of the GPT Store is imminent, signaling a significant opportunity for early adopters. To put this in perspective, consider the revenue generated by platforms like Shopify Apps and the App Store. The potential for the ChatGPT Store is vast and largely untapped.
Here’s the App Store revenue (source):
Similarly, developers made $320 billion from the App Store:
This revenue was shared by roughly 34 million Apple developers (cumulative). In other words, each App Store developer made $320 billion / 34 million = $10,000 on average. If you belong to the top 10% of developers, i.e., you didn’t only create an account but actually submitted an app, you can see why many early App Store developers became millionaires.
Conclusion
OpenAI’s GPT Builder is more than just a tool; it’s a gateway to innovation and entrepreneurship in the AI space.
With the ability to create and customize AI models easily, the possibilities are endless. There are at least as many possible apps as there are in the App Store — probably many more groundbreaking apps!
Be on the right side of change, stay ahead of the curve, and start experimenting with this transformative GPT Builder technology today!
Prompt Engineering with Python and OpenAI

You can check out the whole course on OpenAI Prompt Engineering using Python on the Finxter academy. We cover topics such as:
- Embeddings
- Semantic search
- Web scraping
- Query embeddings
- Movie recommendation
- Sentiment analysis
Academy: Prompt Engineering with Python and OpenAI
Be on the Right Side of Change
Rethinking the Form Factor of the Wind Turbine
https://s3files.core77.com/blog/images/1457511_81_126227_x5sYCJRUD.jpg
Wind turbines are like skyscrapers: We’re just using engineering to make them bigger and bigger, and no one has re-thought the form factor with any concern for economics. Building a 500-foot tower and adding 400-foot blades can generate lots of megawatts, but they’re incredibly expensive to manufacture, difficult to transport, difficult to install, difficult to maintain, and limited in where they can be placed.
A company called Airloom Energy has rethought the form factor. Their clever alternative design for a utility-scale wind-energy-capturing system features "unique geometry [that] generates the same amount of electricity as conventional turbines at a fraction of the cost."

Rather than a tower, Airloom has developed a sort of suspended NASCAR track. This track is mounted to 25-meter (82′) poles, and has a series of vertical 10-meter (33′) blades hanging from it. The blades are not attached directly to the track, but to a cable that runs the perimeter of the track. As the wind blows, the blades start to move around the track. The cable gets pulled through power take-off units that harvest the resultant energy.

Airloom’s system can be built using readily available and conventional materials; no fancy composites are required. They cost 1/10th of a turbine that generates the same amount of energy, and are far easier to transport; broken down, the system fits onto a standard tractor-trailer.
The company also says an Airloom wind farm costs less than 25% of the price of a turbine-based farm, and that the end result is an electricity price of $0.013/kWh—which is about a third of the $0.038/kWh price offered by a turbine wind farm.
Most interestingly, the Airloom system could be set up over actual farmland, or directly next to power lines to decrease transmission distance.
The company has raised seed funding and plans to launch a pilot project in 2025.

Core77
How to use MySQL 8.2 read/write splitting with Connector/Python
https://i0.wp.com/lefred.be/wp-content/uploads/2023/11/Router-ReadWrite.drawio-1.png?resize=917%2C781&ssl=1
As you know, one of the most eagerly waited features was released with MySQL 8.2: the transparent read/write splitting.
In this post, we’ll look at how to use it with MySQL-Connector/Python.
Architecture
To play with our Python program, we will use an InnoDB Cluster.

This is an overview of the cluster in MySQL Shell:
JS > cluster.status()
{
"clusterName": "fred",
"defaultReplicaSet": {
"name": "default",
"primary": "127.0.0.1:3310",
"ssl": "REQUIRED",
"status": "OK",
"statusText": "Cluster is ONLINE and can tolerate up to ONE failure.",
"topology": {
"127.0.0.1:3310": {
"address": "127.0.0.1:3310",
"memberRole": "PRIMARY",
"mode": "R/W",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.2.0"
},
"127.0.0.1:3320": {
"address": "127.0.0.1:3320",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.2.0"
},
"127.0.0.1:3330": {
"address": "127.0.0.1:3330",
"memberRole": "SECONDARY",
"mode": "R/O",
"readReplicas": {},
"replicationLag": "applier_queue_applied",
"role": "HA",
"status": "ONLINE",
"version": "8.2.0"
}
},
"topologyMode": "Single-Primary"
},
"groupInformationSourceMember": "127.0.0.1:3310"
}
JS > cluster.listRouters()
{
"clusterName": "fred",
"routers": {
"dynabook::system": {
"hostname": "dynabook",
"lastCheckIn": "2023-11-09 17:57:59",
"roPort": "6447",
"roXPort": "6449",
"rwPort": "6446",
"rwSplitPort": "6450",
"rwXPort": "6448",
"version": "8.2.0"
}
}
}
MySQL Connector/Python
The Python program uses MySQL-Connector/Python 8.2.0.
This is the initial code:
import mysql.connector
cnx = mysql.connector.connect(user='python',
passowrd='Passw0rd!Python',
host='127.0.0.1',
port='6450')
cursor = cnx.cursor()
query = ("""select member_role, @@port port
from performance_schema.replication_group_members
where member_id=@@server_uuid""")
for (role, port) in cursor:
print("{} - {}".format(role, port))
cursor.close()
cnx.close()
We can already test it:
$ python test_router.py
PRIMARY - 3310
Good, we can connect to the cluster using the read/write splitting port (6540) and execute the query…. oh ?! But why are we reaching the Primary instance ?
Shouldn’t we access a Read/Only instance (one of the Secondaries) ?
autocommit
Connector/Python disables autocommit by default (see MySQLConnection.autocommit Property). And the Read/Write Splitting functionality must have autocommit disabled to work properly.
Add the following code above line 8:
cnx.autocommit = True
Then we can run the program again:
$ python test_router.py
SECONDARY - 3320
$ python test_router.py
SECONDARY - 3330
Great, it works as expected !
query attributes
Now let’s see how to force execution of the query on the Primary node.
The MySQL Router offers the possibility of using a query attribute to force the Read/Write Split decision: router.access_mode.
Add the following line just before executing the query (cursor.execute(query)):
cursor.add_attribute("router.access_mode", "read_write")
Let’s execute it one more time:
$ python test_router.py
PRIMARY - 3310
The accepted values for the router.access_mode are:
- auto
- read_only
- read_write
Test with DML
Let’s try something different, we’re going to insert rows into a table.
We’ll use the following table:
CREATE TABLE `t1` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`port` int DEFAULT NULL,
`role` varchar(15) DEFAULT NULL,
`timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
We’ll use the following python script:
import mysql.connector
cnx = mysql.connector.connect(user='python',
password='Passw0rd!Python',
host='127.0.0.1',
port='6450',
database='test')
cnx.autocommit = True
cursor = cnx.cursor()
for i in range(3):
query = ("""insert into t1 values(0, @@port, (
select member_role
from performance_schema.replication_group_members
where member_id=@@server_uuid), now())""")
cursor.execute(query)
cursor.close()
cnx.close()
for i in range(3):
cnx = mysql.connector.connect(user='python',
password='Passw0rd!Python',
host='127.0.0.1',
port='6450',
database='test')
cnx.autocommit = True
cursor = cnx.cursor()
query = ("""select *, @@port port_read from t1""")
cursor.execute(query)
for (id, port, role, timestamp, port_read) in cursor:
print("{} : {}, {}, {} : read from {}".format(id,
port,
role,
timestamp,
port_read))
cursor.close()
cnx.close()
Let’s execute it :
$ python test_router2.py
1 : 3310, PRIMARY, 2023-11-09 17:44:00 : read from 3330
2 : 3310, PRIMARY, 2023-11-09 17:44:00 : read from 3330
3 : 3310, PRIMARY, 2023-11-09 17:44:00 : read from 3330
1 : 3310, PRIMARY, 2023-11-09 18:44:00 : read from 3320
2 : 3310, PRIMARY, 2023-11-09 18:44:00 : read from 3320
3 : 3310, PRIMARY, 2023-11-09 18:44:00 : read from 3320
1 : 3310, PRIMARY, 2023-11-09 17:44:00 : read from 3330
2 : 3310, PRIMARY, 2023-11-09 17:44:00 : read from 3330
3 : 3310, PRIMARY, 2023-11-09 17:44:00 : read from 3330
We can see that there were no errors and that we wrote to the Primary node and read from all Secondaries.
Be careful if you set the query attribute for router.access_mode to read_only just before writing (line 16), you’ll get an error as writes are not allowed on a secondary:
_mysql_connector.MySQLInterfaceError: The MySQL server is running with the --super-read-only option so it cannot execute this statement
Transactions
Now we’re going to play with transactions. We create a new script that will perform several transactions:
- a read operation in autocommit
- a read operation in a transaction (by default, this is a read/write transaction)
- a read operation in a read only transaction
- a transaction with several inserts and rollback
This is the source of the program:
import mysql.connector
cnx = mysql.connector.connect(user='python',
password='Passw0rd!Python',
host='127.0.0.1',
port='6450',
database='test')
cnx.autocommit = True
cursor = cnx.cursor()
query = ("""select member_role, @@port port
from performance_schema.replication_group_members
where member_id=@@server_uuid""")
cursor.execute(query)
for (role, port) in cursor:
print("{} - {}".format(role, port))
cnx.start_transaction()
query = ("""select member_role, @@port port
from performance_schema.replication_group_members
where member_id=@@server_uuid""")
cursor.execute(query)
for (role, port) in cursor:
print("{} - {}".format(role, port))
cnx.commit()
cnx.start_transaction(readonly=True)
query = ("""select member_role, @@port port
from performance_schema.replication_group_members
where member_id=@@server_uuid""")
cursor.execute(query)
for (role, port) in cursor:
print("{} - {}".format(role, port))
cnx.commit()
cnx.start_transaction()
for i in range(3):
query = ("""insert into t1 values(0, @@port, (
select member_role
from performance_schema.replication_group_members
where member_id=@@server_uuid), now())""")
cursor.execute(query)
cnx.rollback()
cursor.close()
cnx.close()
Let’s execute the script:
$ python test_router3.py
SECONDARY - 3320
PRIMARY - 3310
SECONDARY - 3320
We can see that the first operation (1) reached a secondary instance, the second operation (2), which was a transaction, reached the primary node.
The read-only transaction (3) reached a secondary node.
We didn’t get any errors for the multiple writes that were part of the transaction we rolled back.
Conclusion
We’ve seen how easy it is to use MySQL Connector/Python with MySQL 8.2 Read/Write Splitting for an InnoDB Cluster.
Enjoy using MySQL Read / Write Splitting with MySQL Connector Python !
Planet MySQL
Why This Medical Saw Can’t Cut Your Skin
https://theawesomer.com/photos/2023/11/saw_blade_vs_human_skin_t.jpg
Medical professionals use a special power saw to cut through casts when it’s time to remove them. While the high-speed saw blade slices effortlessly through a hardened cast, it does nothing to your body if it makes contact with your skin. Steve Mould investigates the physics that allows this ingenious device to work without causing bodily harm.
The Awesomer





