Wednesday, November 22, 2023
Fixing software updates by playing with hardware
This is true after all the most recent updates have been applied, regardless of which scanning app is used.
The error is:
"Unable to send data. Check the connection to the scanner and try again. E583-B318"
It reports this error even while visibly communicating with the scanner, with various whirring and clicking as evidence.
Not so helpfully, the error prevents scanning from taking place, after which the scanning app terminates.
With a bit of oddball tinkering, I found a "fix". I'll use the transparency scanner as an example.
If you open the lid of the scanner, and try to scan, you get this error:
"Remove the document mat from the scanner."
But this is not a fatal error. Close the scanner lid, click "ok", and try your scan.
For me (hopefully others) the scanning then works continually, until I unplug the computer from the scanner, or quit the scanning app.
I'll let the reader draw their own conclusions about the level of investment Epson makes to assure software quality ...
Friday, October 20, 2023
gmail broken with arbitrary, invisible, forced short autowrap
... or that's how it looked to me.
Gmail compose took a long input line, which normally would autowrap within whatever box it was viewed, but instead created a hard, short autowrap, in the background, where I could not see it, and did not want it.
Obviously I'd accidental changed a setting. But I scoured the settings, and couldn't see an option that fit the problem.
There's another set of settings, weirdly not referenced or linked in the main settings. These are in the compose window. And they don't apply until the next time you open the compose window.
Those settings are under the three dots to the right of the text tools, and the culprit was "plain text mode".
Now as someone who used email decades before there was email formatting, I was a little irked by the assertion implied by this setting's name.
If it was 'plain text' why not just treat the input the way it will be received? Why create an arbitrarily short autowrap of the input text, which will alway look wrong? This is because it's not previewed, that is, it's not WYSIWYG. It turns a potentially useful option into one that would only be useful for sending emails to very primitive small-screen devices, with no option to use plain text in a way that's under control of the sender.
So, a broken UX in gmail. Which usually is more careful about its features.
Sunday, October 01, 2023
Werkzeug ... and who is responsible for code stability?
Don't you love it when you haven't deployed for a few days, and you change something insignificant, and then your deployed app crashes, because of something far outside your purview?
The python Werkzeug WSGI library was just updated to 3.0. This caused Python Flask 2.2.2 web apps running on Google Cloud's App Engine to automatically update. Which, if you use one of the utilities, url_quote, you get this error:
ImportError: cannot import name 'url_quote' from 'werkzeug.urls'
So, yes, I might have caught this by updating and running it first in my local environment. But Google Cloud could have caught this too, creating a stable environment for incremental deployment.
The fix is to add this line to your requirements.txt file:
werkzeug==2.2.2
This reminds me of the whole unnecessary forced move to Flask, with its mixed bag of improvements and problems. It should be possible to run a webapp of any age (at least with configurations since 2008) in Google App Engine. Why all the unnecessary updating, crashing, and subsequent compulsory code obsolescence? What happened to backwards compatability? If it was still an observed principle, it would be easier now than ever. Why the insistence on forcing programmers to chase after the latest thing?
Saturday, June 17, 2023
Error messages, Google Maps, PinElement, AdvancedMarkerElement, MapId
const bluepin = "/bluepin.png";
...
marker = new google.maps.Marker({position,map,icon: bluepin});
But there's a new set of advanced marker features. The documentation didn't match my use case (my markers are created in a listener), so I started with:const pinBackgroundBlue = new PinElement(
{background: "blue"});
...
marker = new AdvancedMarkerElement(
{map,position: position,
content: pinBackgroundBlue.element});
... and in the console, Javascript told me that pinElement was not found. Fair enough.
In the head element I added an import of the appropriate Google Maps libraries:
<script async defer src="https://maps.googleapis.com/maps/api/js
key=YOUR_GOOGLE_MAPS_KEY
&v=beta
&libraries=visualization,marker
&callback=initMap">
</script>
Now, in the function initMap itself, I didn't change the map declaration.
// Request needed libraries.
const { Map } = await google.maps.importLibrary("maps");
const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary(
"marker"
);
But since this was a listener, javascript gave me an error. The enclosing function wasn't async, so it could not await.
const { Map } = google.maps.importLibrary("maps");
const { AdvancedMarkerElement, PinElement } = google.maps.importLibrary("marker");
const pinBackgroundBlue = new window.google.maps.marker.PinElement(
{background: "blue"});
Now it found PinElement!
But it said "PinElement is not a constructor".
Well ... that's just silly. Of course it is.
So, I read the documentation, watched the Google Maps videos ...
And the only thing that could be missing was something that was new to me:
A "MapId".
This is for elite maps, I suppose, since Google requires that the privilege of a generated MapId is added to your billing account. I don't know what the charge is now, or later.
But if you just want to try it, add the following to the name-value pairs in your map declaration:
mapId: 'DEMO_MAP_ID'
And now PinElement is a constructor! And your custom pin will appear on the map.
Sunday, April 23, 2023
Deleting a Google Cloud Platform domain mapping under Google App Engine
Say you want to free up a domain that you've used on a different project on GCP or GAE.
When you try to use the domain, you get an error like:
error: [domain] is already mapped to a project
To solve this, use the command line (assuming you have the developer tools and so use gcloud regularly to deploy your apps) and do the following:
gcloud auth login
This will open a browser window for you to sign in with your Google account.
Set the project in the gcloud tool to the old project where the domain is currently mapped:
gcloud config set project PROJECT_ID
Replace PROJECT_ID with the actual Project ID of the old project.
List the domain mappings for the old project:
gcloud app domain-mappings list
Locate the domains you want to remove from the list, and then run the following command to delete the domain mapping:
gcloud app domain-mappings delete DOMAIN_NAME
... replacing DOMAIN_NAME with the actual domain you want to remove.
Thursday, April 20, 2023
Simplest Google App Engine Python 3.8 Flask web app with Google Identity
runtime: python38
app_engine_apis: true
env_variables:
CLIENT_ID: 'your client ID copied from your google cloud console'
CLIENT_SECRET: 'your client secret copied from your google cloud console'
SECRET_KEY: "the super secret key you make up"
instance_class: F1
import requests
from flask import Flask, render_template, request, redirect, url_for, session
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import Flow
from google.cloud import datastore
app = Flask(__name__)
SECRET_KEY = os.environ.get("SECRET_KEY", "the super secret key you make up")
app.secret_key = SECRET_KEY
CLIENT_ID = os.environ.get('CLIENT_ID')
CLIENT_SECRET = os.environ.get('CLIENT_SECRET')
REDIRECT_URI = "https://app project name.appspot.com/oauth2callback"
@app.route('/')
def index():
if 'userinfo' in session:
return render_template("home.html", userinfo=session['userinfo'])
return render_template("index.html")
@app.route('/login')
def login():
flow = Flow.from_client_config(
{
"web": {
"client_id": CLIENT_ID,"client_secret": CLIENT_SECRET,"redirect_uris": [REDIRECT_URI],"auth_uri": "https://accounts.google.com/o/oauth2/auth","token_uri": "https://accounts.google.com/o/oauth2/token",
}
},
scopes=[
"https://www.googleapis.com/auth/userinfo.email","https://www.googleapis.com/auth/userinfo.profile","openid",
],
)
flow.redirect_uri = REDIRECT_URI
authorization_url, _ = flow.authorization_url(prompt="consent")
return redirect(authorization_url)
@app.route('/sign_out')
def sign_out():
session.pop('userinfo', None)
return redirect(url_for('index'))
@app.route('/oauth2callback')
flow = Flow.from_client_config(
{"web": {
"client_id": CLIENT_ID,"client_secret": CLIENT_SECRET,"redirect_uris": [REDIRECT_URI],"auth_uri": "https://accounts.google.com/o/oauth2/auth","token_uri": "https://accounts.google.com/o/oauth2/token",
}
},
scopes=[
"https://www.googleapis.com/auth/userinfo.email","https://www.googleapis.com/auth/userinfo.profile","openid",
],
state=request.args.get("state"),)
flow.redirect_uri = REDIRECT_URI
flow.fetch_token(code=request.args.get("code"))
credentials = flow.credentials
userinfo = get_user_info(credentials)
session['userinfo'] = userinfo
return redirect(url_for('index'))
def get_user_info(credentials):
headers = {
"Authorization": f"Bearer {credentials.token}"
}
response = requests.get("https://www.googleapis.com/oauth2/v2/userinfo", headers=headers)
userinfo = response.json()
return userinfo
if __name__ == "__main__":
app.run(debug=True)
templates/home.html
<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>your home page title</title>
</head>
<body>
<h1>Welcome, {{ userinfo['name'] }}</h1>
<h2>Email: {{ userinfo['email'] }}</h2>
<img src="{{ userinfo['picture'] }}" alt="Profile picture" width="100" height="100">
</body>
templates/index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>application title</title>
<script src="https://apis.google.com/js/platform.js" async defer></script>
<meta name="google-signin-client_id" content="{{ CLIENT_ID }}">
</head>
<body>
<h1>Welcome to this web app</h1>
<a href="{{ url_for('login') }}">Sign in with Google</a>
</body>
</html>
Thursday, January 26, 2023
Datastore preservation: migrating Python 2.7 to Python 3 on Google App Engine
Let's say you deploy a production web application on Google App Engine, using a Python 2.7 runtime. That's getting quite old today, and there are increasing numbers of incompatibilities you need to cope with as a result. What to do?
It might seem daunting, and risky, to migrate to the rather different development environment of Google App Engine with the runtime of Python 3.x.
But if done properly, one worry, or risk, can be avoided. The data in datastore. If you have a great deal of it, it will be preserved during this migration, without the complications of an ETL process. If you translate your environment and webapp properly, the data doesn't go anywhere.
See for yourself.
Here are two webapp sequences, for the same simple three-tier application.
The first is a Python 2.7 application.
The second is a Python 3.x application.
It's worth creating this project youself, so you can feel confident in the migration.
After this, when working on the migration of your production application, it's important to take precautions -- such as duplication of the codebase, and copying the datastore instance -- to allow rollback.
But you probably won't need it.
--------------
Sequence 1
A simple three-tier application on Google App Engine with the webapp2 framework and the python 2.7 runtime. We assume you've created your project and enabled billing, in your Google Cloud Console.
create index.html
<html>
<head>
<title><project id></title>
</head>
<body>
<div style="color:white;font-weight:bold;size:20px;">
<project id><br/><br/>
Visits {{visits}}
</div>
</body>
</html>
create <project id>.py
import cgi
import os
import webapp2
from google.appengine.ext.webapp import template
from google.appengine.api import users
from google.appengine.ext import db
class Visit(db.Model):
visitor = db.StringProperty()
timestamp = db.DateTimeProperty(auto_now_add=True)
def store_visit(remote_addr, user_agent):
Visit(visitor='{}: {}'.format(remote_addr, user_agent)).put()
def fetch_visits():
return str(Visit.all().count())
class MainPage(webapp2.RequestHandler):
def get(self):
store_visit(self.request.remote_addr, self.request.user_agent)
visits = fetch_visits();
template_values = { 'visits':visits}
path = os.path.join(os.path.dirname(__file__), 'index.html')
self.response.out.write(template.render(path, template_values))
app = webapp2.WSGIApplication(
[('/', MainPage)],
debug=True)
create app.yaml
runtime: python27
api_version: 1
threadsafe: false
handlers:
- url: /.*
script: <project id>.app
secure: always
redirect_http_response_code: 301
run "python --version"
Python 2.7.4
test your app locally with:
dev_appserver.py --log_level debug .
deploy with:
gcloud app deploy --project <project id>
Visit the live page a few times, and find your datastore entities from Google Cloud Console.
---------------------------------------------
---------------------------------------------
Sequence 2
A simple three-tier application on Google App Engine with the flask framework and the python 3.8 runtime.
On your machine:
virtualenv -p python3.8.2 <project env>
source ./<project env>/bin/activate
pip install gunicorn
pip install flask
pip install google-cloud-datastore
pip list
Create from the list results, include these two versions in a file called "requirements.txt":
Flask==2.2.2
google-cloud-datastore==2.13.2
[if you do this repeatedly, in different local virtual environments, you end up running "pip install -r requirements.txt" often]
create "app.yaml"
runtime: python38
create "main.py"
from datetime import datetime, timezone
import json
import time
import google.auth
from flask import Flask, render_template, request
from google.cloud import datastore
app = Flask(__name__)
ds_client = datastore.Client()
def store_visit(remote_addr, user_agent):
entity = datastore.Entity(key=ds_client.key('Visit'))
entity.update({
'timestamp': datetime.now(timezone.utc),
'visitor': '{}: {}'.format(remote_addr, user_agent),
})
ds_client.put(entity)
def fetch_visits():
'get total visits'
query = ds_client.query(kind="Visit")
return len(list(query.fetch()))
@app.route('/')
def root():
store_visit(request.remote_addr, request.user_agent)
visits = fetch_visits()
# NB: index.html must be in /templates
return render_template('index.html',visits=visits)
if __name__ == '__main__':
app.run()
create "templates/index.html":
<!doctype html>
<html>
<head>
<title><project id></title>
</head>
<body>
<h1><project id></h1>
<p>{{ visits }} visits</p>
</div>
</body>
</html>
set project with:
gcloud config set project <project id>
(Note this needs to be run whenever you switch to working on a different project locally, to switch the datastore your local development environment is connected to, that is, to the project's datastore.)
set credentials
gcloud auth application-default login
(This will launch a browser -- log in, and 'allow.' Note this will connect your local environment with the remote datastore. If you want a local datastore, you need to use the datastore emulator).
run locally with:
gunicorn -b :8080 main:app
deploy with:
gcloud app deploy --project <project id>
deactivate
Check your database on google cloud console. It's still there!