Skip to content

Release candidate - #67

Merged
jwrosewell merged 75 commits into
mainfrom
rc
Nov 12, 2026
Merged

jwrosewell merged 75 commits into
mainfrom
rc

Conversation

@oleksandrlazarenko-pi

@oleksandrlazarenko-pi oleksandrlazarenko-pi commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Releases what has collected on rc since the last release, #39, which merged on 4 November 2026 at 23:05 UTC.

This release moves Python toward one behavior across every 51Degrees language. Each element it changes now follows the published 51Degrees specifications, which in turn follow the RFCs they cite, so that the same input gives the same answer in .NET, Java, Node.js, Python, PHP, Go, Rust and C. The On-premise Engines take the C library release 51degrees-cxx 4.5.105. Each change names the package it is in.

Before you upgrade

  1. Usage Sharing shares less by default. ShareUsage shared every Evidence value, including every cookie, form field and query string parameter. It now shares a cookie only when its name starts 51D_, and a parameter only when its name starts 51D_ or is on query_whitelist. It never shares id.email, id.salt, request-query, request-path or the Cookie header. Pass share_all=True to share everything apart from those five (#50, #61, fiftyone_pipeline_engines_fiftyone).
  2. A usage sharing message starts at 50 entries, where it started at 10. Pass requested_package_size=10 to keep the old size (#103, fiftyone_pipeline_engines_fiftyone).
  3. An element of your own must declare its Evidence. FlowElement.get_evidence_key_filter() accepted every key by default and now accepts none. After process(), flowdata.evidence.get() and get_all() give only the Evidence that an element in the Pipeline accepts, and the rest is in the new Evidence.get_unaccepted(). Override get_evidence_key_filter in your element to name the keys it reads (#63, fiftyone_pipeline_core).
  4. The JSON Builder needs a Sequence Element. JSONBundlerElement raises an Exception with the message "The JSON Builder needs a sequence number. Place a Sequence Element before the JSON Builder in the Pipeline." when there is none. A Pipeline built with PipelineBuilder.build() already has one. A configuration file that lists JSONBundlerElement needs a SequenceElement before it (#76, #101, fiftyone_pipeline_core).
  5. The JSON Builder's output changes in four ways. Every Element Data key and Property key is lower case, apart from javascriptProperties. A WKT value, such as an IP Intelligence area, is written as {"value": "<text>"}, where it was the text alone. A Property whose value is null, and which was not given "no value", has no entry, where it was written as null. It gains a sequence entry that holds session-id and sequence (#77, #82, #85, #70, fiftyone_pipeline_core).
  6. Both On-premise Engines default to the Balanced performance profile, where they defaulted to LowMemory, so an application that left performance_profile unset uses more memory. Pass performance_profile="LowMemory" to keep the old profile. Device Detection also leaves Update Matched User Agent on. Pass update_matched_useragent=False to turn it off (#57, fiftyone_devicedetection_onpremise, #62, fiftyone_ipintelligence_onpremise).
  7. Latitude and Longitude have 3 decimal places. A weighted value gave the stored number in full, such as 18.542741775512695, and now gives 18.543 (#81, fiftyone_ipintelligence_onpremise).
  8. Ip and IpV6 report the one address that was looked up. At most one of them has a value, and an IPv4-mapped address such as ::ffff:8.8.8.8 is reported in Ip as 8.8.8.8. The Engine reads the Data File's client IP keys only, so an address under any other Evidence key is no longer reported (#90, fiftyone_ipintelligence_onpremise).
  9. Settings that were accepted now raise a ValueError when the Pipeline is built. These are a callback protocol other than http or https (#40), a JavaScript object name that the script cannot declare, such as window or document (#73), and an unknown missing translation behavior (#64).
  10. A missing translation behavior given as text now takes effect. A configuration file that named "EmptyString" or "FlowError" was ignored. The option is missing_translation_behavior, and the old name behavior still works with a DeprecationWarning (#64, fiftyone_pipeline_translation).
  11. A 51Did with white space inside it, or with characters from both base64 alphabets, is refused as InvalidBase64. FodId.to_standard_base64 returns None for a text it refuses, so check the result before using it (#56, fiftyone_pipeline_did).
  12. The Sequence Element comes first and writes Element Data. PipelineBuilder.build() puts the Sequence Element before your own elements, where it put it after them, and the Sequence Element writes session-id and sequence as Element Data under sequence as well as the Evidence query.session-id and query.sequence. So Usage Sharing records the sequence after the Sequence Element has added one to it, and FlowData.get_where("type", "int") gives one more entry. get_javascript_elements() gives the JSON Builder and the JavaScript Builder only, and the new get_sequence_elements() gives the Sequence Element (#70, fiftyone_pipeline_core).
  13. Data update log entries change. The data update service writes the messages and levels of the specification's error table, which .NET writes, in place of its own. Most failures are logged at Error where they were Warning, the earlier messages such as "Access denied from ..." are gone, and an answer of 304 is logged at Info. Change anything that matches the old text (#69, #98, fiftyone_pipeline_engines).
  14. _51DEGREES_CLOUD_ENDPOINT replaces FOD_CLOUD_API_URL, which still works in this release. The Cloud Request Engine and the 51Did client read _51DEGREES_CLOUD_ENDPOINT, and read FOD_CLOUD_API_URL only when the new name is unset or empty, with one warning through warnings.warn the first time each uses the old name. Constants.FOD_CLOUD_API_URL and did_client.ENDPOINT_VARIABLE still hold "FOD_CLOUD_API_URL" and are deprecated, and Constants.CLOUD_ENDPOINT and the new did_client.CLOUD_ENDPOINT_VARIABLE hold the new name. The cloud_endpoint setting still comes first. Set _51DEGREES_CLOUD_ENDPOINT to the same value as the old name (#100, #108, fiftyone_pipeline_cloudrequestengine, fiftyone_pipeline_did).
  15. The JSON sent to the browser loses the cloud entry, which held the Cloud Request Engine's Element Data, being the cloud service's whole answer, as the specification asks. Client code that read cloud from the JSON needs to read the Properties of each Engine instead (#91, fiftyone_pipeline_core).

What changes

Web integration and client-side caching

  • fiftyone_pipeline_core.web gains javascript_response() and json_response(), which answer the client-side JavaScript and JSON endpoints of a Flask application with Cache-Control, Vary, Content-Type, Content-Length and an ETag. The examples use them. max_age_seconds sets the max age, which is 1,800 seconds unless given (#59, fiftyone_pipeline_core).
  • The ETag is a weak entity tag, W/"...", and If-None-Match is compared weakly, as RFC 9110 section 8.8.3 and section 13.1.2 ask. A request whose If-None-Match matches gets 304 for GET and HEAD, and 412 for any other method. A response to a request whose processing failed has Cache-Control: private, no-cache and no ETag. The same Evidence gives the same ETag in every process, so a browser's cached response stays valid behind a load balancer (#59, fiftyone_pipeline_core).
  • PipelineBuilder gains the settings add_sequence_element, add_json_builder_element, add_javascript_builder_element and add_set_header_element, each true by default, so each element can be left out on its own. add_javascript_builder and use_setheader_properties keep their meaning (#70, fiftyone_pipeline_core).

JavaScript Builder

  • The JavaScript Builder checks the callback URL before it writes it into the client script. The protocol must be http or https, and the host must be ASCII labels or an IPv6 address in brackets, with no user information, query, fragment or zone identifier, following RFC 3986. A URL from the request that fails is left out of the script and logged (#40, #66, fiftyone_pipeline_core).
  • The session id and the sequence in the script come from the Sequence Element. Without one the script has sequence 1 and an empty session id, whatever the request carried (#94, fiftyone_pipeline_core).
  • The JSON written into the script has <, > and & as \u003c, \u003e and \u0026, so a value cannot end the script early (#70, fiftyone_pipeline_core).

JSON Builder

  • A list of records, such as profiles, is written as a list of objects, and an empty list as []. Both were left out (#85, fiftyone_pipeline_core).
  • The JSON Builder writes delayexecution only for a Property that has a value, as the specification and .NET, Java and Go do (#92, fiftyone_pipeline_core).

Evidence

  • Each element accepts only the Evidence that changes its result, and an element that declares none accepts none (#63, fiftyone_pipeline_core).
  • webevidence() adds each session value as session.<name> Evidence, and a read-only view of the web request as server.http-request (#61, fiftyone_pipeline_core).
  • Evidence keys are compared ignoring letter case, so header.User-Agent and header.user-agent are one key. get_all() gives each key in the letters it was first added with (#95, fiftyone_pipeline_core).

Cloud

  • The Cloud Request Engine sends every Evidence value apart from the reserved names resource, license, licence and values, which it writes itself. The name sent is everything after the first dot of the key, in the key's own letter case. Where two keys give the same name, the query value is sent, then header, cookie, server, fiftyone and location (#79, #99, fiftyone_pipeline_cloudrequestengine).
  • A Cloud Aspect Engine whose Resource Key lacks its product is built with no Properties and logs one warning, where it stopped the Pipeline build (#74, fiftyone_pipeline_cloudrequestengine).
  • A License Key appears in log entries and error messages only as its last 4 characters, with the rest as * (#78, fiftyone_pipeline_core).
  • requested_properties can be given beside a Resource Key, and the Cloud Request Engine sends the list, where it refused it. A name the cloud service does not know is reported with a message that asks you to check its spelling (#89, fiftyone_pipeline_cloudrequestengine).
  • A cloud response that holds a 51Did is never stored in or served from a cache. The new Engine.is_cacheable(data) decides, and is true unless an Engine says otherwise (#83, fiftyone_pipeline_engines, fiftyone_pipeline_cloudrequestengine).

Usage Sharing

  • Usage Sharing accepts no Evidence and reads the web request instead, so a Pipeline that holds it no longer accepts every header (#61, fiftyone_pipeline_engines_fiftyone).
  • The record marks a value with replaced characters escaped="true", where it was replaced="true", and cuts a value at 10,000 characters, where it was 512 (#71, fiftyone_pipeline_engines_fiftyone).
  • After 10 failed sends in 100 seconds, sending pauses, and the pause doubles up to 300 seconds while sends keep failing. A new track_session option tells sessions apart (#53, #50, fiftyone_pipeline_engines_fiftyone).

Data updates

  • Polling waits until the time the current data says new data is expected, and If-Modified-Since is sent as an HTTP date (#69, fiftyone_pipeline_engines).
  • DataFileUpdateService.update_from_memory(data_file, data) refreshes a data file kept in memory from bytes you give it (#69, fiftyone_pipeline_engines).
  • The file system watcher runs on its own file_system_watcher setting, whether automatic updates are on or not, and needs no License Key, as in the published packages (#69, #110, fiftyone_pipeline_engines).
  • UpdateStatus gains AUTO_UPDATE_TEMP_PATH_NOT_SET and AUTO_UPDATE_UNKNOWN_ERROR, numbered 12 and 13 as in .NET. A download that cannot be written to the data file's path ends with AUTO_UPDATE_NEW_FILE_CANT_RENAME, and the Engine keeps the data it has (#98, fiftyone_pipeline_engines).

Device Detection and IP Intelligence

  • A Property is read by the position of its name, fixed when the Engine is built, in place of a lookup by name on every read. A native read of IsMobile by position took 10.1% to 16.2% less time than a read by name in each of the 10 jobs of run 43056841063 (#93, #102, both on-premise packages).
  • An Engine built with no Property names keeps the first Data File's Property order across every reload, so a position names the same Property after a reload (#102, both on-premise packages).
  • A client IP value with a port, or an IPv6 address in brackets, is now read (#90, fiftyone_ipintelligence_onpremise).

C library

  • The C library moves from 51degrees-cxx 4.5.103 to 4.5.105. A thread waits for a free file handle in place of failing when more threads than the Concurrency setting read one Data File, and HighPerformance holds in memory only what the named Properties read (#72, #96, #102, both on-premise packages).

Examples and documentation

  • Every example runs with no settings on the free Data Files, which it downloads the first time. A Property that only the Enterprise Data File holds is shown as needing that file, with a link to ask for a copy (#43).
  • The repository has one LICENSE file at its root, the EUPL 1.2 text, and every source file carries the standard 51Degrees header (#104).

Environment variables

Each name below has a new name that starts _51DEGREES_, which every shell can set. The Cloud Request Engine and the 51Did client read the cloud endpoint, KeyUtils.get_named_key in fiftyone_devicedetection_shared and fiftyone_ipintelligence_shared reads the upper case form of the name it is given, and the examples and tests read the rest. Every old name is still read in this release, but only when its new name is unset or empty. Using an old name is reported once as deprecated, with warnings.warn in the Cloud Request Engine and the 51Did client, with a FutureWarning in KeyUtils.get_named_key, and on the error stream in the examples. The Pipeline settings resource_key, license_key, cloud_endpoint and cloud_request_origin are unchanged (#97, #100, #108).

Old name, deprecated New name
FOD_CLOUD_API_URL, cloud_endpoint, CLOUD_ENDPOINT _51DEGREES_CLOUD_ENDPOINT
cloud_request_origin _51DEGREES_CLOUD_REQUEST_ORIGIN
resource_key, RESOURCE_KEY _51DEGREES_RESOURCE_KEY
license_key, LICENSE_KEY _51DEGREES_LICENSE_KEY
51DEGREES_DD_PATH _51DEGREES_DD_PATH
IPINTELLIGENCEDATAFILE _51DEGREES_IPI_PATH
run_performance_tests _51DEGREES_RUN_PERFORMANCE_TESTS
The upper case form of a name given to KeyUtils.get_named_key The name as given

Fixed

  • Usage Sharing raised AttributeError for every cookie whose name did not start 51d_, once cookie, query_whitelist or header_blacklist was set (#50, fiftyone_pipeline_engines_fiftyone).
  • Evidence.filter() raised RuntimeError: dictionary changed size during iteration when it removed a key that no element accepted (#50, fiftyone_pipeline_core).
  • A session id that held <, & or " made a usage sharing message that did not parse, so the whole message was lost (#71, fiftyone_pipeline_engines_fiftyone).
  • Usage Sharing sent every message to a service that was failing, and its queue had no limit (#53, fiftyone_pipeline_engines_fiftyone).
  • License Keys appeared in full in data update log entries and errors, and in Cloud Request Engine errors (#78).
  • The JavaScript Builder wrote the user information of a refused callback URL to the log, password included (#66, fiftyone_pipeline_core).
  • An Engine restricted to IsMobile refused flowdata.device.ismobile (#93, fiftyone_pipeline_engines).
  • Four getters of the IP Intelligence native configuration freed memory that the configuration still owned, which corrupted the heap (#72, fiftyone_ipintelligence_onpremise).
  • The data update service stopped checking after an answer of 304, a connection failure, an answer with no content-md5 header when verify_md5 was on, content that was not gzip when decompress was on, or a callback that raised (#69, fiftyone_pipeline_engines).
  • If-Modified-Since was not sent as an HTTP date, so a server ignored it (#69, fiftyone_pipeline_engines).
  • Calling PipelineBuilder.build() twice added the Sequence Element, the JSON Builder and the JavaScript Builder twice (#70, fiftyone_pipeline_core).
  • A session id or sequence under a key in other letters, such as Query.Session-Id, was set aside, so the Sequence Element started a new session (#95, fiftyone_pipeline_core).

Issues this closes

Build

Full build 43310906727 is green, with macOS. It ran from 21:30 to 22:08 UTC on 12 November 2026 on tmp/pr-matrix-67rel. All 16 Setting 'PullRequestSha' lines, being Configure and the 15 Build and Test jobs, read c69d00fe75b019018b8791e77689acfdc35e3c6d, which is this pull request's head and the head of rc, so every job built this tree and nothing else. Every job is a success, being Configure, Build and Test on Python 3.10 to 3.14 on each of Ubuntu, Windows and macOS, Compare Performance and Complete. Report Compare Performance was skipped because Compare Performance passed.

Compare Performance passed for all 15. The floor is the average of the last six results less the larger of two standard deviations and 10 per cent of the average. The figures are detections per second.

Job This build Average of the last 6 Standard deviation Floor Change
Ubuntu Python 3.10 39,928 33,456 7,372 18,712 +19.3 per cent
Ubuntu Python 3.11 52,866 35,527 8,795 17,938 +48.8 per cent
Ubuntu Python 3.12 32,934 33,612 3,316 26,981 -2.0 per cent
Ubuntu Python 3.13 60,184 48,123 16,001 16,121 +25.1 per cent
Ubuntu Python 3.14 32,666 33,789 1,175 30,410 -3.3 per cent
Windows Python 3.10 13,294 17,423 4,930 7,562 -23.7 per cent
Windows Python 3.11 15,276 17,807 4,351 9,106 -14.2 per cent
Windows Python 3.12 14,520 16,673 2,206 12,261 -12.9 per cent
Windows Python 3.13 22,649 20,661 4,253 12,155 +9.6 per cent
Windows Python 3.14 24,439 17,534 3,913 9,707 +39.4 per cent
macOS Python 3.10 20,502 24,158 5,400 13,358 -15.1 per cent
macOS Python 3.11 17,144 27,143 5,206 16,731 -36.8 per cent
macOS Python 3.12 31,908 26,630 6,504 13,622 +19.8 per cent
macOS Python 3.13 21,071 24,193 4,957 14,278 -12.9 per cent
macOS Python 3.14 25,962 25,324 5,480 14,364 +2.5 per cent

The changes go both ways, from 36.8 per cent down to 48.8 per cent up, and their median is 2.0 per cent down, so this head is not slower than its history. The lowest is macOS Python 3.11, 1.92 standard deviations below its average and above its floor. That result is inside the gate that Compare Performance applies.

rc is final for this release at c69d00fe75b019018b8791e77689acfdc35e3c6d. The last change was #110, merged at 21:29 UTC on 12 November 2026, which puts the file system watcher back on its own setting. Every draft this section named before is in rc, being #59, #69, #70, #89, #94, #98, #108, #91, #92 and #110, and the release note above describes all of them.

Only the patch number moves.

jwrosewell and others added 26 commits November 3, 2026 21:02
* Point the documentation build at the example folders and package

List the cloud request engine examples and the device detection examples
source folder in EXAMPLE_PATH, so Doxygen finds the files the examples
name. Include each client hints example's template from its own folder,
and read the location package from src/fiftyone_location.

* Keep the cloud web example doc comment unbroken and name its view by folder

* Keep the onpremise web example doc comment unbroken and name its view by folder
…25)

* Point links to the archived repositories at where the code now lives

The examples, readmes and example web pages linked to the archived
Python repositories, and the IP Intelligence readmes linked to the
archived IP Intelligence C repository. Each link now points at the same
file or folder in 51degrees-python, or at the IP Intelligence readme in
51degrees-cxx, so readers land on code that is still maintained.

Links to a file follow the file to its folder in this repository, for
example the Device Detection examples now sit under
device-detection/fiftyone_devicedetection_examples/src. Links that used
the old version/4.5 branch now use main.

The match metrics example closed its Markdown link with a square bracket
instead of a parenthesis. The link now closes with a parenthesis. The
cloud examples readme changes into the cloned folder after cloning, so
its cd line now follows the new clone address.

The history table in the root README still names the earlier
repositories, because their tags and releases stay there. The links to
the data repositories are unchanged, because those repositories are
still current.

* Describe the IP Intelligence performance test gap without old repository names
* Add a temporary workflow that generates the SWIG wrappers for 51degrees-cxx 4.5.102

* Name the SWIG modules in the temporary workflow instead of reading them

* Generate both SWIG wrappers again for 51degrees-cxx 4.5.102

Between 4.5.100 and 4.5.102, 51degrees-cxx removed the two
EngineHash process overloads and the three EngineIpi process
overloads that take required property indexes. The committed
wrappers still called them, so neither on-premise package compiled
against 4.5.102, which the nightly submodule update brings in #26.

This moves the cxx submodule from 4110d681, the 4.5.100 release, to
1bec7b57, the 4.5.102 release, and generates hash_python_wrap.cxx,
DeviceDetectionHashEngineModule.py, ipi_python_wrap.cxx and
IpIntelligenceEngineModule.py again with SWIG 4.4.1 built from its
v4.4.1 tag. Run against 4110d681, the same command gives files byte
for byte the same as the ones committed before, so every difference
here comes from cxx. Both engines call only process(evidence), which
cxx keeps.

* Remove the temporary SWIG wrapper workflow
Co-authored-by: Automation51D <51DCI@51Degrees.com>
…1Did workflow (#58)

* Fail the cloud tests when no resource key is set, and run the Cloud 51Did workflow

Tests that exist to call the live cloud service skipped when no resource
key reached them, and the 51Did live tests also skipped when the answer
lacked what they checked, so a run with no key or with a key lacking a
product reported success without the cloud being called.

- Each test folder that calls the live service has the same live_cloud.py,
  which reads _51DEGREES_RESOURCE_KEY, then _51DEGREES_RESOURCE_KEY_BESPOKE,
  then resource_key, and gives the failure messages. Every skip for a
  missing key in the 51Did, Device Detection cloud, Device Detection
  example, IP Intelligence example and pipeline example tests is now a
  failure naming the variables, the product the key must carry and how to
  see what a key carries.
- Where the 51Did answer lacks an identifier, the failure gives the cloud's
  null reason and the product. A host without the creator context fails.
  Without a licence key the redeem tests check the sealed result redeems as
  unreadable.
- test_did_tier_live.py checks what a Basic key gets, being a 51Did for
  non-marketing and a refusal naming CloudV5FODiD for standard and
  personalized.
- The live tests are marked, the unit steps deselect them through
  PYTEST_ADDOPTS, and the pipeline integration step now runs the 51Did
  live tests. The integration scripts fail when no TestResourceKey is
  passed, and the Device Detection one keeps the cloud package in the run
  when 51Degrees.csv is missing.
- cloud-51did.yml moves to the root .github/workflows, where GitHub reads
  it, with its paths corrected. It fails when no key reaches it, and runs
  the Basic key only through the tier test.

The licence-key tests in the pipeline examples and test_licensekey_live.py
still skip, because CI has no cloud licence key yet (#13).

* Wrap the tox.ini comment about the cloud test keys
…de (#65)

- The IP Intelligence data file update example now names the LowMemory
  profile for the pipeline that downloads and reloads the Enterprise data
  file, rather than taking the engine's default.
- The Device Detection getting started example's comment said it uses the
  low memory profile, but the code sets MaxPerformance. The comment now
  says MaxPerformance, and why it suits the free Lite data file.
- The IP Intelligence performance example's header said worker processes
  share a memory mapped data file under LowMemory, as if that were the
  default. It now says the default MaxPerformance loads the whole file
  into each worker, and that -p LowMemory lets the workers share the
  operating system's file cache.
- The IP Intelligence package readme named the Lite data file
  51Degrees-LiteIpiV41.ipi, which is CI's name for it. The data
  repository's scripts save it as 51Degrees-LiteV41.ipi.
)

The IP Intelligence on-premise specification gives Balanced as the
default performance profile, and the C library's default IP Intelligence
configuration is the Balanced one, but the Python engine defaulted to
LowMemory. The engine now defaults to "Balanced", which its profile
switch already maps to the native Balanced configuration, and the
docstrings in the engine and both builders say so.

Balanced loads some collections into memory and caches others, where
LowMemory reads everything from the data file, so a caller relying on the
default uses more memory. Naming "LowMemory" keeps the previous behaviour.

A new test, on the free ASN data file, compares the collection settings
of the configuration an engine built at its defaults gives the native
code with those of each profile, because the engine keeps no name for the
profile in use.
… as text (#64)

* Accept the missing translation behavior as text, and refuse an unknown one

TranslationEngineBase and Translator stored the behavior they were given
without checking it, and compared it with the MissingTranslationBehavior
members. A configuration file can only give text, so a configuration
naming "EmptyString" or "flow_error" matched no member and a missing
translation kept its original value, with nothing to say the setting had
been ignored.

MissingTranslationBehavior.from_value now returns a member as it is, and
turns text naming a member into that member. It takes the
specification's names (Original, EmptyString, FlowError) and the members'
own names and values, ignoring case and underscores, and raises
ValueError for anything else. The engine and the translator use it, so
an unknown value stops the engine being built. The parameter is still
named behavior.

* Accept the specification's name for the missing translation behavior

The specification names the Translation Engine's options
SourceElementDataKey and MissingTranslationBehavior. Python already took
source_element_data_key, but named the second option behavior.

TranslationEngineBase and Translator now take missing_translation_behavior.
behavior still works as a keyword-only deprecated alias that raises a
DeprecationWarning, and when both are given missing_translation_behavior
wins. Neither given means Original, as before. A value given by position
is taken as missing_translation_behavior, as it was taken as behavior.

The IP Intelligence translation engines now pass every option by its name,
so they need a fiftyone_pipeline_translation that takes the new name. Their
package requires fiftyone_pipeline_translation 4.5.43 or later, the
version the release plan gives this commit.

* Leave the IP Intelligence translation engines as they were on rc

The IP Intelligence translation engines pass the missing translation
behavior by position. A value passed by position is taken as
missing_translation_behavior by the new translation package and as
behavior by the old one, so the engines work with either release and
raise no deprecation warning without any change. Passing it by name
needed a minimum version of fiftyone_pipeline_translation that cannot be
known until the release that carries the new name, so the engines and
their package's requirements are back to their rc content.

* Take None as not given for the missing translation behavior

rc treated None as Original, because the translator took any value that
was not EMPTY_STRING or FLOW_ERROR as Original. Refusing it would stop a
configuration holding null, or code passing an unset option on as None,
from building after a patch release. The repository's other engines use
None to mean not set, so the translation engine and Translator now do the
same. Both parameters default to None, None under either name gives
Original without a warning, and anything else that names no member is
still refused.
…_ name (#97)

* Rename the data file environment variable to _51DEGREES_DD_PATH

A POSIX shell cannot set a variable whose name starts with a digit, so
51DEGREES_DD_PATH could not be exported the way the examples described.
Every name now starts with an underscore, and the old name is not read
anywhere.

A new check reads every file git tracks and fails on a name that starts
with the digit, run by its own workflow on each pull request.

* Fail the environment name check on a fallback name, in any case for a digit-initial name

Each variable has one name with nothing read in its place, so the check
now also fails on a bare RESOURCE_KEY or LICENSE_KEY read or set as an
environment variable, and on a LEGACY_..._ENV_VAR constant, the shape the
examples' fallbacks take. This is the check 51degrees-php uses, with the
forms Python reads an environment variable by.

A digit-initial name is matched in any case, since no tracked file holds
one in lower case. The fallback names are matched with case, because the
lower case resource_key and license_key are still the one name some
examples and tests read.

git lists paths unquoted, so a path holding a non-ASCII character is
still read, and the check says how many paths it read and which it
skipped as not files.

This commit is the check alone, before the fallbacks are removed, so it
fails.

* Fail the environment name check on a bare key name held in a string

The 51Did example reads its keys through env("_51DEGREES_RESOURCE_KEY",
"RESOURCE_KEY"), a fallback the read forms do not see. A string holding
RESOURCE_KEY or LICENSE_KEY and nothing else is now matched as well, and
is only ever such a name.

Still the check alone, before the fallbacks are removed, so it fails on
12 names.

* Read each key from its _51DEGREES_ name alone, with no fallback

Each variable has one name with nothing read in its place.

- The Device Detection and cloud request engine examples read the
  resource key from _51DEGREES_RESOURCE_KEY and the licence key from
  _51DEGREES_LICENSE_KEY, and no longer fall back to resource_key and
  license_key. The LEGACY_ constants go with them.
- The 51Did example reads _51DEGREES_RESOURCE_KEY and
  _51DEGREES_LICENSE_KEY, and no longer RESOURCE_KEY and LICENSE_KEY.
- The live tests' live_cloud.py, in all five copies, no longer tries
  resource_key after the two _51DEGREES_ names, and the 51Did client and
  licence key live tests read _51DEGREES_LICENSE_KEY alone.
- KeyUtils.get_named_key in both shared packages, and the IP
  Intelligence examples' reader, read the name given and no longer its
  upper case form.

common-ci sets only resource_key, so the IP Intelligence and pipeline
integration scripts now set _51DEGREES_RESOURCE_KEY from the test key,
as the Device Detection one already did. The READMEs and AGENTS.md name
the one variable each example reads.

* Name the environment name check's job for both of its checks

* Make the environment name check's matching a function its tests can call

Dot-sourcing the script now defines Find-EnvironmentNameProblem without
reading the repository, and the scan leaves out the script's own tests,
which name every form it fails on. What the check matches is unchanged.

* Test the environment name check, with names in lower case

Windows reads an environment variable name in any case, so a lower case
old name is still the old name there, and the check must fail on it. The
tests give each form the check must find, in upper, lower and mixed case,
and the lines it must leave alone, being the _51DEGREES_ names, the
builder's resource_key setting, a placeholder and a longer name. The
workflow runs them before the check.

This commit is the tests alone, so the lower case fallback forms fail.

* Match environment names in any case, and read every key under its _51DEGREES_ name

Windows reads an environment variable name in any case, so the check now
matches the fallback names in any case, as it already did the
digit-initial ones. A key name in a string is now matched only as a second
name after a _51DEGREES_ one, the shape of a fallback, because the builder
setting is also called resource_key.

Matching in any case finds the names that had no _51DEGREES_ form at all,
so each now reads its _51DEGREES_ name, as 51degrees-node does.

- The location examples and tests, the IP Intelligence examples and the
  cloud request engine's own tests read _51DEGREES_RESOURCE_KEY rather
  than resource_key.
- The two data file update examples read _51DEGREES_LICENSE_KEY rather
  than license_key, and the tests that run the examples without settings
  clear that name.
- The Device Detection integration script sets _51DEGREES_LICENSE_KEY
  from the data file licence key, and the location one sets
  _51DEGREES_RESOURCE_KEY, since common-ci sets only the older names.
- Every tox.ini passes the _51DEGREES_ names through in place of
  resource_key and license_key, which nothing reads now.
- The READMEs, AGENTS.md and example comments name the new variables.
…ber (#101)

* Test the exact JSON Builder error text with no sequence number

* Give the specification's JSON Builder error text with no sequence number

Every language gives the same text when no Sequence Element has produced a sequence number, as the JSON Builder page of the specification states.
)

* Test that IP Intelligence latitude and longitude have three decimal places

* Give IP Intelligence latitude and longitude to three decimal places

The native code gives the coordinates in text and in the WKT of Areas to three
places, and the numbers read through the weighted and single value getters are
rounded to match. The unused sixteen place setting is removed.

* Test that a half rounds away from zero and a coordinate rounding to zero has no sign

* Round a half away from zero and give a coordinate that rounds to zero no sign
… under the name after the first dot, with prefixes ranked as evidence.md lists them (#99)

* Test that the Cloud Request Engine sends evidence the cloud does not list

* Send the evidence no element accepts to the cloud as well

* Test the reserved names with credentials the engine accepts

A resource key with requested properties is refused when the engine is
built, so the reserved name test now sends twice, once with a resource key
and a licence key and no property list, and once with a licence key alone
and the property list it needs.

* Test that every evidence value is sent under the name after the first dot, in its own letter case

The two rc tests that asserted a header and a cookie no element accepts
are left out now assert they are sent, under decision 1. The sent name is
everything after the first dot, as .NET's CloudRequestEngine.AddQueryData
takes it, and keeps the letter case of the evidence key.

* Send each evidence value under the name after the first dot, in its own letter case

The prefix is the text before the first dot, so query.a.license is sent as
a.license rather than dropped as the reserved name license, and the name
keeps the letter case of the evidence key, as .NET's AddQueryData does.
A name already added in other letters is the same name, so precedence
still sends it once.

* Test that the Cloud Request Engine ranks Evidence prefixes in evidence.md's order

* Rank server, fiftyone and location Evidence in evidence.md's order, then other prefixes alphabetically
* Test that usage sharing pauses sending after failed sends

Usage sharing in Python has no back-off. Every package is sent while the
service is failing, and is lost. These tests expect the back-off .NET is
being given, at its default settings of ten failed sends within 100
seconds and a first pause of two seconds:

- after ten failed sends nothing is sent during the pause, and the
  evidence processed during it is all sent once the pause ends and the
  service answers, with no more requests processed;
- closing while paused returns at once and sends nothing.

The test receiver can now answer with another status, and records every
request it gets as well as the data it accepts. The tests fail until the
back-off is added.

* Pause usage sharing sends after repeated failures

Python now has the same back-off as .NET, with the same five settings
and defaults, as constructor arguments.

- After failures_to_enter_recovery sends fail within
  failures_window_seconds, sending pauses for
  initial_recovery_delay_seconds. A send fails when it raises or the
  answer is not 200.
- When a pause ends one send is tried. If it fails the delay is
  multiplied by recovery_multiplier, up to max_recovery_delay_seconds.
  If it succeeds the back-off ends.
- Processing is not affected. Evidence is queued during a pause, and new
  evidence is dropped only when the queue is full. The new
  max_queue_size argument sets the size, by default 1000 or ten times the
  requested package size if that is larger, as in .NET. The queue had no
  limit before, which a pause would have needed.
- One thread sends at a time, as in .NET and Java. It sends until fewer
  than requested_package_size entries are queued. A timer that does not
  keep the process running starts sending when a pause ends, so the queue
  drains without more requests being processed.
- Closing cancels the timer and sends nothing while paused, logging how
  many entries were not sent.

ShareUsageBackOff holds the rules and takes the time from the caller, so
its tests set the time. send_thread now returns whether the service
accepted the message.

* Test the full queue log, the failed answer log, message size, close and the repeat boundary

* Log a full queue and the content of a failed answer, send up to twice the package size, send what is queued on close and share a repeat at the interval

* Build the Devices message in one place, say the message size in the docstring, and give the constants their units

* Guard the failure deque against reading from an empty queue

* Take the message inside the lock so the name is always bound
…ix the README links (#46)

* Show the root logo in every README, add one contributing guide, and fix the README links

Twenty READMEs showed an older 51Degrees wordmark from 51degrees.com rather
than the logo the root README shows, and the fiftyone_pipeline_did package
README had none. Each now shows the root logo as a Markdown image from
raw.githubusercontent.com, using .github/images/logo-160.png, a 160 pixel
copy of .github/images/logo.png, the size the root README shows it at.

The repository had no contributing guide of its own. One CONTRIBUTING.md
at the root now links the 51Degrees contributing guide.

The Developer Documentation link at the top of thirteen READMEs went to the
main page of the product's generated API reference, which has no content,
and the translation package's went to the documentation home page. They
now go to the product's documentation. The dependencies page that four
READMEs linked is gone, and the next sentence in each already links the
tested versions page, so the sentence is removed.

* Link each product's quick start from the READMEs

The documentation links went to section pages of the 51Degrees
documentation, such as _device_detection__index.html, which only list the
pages in the section. They now go to the page in each section that is most
helpful to a developer and the most recently kept up to date. Those are the
Device Detection, IP Intelligence and Reverse Geocoding quick starts, which
show how to choose between the cloud and on-premise and how to start in
each language, and Pipeline Basics for the Pipeline API, which has no quick
start. The campaign tags of each link are unchanged.

* Point each Find out more documentation line at the documentation home
…es (#102)

* Move cxx to the 51degrees-cxx release commit 7c4ac436

7c4ac436 is the second cxx release of 11 November, from 51Degrees/51degrees-cxx#150. It carries 51Degrees/51degrees-cxx#148, which keeps the first load's property order for an engine built with no names. No SWIG interface changes beyond a comment, so the committed wrappers stay as they are.

* Test that an engine built with no names reads by index

An engine built with no names takes the names the native code gives after the first load as its list. Its table holds every one of them, each index names its property after a reload, and a read through the table gives what an engine built with the same names gives. The Device Detection test also times a read by name, a read by index, the typed read and a build of the table.

* Read by index on an engine built with no names

An engine built with no names now builds its index table from the names the native code gives after the first load, through getRequiredProperties. Since 51Degrees/51degrees-cxx#148 those names are every property of the first data set, sorted by name, and a reload does not change them, so the index of each one is fixed in the same way as for an engine built with names.
…imum entries per message (#103)

* Test that closing stops after five seconds and fifty entries start a message

* Bound usage sharing cleanup at five seconds and take fifty as the minimum entries per message

* Write the specification's numbers in the tests rather than reading them from the element
* Make the name check fail on the names that remain, red

The check already failed on a name starting with the digit and on a
RESOURCE_KEY or LICENSE_KEY read behind a _51DEGREES_ one. It now also
fails on FOD_CLOUD_API_URL, cloud_endpoint, IPINTELLIGENCEDATAFILE,
ipintelligencetestkey and run_performance_tests, read or set as an
environment variable or held in a constant to read later.

A pipeline setting carries the same word as an old variable name, so
only the environment forms are matched and a setting written by its key
is left alone, which the tests state.

This commit is the check alone, so it fails over the repository as it
stands and names every place the next commit changes.

* Make the name check fail on cloud_request_origin too

The examples read it for a resource key limited to particular domains,
and it was missed when the other old names were added.

* Read every environment variable by its one _51DEGREES_ name

The cloud endpoint was read under two names, FOD_CLOUD_API_URL by the
cloud request engine and the 51Did client, and cloud_endpoint by the
examples. Both become _51DEGREES_CLOUD_ENDPOINT, which the Rust library
already reads, and the constants that hold the name move with it.

cloud_request_origin becomes _51DEGREES_CLOUD_REQUEST_ORIGIN,
IPINTELLIGENCEDATAFILE becomes _51DEGREES_IPI_PATH, and
run_performance_tests becomes _51DEGREES_RUN_PERFORMANCE_TESTS.

The shared key helper test set a variable called ipintelligencetestkey
as scaffolding rather than as a 51Degrees setting, so it takes
_51DEGREES_TEST_NAMED_KEY rather than the real resource key name, which
the test would otherwise overwrite for whatever ran next.

A pipeline setting and a function parameter carry the same words as
some of these names and are left alone, because only the environment
variable moves.

* Leave a fake resource key in a test fixture alone

The constant clause matched any assignment of an old name, so it also
matched resourceKey = "resource_key" in the cloud request engine's and
the IP Intelligence cloud tests, where the name is the fake key sent on
the wire rather than a variable.

It now matches only a constant whose own name says it holds a variable
name, and the names that are never anything but a variable are matched
wherever they appear. The tests state both.

* Treat cloud_request_origin as a setting as well, and stop the check naming names

cloud_request_origin is a pipeline builder parameter and a setting key
across the device detection and IP Intelligence builders, so it joins the
names matched only where a variable is read, set or named in a constant.

The names matched wherever they appear were written out in the pattern
and in the comment above it, so the check failed on its own source. Each
is now built from two pieces, as the digit-initial start already was.

* Leave the IpIntelligenceDataFile class alone

The names matched wherever they appear were matched in any case, so the
IP Intelligence data file's old name also matched the class name
IpIntelligenceDataFile, which differs from it by case alone. Those four
names now keep their case, and the tests state it.

* Say which group of old names keeps its case

The paragraph about keeping case sat under the paragraph about
RESOURCE_KEY, LICENSE_KEY, cloud_endpoint and cloud_request_origin, which
are matched in any case, so it read as though it described them. The
(?-i:) is on the other group. The paragraph now sits with the group it
describes and each paragraph names the variable that holds its four.

Comment only, and the two behaviours it describes are already pinned by
the Pester cases for the IpIntelligenceDataFile class and for a lower case
read of the shared names.

* See an old name braced as a PowerShell variable

The check misses an old name written as ${env:NAME}, as Env:\NAME or as a
workflow ${{ env.NAME }}, because its prefix wants the dollar and the env
token adjacent. The CI scripts here are PowerShell, where a name is braced
whenever it sits beside other text, so the check cannot see a form the
files it watches really use. The Go, Node, Java and PHP checks all match
the token on its own.

These cases fail until this one does the same.

* Match the env token on its own, so a braced old name is seen

The prefix wanted the dollar sign and the env token next to each other, so
${env:NAME}, Env:\NAME and a workflow ${{ env.NAME }} went past it.
Matching the token on its own, with a following backslash allowed, closes
all three and brings this check into line with the Go, Node, Java and PHP
ones.
… in every file (#104)

* Use the standard licence text once, with its additional terms in NOTICE

The root LICENSE and ip-intelligence/LICENSE held the EUPL v1.2 text and
the two additional terms in Windows-1252, and pipeline/LICENSE held the
51Degrees header form. The root LICENSE now holds the text byte for byte
as 51degrees-cxx has it, NOTICE holds the two additional terms as
51degrees-cxx has them, and the two other copies are removed. Neither
copy sat in a package folder, and every setup.py declares EUPL-1.2.

Nine test files had no licence header and get the standard one. Three
headers had a closing line missing or written in the C comment form, and
now end as the standard header does.

* Keep the additional terms in the file headers rather than in NOTICE

The two additional terms apply where each file's header states them, so
the repository has LICENSE alone, without a NOTICE file. The README's
licence line goes back to what it said before.
* Test that a Cloud Request Engine response with a fodid is not cached

* Do not cache a Cloud Request Engine response that holds a fodid

An engine can say a result must not be stored in its cache. The Cloud Request
Engine says so for a response with a top-level fodid, in any case, which holds
a 51Did created for the one request. Such a response is therefore never served
from the cache either.

* Remove a statement the fodid cache test does not need

* Test that a cached response holding a fodid is not served

The cache tests covered the store half only. Two tests now reach an entry already in the cache, one ordinary and one holding a fodid, through a cache that answers every lookup with the entry it holds.

* Do not serve a cached response that holds a fodid

is_cacheable now takes the result rather than the flowdata, so in_cache can ask it about an entry it has found, as .NET, Node.js and PHP do. An entry the engine may not serve counts as a miss.
* Test that evidence keys ignore letter case

* Treat evidence keys that differ only in letter case as one key, as the specification says

* Test that the elements reading evidence keys ignore letter case

get_all gives each key in the letters it was added with, so the Sequence Element's key filter and the JavaScript Builder's reads are tested with keys in other letters.

* Match evidence keys in lower case where an element reads them

The Sequence Element's key filter and the JavaScript Builder's key filter, session id, sequence and query parameters all matched the keys get_all gives with letter case, so a key added in other letters was missed. Each now matches in lower case, and a name sent on keeps the letters it was added with.
Each test job downloaded the Asn data file again with the data repository's script, outside the asset cache that the run fills once and every test job restores. They are now fetched as assets with the others, restored from the cache in each test job, and copied or linked to where the tests and examples look for them, so no test job downloads a data file. The prefetch in Configure fetches every product's files, because the cache key does not depend on the products a change touches. The notes say CI takes every data file from the assets, and that a new data file needs an entry in common-ci's steps/fetch-assets.ps1. Invoke-ProductStep takes the products to run when it is given them. The Device Detection script's call to the data repository's script downloaded nothing, so it is removed.
…ed time, update from memory and logging (#69)

* Align the data update service to the specification

Send If-Modified-Since as an HTTP date, wait for the data update expected
time before the first poll, add update_from_memory, and log the actions
with the messages and levels in the Logging section of the data updates
specification.

* Treat a data file that is already current as a finished update in the update examples

With If-Modified-Since sent as an HTTP date, the Distributor answers 304 when
the file is current, which the examples reported as a failure.

* Test that no update message holds the licence key, the watcher needs automatic updates, a file data file is written, and content checks carry on

* Register the watched data files in the watcher tests with automatic updates on

* Mask the licence key in every update message, create the watcher only with automatic updates, write update_from_memory's bytes to a data file kept on disk, and carry on without an MD5 header or with content that is not compressed

* Test that a callback that raises does not stop the checks, and a connection failure keeps polling

* Log a callback that raises, so the other callbacks and the next check still run

* Test that the watcher refresh is routed as the polling refresh is

* Route the watcher refresh through the polling refresh

* Test that a callback's error is logged with .NET's message for any other error

DataUpdateService.cs logs an error from an update complete callback with
MessageAutoUpdateUnhandledError at Error level. The test now asserts that
text, so it fails while Python writes its own sentence.

* Test that a callback that raises stops the callbacks after it

data-updates.md at e89679e9 says the callbacks after the one that raised
are not called. On update polling the error is logged with the last
row's message, which is .NET's MessageAutoUpdateUnhandledError, and on a
programmatic check it passes to the caller. The tests cover polling, a
programmatic check and an update from memory, and that polling still
schedules its next check. This commit is the tests alone, so they fail.

* Test that a callback that raises is logged and stops the callbacks after it

data-updates.md at e89679e9 says the callbacks after the one that raised
are not called, and that on update polling and the file watcher the error
is logged with the last row's message, .NET's
MessageAutoUpdateUnhandledError. Passing the error to the caller of a
programmatic check, an update from memory or the update on start-up waits
for next week, so on those routes it is logged in the same way for now.
This commit is the tests alone, so they fail.

* Stop at the first callback that raises, and log it with .NET's message

DataUpdateService.cs wraps the callback loop in one try, so the first
callback that raises stops the rest, and LogUpdateException logs it with
MessageAutoUpdateUnhandledError, "An unhandled error occurred while
checking for automatic updates for engine '{0}'", at Error.
data-updates.md says the same. Python now does both, on every route, and
the next check is still scheduled.

* Attach the error a callback raised to the log entry

The errors table in the data updates specification says an error of any
other kind is logged with the error attached, and that a callback's
error is logged in the same way. __emit wrote the message and bound no
variable, so the error the callback raised was lost. It is now written
after the message, which is what .NET, Java, Node.js and Go each do.

The new test file takes the licence header every other file in the
repository carries.

* Name only the languages that attach a callback's error

The comment on the expected message said .NET, Node.js and Go write the
error the callback raised, and Java as well. Java's data update service
calls each onUpdateComplete in a bare loop with no catch
(DataUpdateServiceDefault.java on its rc, the loops after the update and
after the file watcher's refresh), so a callback that raises there is not
caught and nothing is logged. The comment now names the three that do.

* Test that a callback's error is masked in the log entry

* Mask every licence key in a callback's error before logging it
…and give it Element Data (#70)

* Give each web integration element its own switch and put Sequence first

The Sequence, JSON Builder, JavaScript Builder and Set Headers elements each
have a switch that defaults to on, and add_javascript_builder and
use_setheader_properties work as before. The Sequence Element is the first
element of the pipeline and also writes the session-id and sequence Element
Data, as well as its Evidence.

* Include the Sequence Element in the JSON, declare its properties and say the Lite file has few Properties

The JSON Builder specification requires a top-level entry for each Element
Data and excludes only Set Headers, the Cloud Request Engine and Usage
Sharing, so the JSON Builder no longer skips the Sequence Element. The
Sequence Element declares session-id and sequence as the Sequence Element
specification lists them. The Device Detection example comments now say what
the required examples specification says of the Lite data file.

* Write an empty session id to the Sequence Element Data when the script cannot be given it

The Sequence Element's session id now goes into the JSON that is written into
the script, so an id the script cannot take is empty there, as it is in the
script's own sessionId.

* Expect the incremented sequence in the usage sharing record now that Sequence is first

* Give the native engines evidence as text, as the Sequence Element now adds a number before them

* Read the session id and the sequence independently in the Sequence Element

A sequence with no session id now counts up with a new id. A session id that is
not a non-empty string is absent, a sequence in text must be decimal digits, and
the Evidence and the Element Data are written from the same values. The JSON
written into the script escapes the characters that could end the script.

* Check the escaped script end in the session id test

* Take a sequence of 0 to 2147483646, as the specification says

The Sequence Element took a sequence of 1 to 2147483647 and added one, so
2147483647 gave an output of 2147483648, which leaves a 32 bit integer.
The specification sets the top at 2,147,483,646 so that adding one stays
within one, which is the range 51degrees-node and 51degrees-java take.

* Say the Lite data file has a small number of Properties in the last example too

datafileupdate_console.py still said "limited capabilities", which is
the wording 51degrees-node also replaced in its own update example.

* Say plainly in the docstring what add_set_header_element does
…in every process and none after errors (#59)

* Test the cache headers of the client-side endpoints

* Set the client-side cache headers, with an ETag that is the same in every process and none after errors

* Test that a POST with a matching ETag gets 412, and a GET or HEAD 304

* Answer a POST whose If-None-Match matches with 412, as RFC 9110 section 13.1.2 asks

* Test that the web request value added for usage sharing is not in the ETag

* Leave the web request value out of the ETag

* Test that the client-side ETag is weak and If-None-Match is read as RFC 9110 says

* Send the client-side ETag as a weak entity tag, and match If-None-Match only when it is "*" or a list of entity tags

* Test that the client-side max age can be set

* Let the caller set the client-side max age, which the specification says should be configurable

* Read the session id from the minified script in the ETag test

* Name every header in the evidence key filter in Vary, as the other languages do

The Vary header named only the headers the request carried, so a cache
would not re-fetch when a header the pipeline accepts appeared for the
first time. The web integration specification's cache header detail says
Vary carries all headers in the Pipeline Evidence key filter, and
51degrees-node, 51degrees-php and 51degrees-go all build it from each
element's filter list.

_vary now reads each element's evidence key filter list, and asks a
filter that cannot list its keys about the request's own headers one by
one, which is what PHP does. A key that is not a field name is left out,
and the revalidation headers stay out as before.

Three tests cover it.

* Name Host in the Vary header expectations, which the JavaScript Builder adds

The three new Vary tests expected only the headers their own element
accepts. Run 43175480017 showed Host beside them, which is right, because
the JavaScript Builder's evidence key filter accepts header.host to build
the callback URL the client script posts to, and its filter is not a list
so the request's own headers are asked of it one by one.

* Test that Content-Length is the length of what was sent
…cence key (#89)

* Test that requested properties beside a resource key are sent, with or without a licence key

* Send requested properties beside a resource key, with or without a licence key

The engine refused requested_properties whenever a resource key was set,
because the cloud service was thought to ignore the list. The service
adds the listed properties to those the resource key carries, so the
engine now builds and sends the list as values, and the licence key too
where there is one. It still refuses no key at all, and a licence key
with no resource key or list, which the service refuses on every request.

The message for properties an answer leaves out now says they are not
covered by the keys in use, as a resource key can be the only one.

* Test that a name left out beside a resource key alone is reported as not a property

* Report a name left out beside a resource key alone as not a property, and drop history from two comments

* Say the keys in use in the properties not covered warning, and drop a stale claim about discovery

.NET, Java, Node.js and PHP all word the warning as not covered by the
keys in use, because the licence behind a resource key and a licence key
beside it both decide what comes back. The name not known warning gains
the same closing line and the spelling advice those languages give.

The discovery comment no longer says a licence key beside a resource key
widens the answer, which the corrected Accepted combinations table
replaced.

Tests assert both wordings.

* Test an answer that is not an object, and the message .NET and Java write

The report compares the answer with the list it asked for, and an answer
that parses as JSON but is not an object has nothing to compare, so the
test pins that the request still succeeds and nothing is written.

The warning for a name that is not a property is asserted as .NET and
Java word it, both of them carrying the same sentence.

* Guard the report against an answer that is not an object, and take .NET and Java's wording

An answer that parses as JSON but is not an object has no sections to
look in, so the report returned an AttributeError out of processing and
failed a request that had succeeded. It now leaves such an answer alone,
with the same guard and the same wider catch that is_cacheable uses on
the same text.

The warning for a name that is not a property is now word for word the
sentence .NET and Java both write, so Python's two warnings match theirs
exactly.
#98)

* Align the data update service to the specification

Send If-Modified-Since as an HTTP date, wait for the data update expected
time before the first poll, add update_from_memory, and log the actions
with the messages and levels in the Logging section of the data updates
specification.

* Treat a data file that is already current as a finished update in the update examples

With If-Modified-Since sent as an HTTP date, the Distributor answers 304 when
the file is current, which the examples reported as a failure.

* Test that no update message holds the licence key, the watcher needs automatic updates, a file data file is written, and content checks carry on

* Register the watched data files in the watcher tests with automatic updates on

* Mask the licence key in every update message, create the watcher only with automatic updates, write update_from_memory's bytes to a data file kept on disk, and carry on without an MD5 header or with content that is not compressed

* Test that a callback that raises does not stop the checks, and a connection failure keeps polling

* Log a callback that raises, so the other callbacks and the next check still run

* Test that the watcher refresh is routed as the polling refresh is

* Route the watcher refresh through the polling refresh

* Test the data update messages and levels against .NET

* Align the data update messages and levels to .NET

Each event in the data update service now carries the message and level of
the .NET data update service, which the Errors table of data-updates.md
follows. A failed connection, a 429, a 403, any other status, a failed
integrity check and a failed refresh move from Warning to Error and take
.NET wording. A failed write to the data file becomes a Warning and the
engine is still refreshed from the data in memory.

Every check is reported to the update complete callbacks once it ends. A
watcher event with no newer file reports not needed, and one that arrived
whilst another was being applied reports in progress.

* Test that a callback's error is logged with .NET's message for any other error

DataUpdateService.cs logs an error from an update complete callback with
MessageAutoUpdateUnhandledError at Error level. The test now asserts that
text, so it fails while Python writes its own sentence.

* Test that a callback that raises stops the callbacks after it

data-updates.md at e89679e9 says the callbacks after the one that raised
are not called. On update polling the error is logged with the last
row's message, which is .NET's MessageAutoUpdateUnhandledError, and on a
programmatic check it passes to the caller. The tests cover polling, a
programmatic check and an update from memory, and that polling still
schedules its next check. This commit is the tests alone, so they fail.

* Test that a callback that raises is logged and stops the callbacks after it

data-updates.md at e89679e9 says the callbacks after the one that raised
are not called, and that on update polling and the file watcher the error
is logged with the last row's message, .NET's
MessageAutoUpdateUnhandledError. Passing the error to the caller of a
programmatic check, an update from memory or the update on start-up waits
for next week, so on those routes it is logged in the same way for now.
This commit is the tests alone, so they fail.

* Stop at the first callback that raises, and log it with .NET's message

DataUpdateService.cs wraps the callback loop in one try, so the first
callback that raises stops the rest, and LogUpdateException logs it with
MessageAutoUpdateUnhandledError, "An unhandled error occurred while
checking for automatic updates for engine '{0}'", at Error.
data-updates.md says the same. Python now does both, on every route, and
the next check is still scheduled.

* Test that an error from the errors table is logged as .NET logs it

data-updates.md at e89679e9, lines 476 to 485, says an error from a row at
Error level is logged at Error as "Error during check for data update"
with the error attached, and the error carries the row's text. An error of
any other kind, a callback's included, is logged with the last row's
message, also with the error attached. Python's logger takes a string, so
the error follows the entry text, its cause after it as "Error detail:".
This commit is the tests alone, so they fail.

* Log an error from the errors table as .NET does, with its cause

An error from a row at Error level is now logged as "Error during check
for data update", with the error after it, being the row's text and then
its cause, as .NET logs a DataUpdateException with the exception
attached. Any other error, a callback's included, is logged with the last
row's message and the error after it. Python's logger takes a string, so
the error follows as "Error detail: <cause>", as the other Python
messages put a cause. The cause of a failed connection is masked, as it
can name the URL with its licence key.

* Test that the error entry is written exactly as 51degrees-node writes it

51degrees-node writes an error from a row of the errors table as "Error
during check for data update. Error detail: <row text>", with ". Error
detail: <cause>" after it when there is a cause, and nothing more when
there is none. The entry constant now holds that text, and a new test
writes out two entries in full, one without a cause and one with. This
commit is the tests alone, so they fail.

* Write the error entry as 51degrees-node does

An error from a row of the errors table is now logged as "Error during
check for data update. Error detail: <row text>", followed by ". Error
detail: <cause>" when there is a cause, which is the text 51degrees-node
writes at f471d65d (checkErrorText and withCause in
dataFileUpdateService.js), so the two languages' logs match character for
character.

* Test the start-up, refresh and integrity texts as 51degrees-node writes them

51degrees-node at f471d65d writes "Updating on start-up", as the
data-updates.md table at e89679e9 does, names the one try in a refresh
after a check ("... after 1 tries.") and ends a refresh from memory with a
full stop, and writes the integrity row as the table does, with no space
before "data update". The tests now expect those texts, so they fail.

* Write the start-up, refresh and integrity texts as 51degrees-node does

"Updating on start-up" takes the hyphen the data-updates.md table at
e89679e9 and 51degrees-node write. A failed refresh after a check or a
watched change names its one try ("... after 1 tries."), and one from
update_from_memory ends with a full stop, as the table's two rows and
51degrees-node's refreshDataFile do. The integrity row has no space before
"data update", as the table writes it.

* Test that a failed write ends the update and that an untouched watched file is not reported

A data file kept on disk is read back by its engine, so the tests now use a
data file whose refresh reads the file, as the on-premise engines do. They
cover a download whose write fails, a temporary copy that cannot be written,
an update from memory whose write fails, and a watched file nobody has
written to.

* End the update when the data file cannot be written, and report only the changes the watcher finds

The engine of a data file kept on disk reads that file, so letting a failed
write through reported a success with the old data still in use. Both routes
now end with the failure, under the errors table row for the copy that
replaces the file, or the row for any other error when the temporary copy is
the one that cannot be written, which is what 51degrees-node writes.

The watcher looks every ten seconds where .NET and 51degrees-node are told by
the operating system, so a look that finds the file untouched is not an event
and is not reported. The statuses take the numbers .NET gives them.

* Attach the error a callback raised to the log entry

The errors table in the data updates specification says an error of any
other kind is logged with the error attached, and that a callback's
error is logged in the same way. __emit wrote the message and bound no
variable, so the error the callback raised was lost. It is now written
after the message, which is what .NET, Java, Node.js and Go each do.

The new test file takes the licence header every other file in the
repository carries.

* Name only the languages that attach a callback's error

The comment on the expected message said .NET, Node.js and Go write the
error the callback raised, and Java as well. Java's data update service
calls each onUpdateComplete in a bare loop with no catch
(DataUpdateServiceDefault.java on its rc, the loops after the update and
after the file watcher's refresh), so a callback that raises there is not
caught and nothing is logged. The comment now names the three that do.

* One constant for the unhandled error entry, with the cause written once

The merge of the data update alignment brought a second constant for the
same entry. This branch already built the text with the cause attached, so
appending the cause again made the expectation read it twice. The one
constant carries the whole entry, and the comment saying why the cause is
attached stays with it.

* Test that a callback's error is masked in the log entry

* Mask every licence key in a callback's error before logging it

* Pair a failed temporary copy with the status its message names

The errors table pairs the any other error message with AUTO_UPDATE_UNKNOWN_ERROR, and a temporary copy that cannot be written has no row of its own, so it is that error. The test asserted the copy-over-the-existing-file status instead.

* A failed temporary copy reports the unknown error status

The write failure logged the any other error message and returned the copy-over-the-existing-file status, which the errors table pairs with a different message. Each branch now returns the status its own row gives.
…ender 1 and no session id without one (#94)

* Test that the JavaScript Builder renders sequence 1 and no session id with no Sequence Element

* Render sequence 1 and an empty session id with no Sequence Element, as step 5 says

* Pin both sides of the branch, and say in the older tests where the no Sequence Element case is covered

* Take the session id and the sequence from the Sequence Element

Step 5 of the JavaScript Builder's process reads both values from the
Sequence Element's Element Data, and renders 1 and an empty session id
where there is no Sequence Element. This draft had the second half and
read both values from Evidence, which is the half PHP, .NET and Java
already have.

The Sequence Element now publishes the pair it writes to Evidence as its
own Element Data, and the builder reads it from there. The Evidence
writes stay, because the JSON Builder and the Sequence Element's own
evidence key filter read them.

The case-folded Evidence lookup the builder used for these two keys goes
with them, having no other reader. A key given in other letters is still
read, because Evidence.get folds the key it is given and the Sequence
Element reads through it.
…eprecated, starting with FOD_CLOUD_API_URL (#108)

* Test that the old cloud endpoint variable name is still read

The cloud request engine, the 51Did client and the 51Did creator context
example read the cloud endpoint from _51DEGREES_CLOUD_ENDPOINT first and,
when that is unset or empty, from FOD_CLOUD_API_URL, reporting once that
the old name is deprecated. Constants.FOD_CLOUD_API_URL and
did_client.ENDPOINT_VARIABLE hold "FOD_CLOUD_API_URL" again and warn that
they are deprecated when read.

The environment variable name check leaves the old endpoint name alone,
so its own tests move those lines from the names it finds to the names it
leaves.

Tests only. The code follows in the next commit.

* Read FOD_CLOUD_API_URL when _51DEGREES_CLOUD_ENDPOINT is not set, and bring back the constants that held the old name

The cloud request engine, the 51Did client and the 51Did creator context
example read the cloud endpoint from _51DEGREES_CLOUD_ENDPOINT first.
When that is unset or empty they read FOD_CLOUD_API_URL, the name the
variable had in 4.5.43, and warn once with a FutureWarning that the old
name is deprecated and which name replaces it. When both are set the new
name wins and nothing is reported.

Constants.FOD_CLOUD_API_URL is back in the cloud request engine, holding
"FOD_CLOUD_API_URL", and warns with a DeprecationWarning naming
Constants.CLOUD_ENDPOINT when read. did_client.ENDPOINT_VARIABLE holds
"FOD_CLOUD_API_URL" again and warns in the same way, naming the new
did_client.CLOUD_ENDPOINT_VARIABLE.

The environment variable name check no longer fails on the old endpoint
name, because the name is read again.

* Test that the old 51Did endpoint constant is a plain module attribute

did_client.ENDPOINT_VARIABLE has to be found every way it was before, so
the tests check dir(), the module's dictionary and a star import as well
as a read by name, and that the client reads the old variable through the
constant, as it did in 4.5.43.

Tests only. The code follows in the next commit.

* Make the old 51Did endpoint constant a plain module attribute again

did_client.ENDPOINT_VARIABLE was served from the module's __getattr__ so
that reading it warned, which took it out of the module's dictionary, so
dir() and a star import no longer gave it. It is a plain attribute again,
holding "FOD_CLOUD_API_URL" and marked deprecated in its comment, which
points at CLOUD_ENDPOINT_VARIABLE. The client reads the old variable
through the constant, as it did in 4.5.43.

* Test that every renamed environment variable is still read under its old name

Every environment variable name a package, a test, an example or a CI
script reads is a public name, so a name that changed to its _51DEGREES_
form is still read under the old name when the new one is unset or empty,
with one message that the old name is deprecated.

The tests cover the helper each test folder holds (live_cloud.py), the
three example helpers and the constants they hold, the two Location cloud
examples, the 51Did creator context example's two keys, the IP
Intelligence test data file, and the upper case read in the two key
helpers. The name check's tests say an old name is left alone in a
deprecated read and in the files that may name one.

Tests only. The code follows in the next commit.

* Read every renamed environment variable under its old name when the new one is not set

resource_key, license_key, cloud_endpoint and cloud_request_origin read as
variables, 51DEGREES_DD_PATH, IPINTELLIGENCEDATAFILE and
run_performance_tests are read again by the tests and examples that read
them in 4.5.43, each only when its _51DEGREES_ name is unset or empty, and
each printing once that the old name is deprecated. RESOURCE_KEY and
LICENSE_KEY are read again by the 51Did creator context example.

KeyUtils.get_named_key in fiftyone_devicedetection_shared and
fiftyone_ipintelligence_shared reads the upper case form of a name again
when the name as given is not set, with a FutureWarning.

The constants in the example helpers that held an old name hold it again,
marked deprecated, beside a constant for each new name. Each tox.ini passes
the old names to the tests beside the new ones.

The environment variable name check leaves an old name alone where it is
the argument straight after its _51DEGREES_ name, and in the files that
hold the deprecated constants, their tests and the tox lists.

* Test the upper case reads in the IP Intelligence example helper, a switch read by presence, and the name check file by file

The IP Intelligence example helper read its resource key and its cloud
endpoint under the old names in upper case as well, so a test sets only
RESOURCE_KEY and only CLOUD_ENDPOINT.

A switch such as the one that turns the performance examples on is on
when its variable is set, whatever it holds, so the test helper gains
tests of that under both names.

The name check's tests say a listed file may hold the old names it is
listed with and no others, and that any other name starting with a digit
still fails there.

Tests only. The code follows in the next commit.

* Read the IP Intelligence examples' old names in upper case, keep the performance switch a presence test, and allow each file only its own old names

The IP Intelligence example helper reads RESOURCE_KEY and CLOUD_ENDPOINT
again, after resource_key and cloud_endpoint, as it did in 4.5.43.

The test helper live_cloud.py gains is_set, which says whether a variable
is set under its new name or its old one, whatever it holds. The
performance examples' switch uses it, so setting the variable to an empty
value still turns them on.

The environment variable name check no longer leaves a listed file out
whole. Each file is listed with the old names it may hold, those names
alone are left alone there, and every other old name and every other name
that starts with a digit still fails.

* Test that the engine reads the old endpoint variable through its constant

Constants.FOD_CLOUD_API_URL has to be a plain attribute of its class, so
every way of reading it gives the old name, and the engine has to read the
old variable through it, as it did in 4.5.43, so that code which points
the constant at another variable still decides what is read.

The tests that reset a record of what has been reported no longer create
the attribute when it is missing, so a rename of one would fail the tests.

Tests only. The code follows in the next commit.

* Make the engine's old endpoint constant a plain attribute that the engine reads

Constants.FOD_CLOUD_API_URL was a descriptor that warned when read, and
the engine read the old name from a private copy. So the class dictionary
held an object where 4.5.43 held the name, and pointing the constant at
another variable no longer changed what the engine read. It is a plain
attribute again, holding "FOD_CLOUD_API_URL" and marked deprecated in its
comment, which points at Constants.CLOUD_ENDPOINT, and the engine reads
the old variable through it. The 51Did client already does the same.
#91)

* Test that the JSON Builder leaves out the Cloud Request Engine's data

* Leave the Cloud Request Engine's data out of the JSON, as the specification says
* Test that the JSON Builder writes delayexecution only for a property with a value

* Write delayexecution only for a property with a value, as the specification says
* Test that the file system watcher runs on its own setting

testTheWatcherRunsOnItsOwnSetting registers a data file in three cases,
being the watcher on with automatic updates on, the watcher on with them
off, and the watcher off with them on. A watcher is created, logged and
loads a replaced file exactly when the watcher's own setting is on.

The five watcher tests in FileSystemWatcherTests no longer turn automatic
updates on through register_watched, which existed only to get a watcher,
and testFileSystemWatcherIsLogged no longer sets auto_update.
testNoFileSystemWatcherWithoutAutomaticUpdates, which held the watcher to
the automatic update setting, is removed.

* Run the file system watcher on its own setting

register_data_file creates the watcher when file_system_watcher is on,
whether or not auto_update is, as it did on main. Watching a file
downloads nothing, so it needs no licence key, and the on-premise engines,
which turn auto_update off without a key, keep watching the data file.
Polling, the update on start-up and the licence key handling are
unchanged.
@jwrosewell
jwrosewell marked this pull request as ready for review November 12, 2026 23:38
@jwrosewell
jwrosewell merged commit 6b789d7 into main Nov 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants