generated from john/python-template
This commit is contained in:
@@ -23,13 +23,38 @@ Use category-driven semantics aligned to canonical V4 policy:
|
|||||||
- `internal`
|
- `internal`
|
||||||
|
|
||||||
Do not invent ad hoc categories in user/API-facing envelopes unless canonical docs are updated.
|
Do not invent ad hoc categories in user/API-facing envelopes unless canonical docs are updated.
|
||||||
Service-layer exceptions must normalize to this category set before crossing service boundaries.
|
|
||||||
|
Runtime/internal categories may be more specific for diagnostics and persistence, but they must map
|
||||||
|
deterministically to the canonical envelope categories through the centralized mapper in
|
||||||
|
`transcription.errors.canonical_error_category`.
|
||||||
|
|
||||||
|
Current internal categories:
|
||||||
|
|
||||||
|
- `validation_error`
|
||||||
|
- `user_input_error`
|
||||||
|
- `not_found_error`
|
||||||
|
- `conflict_error`
|
||||||
|
- `external_provider_error`
|
||||||
|
- `external_timeout_error`
|
||||||
|
- `processing_error`
|
||||||
|
- `infrastructure_transient_error`
|
||||||
|
- `infrastructure_persistent_error`
|
||||||
|
- `internal_unexpected_error`
|
||||||
|
|
||||||
|
Required internal -> canonical mapping:
|
||||||
|
|
||||||
|
- `validation_error`, `user_input_error` -> `validation`
|
||||||
|
- `not_found_error` -> `not_found`
|
||||||
|
- `conflict_error` -> `conflict`
|
||||||
|
- `external_provider_error` -> `external`
|
||||||
|
- `external_timeout_error`, `infrastructure_transient_error` -> `timeout`
|
||||||
|
- `processing_error`, `infrastructure_persistent_error`, `internal_unexpected_error` -> `internal`
|
||||||
|
|
||||||
## Translation Boundaries
|
## Translation Boundaries
|
||||||
|
|
||||||
- **Provider/adapters:** raise provider/domain exceptions; do not emit UI text.
|
- **Provider/adapters:** raise provider/domain exceptions; do not emit UI text.
|
||||||
- **Services:** map raw exceptions into domain categories and preserve causal chain (`raise ... from ...`).
|
- **Services:** map raw exceptions into internal categories and preserve causal chain (`raise ... from ...`).
|
||||||
- **UI/API:** emit user-safe, actionable messages based on category + operation context.
|
- **UI/API:** map internal category -> canonical envelope category and emit user-safe, actionable messages.
|
||||||
|
|
||||||
## Retry Rules
|
## Retry Rules
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,46 @@ This policy defines active V4 error taxonomy, translation boundaries, and retry
|
|||||||
| `timeout` | Provider call exceeded configured bound | worker/provider client timeout | Retry path and bounded messaging |
|
| `timeout` | Provider call exceeded configured bound | worker/provider client timeout | Retry path and bounded messaging |
|
||||||
| `internal` | Unexpected local failure | unhandled service/runtime faults | Safe generic message + diagnostics capture |
|
| `internal` | Unexpected local failure | unhandled service/runtime faults | Safe generic message + diagnostics capture |
|
||||||
|
|
||||||
|
## Runtime Taxonomy and Canonical Mapping
|
||||||
|
|
||||||
|
Runtime code uses a richer internal taxonomy for diagnostics and persisted evidence, then maps that
|
||||||
|
taxonomy to the six canonical categories at the API/UI envelope boundary.
|
||||||
|
|
||||||
|
### Internal runtime categories
|
||||||
|
|
||||||
|
- `validation_error`
|
||||||
|
- `user_input_error`
|
||||||
|
- `not_found_error`
|
||||||
|
- `conflict_error`
|
||||||
|
- `external_provider_error`
|
||||||
|
- `external_timeout_error`
|
||||||
|
- `processing_error`
|
||||||
|
- `infrastructure_transient_error`
|
||||||
|
- `infrastructure_persistent_error`
|
||||||
|
- `internal_unexpected_error`
|
||||||
|
|
||||||
|
### Internal -> Canonical mapping
|
||||||
|
|
||||||
|
| Internal category | Canonical envelope category |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `validation_error` | `validation` |
|
||||||
|
| `user_input_error` | `validation` |
|
||||||
|
| `not_found_error` | `not_found` |
|
||||||
|
| `conflict_error` | `conflict` |
|
||||||
|
| `external_provider_error` | `external` |
|
||||||
|
| `external_timeout_error` | `timeout` |
|
||||||
|
| `infrastructure_transient_error` | `timeout` |
|
||||||
|
| `processing_error` | `internal` |
|
||||||
|
| `infrastructure_persistent_error` | `internal` |
|
||||||
|
| `internal_unexpected_error` | `internal` |
|
||||||
|
|
||||||
|
`ExecutionAttempt.error_category` stores the internal category value so diagnostics remain specific.
|
||||||
|
|
||||||
## Translation Boundaries
|
## Translation Boundaries
|
||||||
|
|
||||||
- **Provider layer:** raise provider-scoped exceptions with provider context; do not emit UI text.
|
- **Provider layer:** raise provider-scoped exceptions with provider context; do not emit UI text.
|
||||||
- **Service layer:** map raw exceptions into domain-aware categories and preserve causal chain.
|
- **Service layer:** map raw exceptions into internal categories and preserve causal chain.
|
||||||
- **UI/API layer:** convert category to user-safe message with contextual action guidance.
|
- **UI/API layer:** convert internal categories to canonical categories using the centralized mapping.
|
||||||
|
|
||||||
## Decision Context
|
## Decision Context
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ _STATUS_BY_CATEGORY: dict[ErrorCategory, int] = {
|
|||||||
ErrorCategory.NOT_FOUND: 404,
|
ErrorCategory.NOT_FOUND: 404,
|
||||||
ErrorCategory.CONFLICT: 409,
|
ErrorCategory.CONFLICT: 409,
|
||||||
ErrorCategory.EXTERNAL_PROVIDER: 503,
|
ErrorCategory.EXTERNAL_PROVIDER: 503,
|
||||||
|
ErrorCategory.EXTERNAL_TIMEOUT: 503,
|
||||||
ErrorCategory.INFRA_TRANSIENT: 503,
|
ErrorCategory.INFRA_TRANSIENT: 503,
|
||||||
|
ErrorCategory.PROCESSING: 500,
|
||||||
ErrorCategory.INFRA_PERSISTENT: 500,
|
ErrorCategory.INFRA_PERSISTENT: 500,
|
||||||
ErrorCategory.INTERNAL_UNEXPECTED: 500,
|
ErrorCategory.INTERNAL_UNEXPECTED: 500,
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-13
@@ -17,6 +17,7 @@ class ErrorCategory(StrEnum):
|
|||||||
NOT_FOUND = "not_found_error"
|
NOT_FOUND = "not_found_error"
|
||||||
CONFLICT = "conflict_error"
|
CONFLICT = "conflict_error"
|
||||||
EXTERNAL_PROVIDER = "external_provider_error"
|
EXTERNAL_PROVIDER = "external_provider_error"
|
||||||
|
EXTERNAL_TIMEOUT = "external_timeout_error"
|
||||||
PROCESSING = "processing_error"
|
PROCESSING = "processing_error"
|
||||||
INFRA_TRANSIENT = "infrastructure_transient_error"
|
INFRA_TRANSIENT = "infrastructure_transient_error"
|
||||||
INFRA_PERSISTENT = "infrastructure_persistent_error"
|
INFRA_PERSISTENT = "infrastructure_persistent_error"
|
||||||
@@ -61,19 +62,19 @@ class ErrorEnvelope:
|
|||||||
|
|
||||||
def canonical_error_category(error: AppError) -> str:
|
def canonical_error_category(error: AppError) -> str:
|
||||||
"""Map internal categories to canonical API/UI envelope categories."""
|
"""Map internal categories to canonical API/UI envelope categories."""
|
||||||
match error.category:
|
mapping: dict[ErrorCategory, str] = {
|
||||||
case ErrorCategory.VALIDATION | ErrorCategory.USER_INPUT:
|
ErrorCategory.VALIDATION: "validation",
|
||||||
return "validation"
|
ErrorCategory.USER_INPUT: "validation",
|
||||||
case ErrorCategory.NOT_FOUND:
|
ErrorCategory.NOT_FOUND: "not_found",
|
||||||
return "not_found"
|
ErrorCategory.CONFLICT: "conflict",
|
||||||
case ErrorCategory.CONFLICT:
|
ErrorCategory.EXTERNAL_PROVIDER: "external",
|
||||||
return "conflict"
|
ErrorCategory.EXTERNAL_TIMEOUT: "timeout",
|
||||||
case ErrorCategory.EXTERNAL_PROVIDER:
|
ErrorCategory.INFRA_TRANSIENT: "timeout",
|
||||||
return "external"
|
ErrorCategory.PROCESSING: "internal",
|
||||||
case ErrorCategory.INFRA_TRANSIENT:
|
ErrorCategory.INFRA_PERSISTENT: "internal",
|
||||||
return "timeout"
|
ErrorCategory.INTERNAL_UNEXPECTED: "internal",
|
||||||
case _:
|
}
|
||||||
return "internal"
|
return mapping.get(error.category, "internal")
|
||||||
|
|
||||||
|
|
||||||
def build_error_envelope(error: AppError) -> ErrorEnvelope:
|
def build_error_envelope(error: AppError) -> ErrorEnvelope:
|
||||||
|
|||||||
@@ -122,9 +122,8 @@ async def create_document_job(
|
|||||||
_best_effort_delete(stored_path)
|
_best_effort_delete(stored_path)
|
||||||
raise SourceStorageError(
|
raise SourceStorageError(
|
||||||
"Failed to create Document, Source, and Job records",
|
"Failed to create Document, Source, and Job records",
|
||||||
category=ErrorCategory.INFRA_TRANSIENT,
|
category=ErrorCategory.INFRA_PERSISTENT,
|
||||||
suggestion="Retry upload. If this keeps happening, verify database availability.",
|
suggestion="Retry upload. If this keeps happening, verify database availability.",
|
||||||
retriable=True,
|
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
logger.info("Created document job document_id=%s job_id=%s", document.id, job.id)
|
logger.info("Created document job document_id=%s job_id=%s", document.id, job.id)
|
||||||
@@ -200,9 +199,8 @@ async def create_job_for_document(
|
|||||||
_best_effort_delete(source.stored_path)
|
_best_effort_delete(source.stored_path)
|
||||||
raise SourceStorageError(
|
raise SourceStorageError(
|
||||||
"Failed to create Job records from Source files",
|
"Failed to create Job records from Source files",
|
||||||
category=ErrorCategory.INFRA_TRANSIENT,
|
category=ErrorCategory.INFRA_PERSISTENT,
|
||||||
suggestion="Retry creation. If this keeps happening, verify database availability.",
|
suggestion="Retry creation. If this keeps happening, verify database availability.",
|
||||||
retriable=True,
|
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
logger.info("Created explicit job document_id=%s job_id=%s sources=%s", document_id, job.id, len(source_ids))
|
logger.info("Created explicit job document_id=%s job_id=%s sources=%s", document_id, job.id, len(source_ids))
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ async def process_queued_job( # noqa: PLR0915
|
|||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
error = AppError(
|
error = AppError(
|
||||||
f"Provider call timed out after {runtime_settings.worker_provider_timeout_seconds:.1f}s",
|
f"Provider call timed out after {runtime_settings.worker_provider_timeout_seconds:.1f}s",
|
||||||
category=ErrorCategory.EXTERNAL_PROVIDER,
|
category=ErrorCategory.EXTERNAL_TIMEOUT,
|
||||||
suggestion="Retry the job. If this repeats, verify provider latency and request payload size.",
|
suggestion="Retry the job. If this repeats, verify provider latency and request payload size.",
|
||||||
retriable=True,
|
retriable=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ class TestApiErrorResponses:
|
|||||||
(ErrorCategory.NOT_FOUND, 404, "not_found"),
|
(ErrorCategory.NOT_FOUND, 404, "not_found"),
|
||||||
(ErrorCategory.CONFLICT, 409, "conflict"),
|
(ErrorCategory.CONFLICT, 409, "conflict"),
|
||||||
(ErrorCategory.EXTERNAL_PROVIDER, 503, "external"),
|
(ErrorCategory.EXTERNAL_PROVIDER, 503, "external"),
|
||||||
|
(ErrorCategory.EXTERNAL_TIMEOUT, 503, "timeout"),
|
||||||
(ErrorCategory.INFRA_TRANSIENT, 503, "timeout"),
|
(ErrorCategory.INFRA_TRANSIENT, 503, "timeout"),
|
||||||
|
(ErrorCategory.PROCESSING, 500, "internal"),
|
||||||
(ErrorCategory.INFRA_PERSISTENT, 500, "internal"),
|
(ErrorCategory.INFRA_PERSISTENT, 500, "internal"),
|
||||||
(ErrorCategory.INTERNAL_UNEXPECTED, 500, "internal"),
|
(ErrorCategory.INTERNAL_UNEXPECTED, 500, "internal"),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ BY WAY OF INTRODUCTION:-
|
|||||||
|
|
||||||
These few paragraphs of introduction may help you read BOOK 2 which covers a wider range than did BOOK 1 (Pioneer Days).
|
These few paragraphs of introduction may help you read BOOK 2 which covers a wider range than did BOOK 1 (Pioneer Days).
|
||||||
|
|
||||||
BOOK 1 had 54 pages; 14 chapters. BOOK 2 has 70 pages; 18 chapters. BOOK 1 consisted largely of first generation family history. BOOK 2 throws more light on the second generation. Sidney promises a BOOK 3 and that may begin to do justice to the third generation. We suggest that Sidney get the help of Louis Shinn who has a chapter in this book (Chapter 16 - The Last 25 Years on the Doumecq Plains. Louis has the gift of seeing, recalling and telling. One sentence in his chapter gives a great tribute to the Doumecqers--so far as he knows no one on the Doumecq Plains went on relief during the depression. That in a nutshell shows the sturdy character of the residents of the Doumeoq Plains.
|
BOOK 1 had 54 pages; 14 chapters. BOOK 2 has 70 pages; 18 chapters. BOOK 1 consisted largely of first generation family history. BOOK 2 throws more light on the second generation. Sidney promises a BOOK 3 and that may begin to do justice to the third generation. We suggest that Sidney get the help of Louis Shinn who has a chapter in this book (Chapter 16 - The Last 25 Years on the Doumecq Plains. Louis has the gift of seeing, recalling and telling. One sentence in his chapter gives a great tribute to the Doumecqers--so far as he knows no one on the Doumecq Plains went on relief during the depression. That in a nutshell shows the sturdy character of the residents of the Doumecq Plains.
|
||||||
|
|
||||||
We promised in BOOK 1 that in BOOK 2 we would give the story of the trip of John E. Cochran and wife to Tennessee, Cuba and the Panama Canal. You will see by the Table of Contents that the first four chapters have been given to those trips. Those chapters are worth reading and re-reading. Mr. Cochran has eyes to see and a pen to tell. We think the people in Tennessee will read with great pleasure the comments he makes on conditions today.
|
We promised in BOOK 1 that in BOOK 2 we would give the story of the trip of John E. Cochran and wife to Tennessee, Cuba and the Panama Canal. You will see by the Table of Contents that the first four chapters have been given to those trips. Those chapters are worth reading and re-reading. Mr. Cochran has eyes to see and a pen to tell. We think the people in Tennessee will read with great pleasure the comments he makes on conditions today.
|
||||||
|
|
||||||
Some who get this book will consider the group picture the best thing in the book. It took a lot of preliminary photographing to reduce some pictures, enlarge others and bring out the tin types. We wish that instead of 44 faces we could have given 88. Do not blame Ethel Cochran-Shinn for the selection. She furnished enough pictures but we had to take only part of them. We think there are great possibilities in reproducing old pictures. We wish we had a Pickard group. Some Pickard descendant may wish to make a collection.
|
Some who get this book will consider the group picture the best thing in the book. It took a lot of preliminary photographing to reduce some pictures, enlarge others and bring out the tin types. We wish that instead of 44 faces we could have given 88. Do not blame Ethel Cochran-Shinn for the selection. She furnished enough pictures but we had to take only part of them. We think there are great possibilities in reproducing old pictures. We wish we had a Pickard group. Some Pickard descendant may wish to make a collection.
|
||||||
|
|
||||||
We are much impressed with the future possibilities of getting a complete geneology [sic] of the Pickard family. Mr. Cochran has a fine chapter on the Pickards but to date we have not had the pleasure of finding all of the family dates. We had intended to give more family data in this book but it takes time to get the correct dates. Often times it requires trips to cemeteries to get dates on the tombstones. Winter is no time to collect dates on tombstones.
|
We are much impressed with the future possibilities of getting a complete geneology [sic] of the Pickard family. Mr. Cochran has a fine chapter on the Pickards but to date we have not had the pleasure of finding all of the family dates. We had intended to give more family data in this book but it takes time to get the correct dates. Often times it requires trips to cemeteries to get dates on the tombstones. Winter is no time to collect dates on tombstones.
|
||||||
|
-2-
|
||||||
~2~
|
|
||||||
|
|||||||
@@ -8,101 +8,31 @@ Home | Sibling's Stories | 1st Cousins | Ancestor's Stories | Reunion History |
|
|||||||
|
|
||||||
OMIE WRITES HOME
|
OMIE WRITES HOME
|
||||||
|
|
||||||
Ed. Note: The following letter was written by Omie Cochran in Nome, Alaska and sent to her
|
Ed. Note: The following letter was written by Omie Cochran in Nome, Alaska and sent to her sister, Ethel Shinn, in Canfield, Idaho in 1923. It has been stored away these 63 years in the original envelope with its 2 cent stamp. The letter has a number of references to the Shinn children. Peter was Louis; Polly was Edith; and the 'little black rascal' referred to Maurice. Miss Saville was the nurse at the Nome Hospital that was mentioned in the article by Inez in the family newsletter two years ago.
|
||||||
sister, Ethel Shinn, in Canfield, Idaho in 1923. It has been stored away these 63 years in the
|
|
||||||
original envelope with its 2 cent stamp. The letter has a number of references to the Shinn
|
|
||||||
children. Peter was Louis; Polly was Edith; and the 'little black rascal' referred to Maurice.
|
|
||||||
Miss Saville was the nurse at the Nome Hospital that was mentioned in the article by Inez in
|
|
||||||
the family newsletter two years ago.
|
|
||||||
|
|
||||||
Nome Alaska August 26, 1923
|
Nome Alaska August 26, 1923
|
||||||
|
|
||||||
My Dear Ethel et al.
|
My Dear Ethel et al.
|
||||||
|
|
||||||
[photograph of a group of people standing on snow with poles/sled gear]
|
I don't know when I did write or when you did but I am going to write now however and never the less. But I wish I could talk (I can yet but I mean to tell you all) instead and see ole Unc Pete and Polly sit up and listen and that little black rascal of yours would fairly sparkle with listening. Can't I see him listening now to all the yarns we told last summer?
|
||||||
|
|
||||||
I don't know when I did write or when you did
|
You see, we-Miss Saville and I, took a trip north on the Buford and it was very interesting. We went north thru the Bering Strait into the Arctic and as far as the Ice Pack. There the captain of our craft and some other mighty hunters went out first in kayaks and later in row boats and shot seven walrus. When they also took a movie man and camera, so you will likely see all this in the movies before I get to tell you. They came back on board and the ship went up along the icebergs and the walrus were 'heisted' on board by the use of the crane which loads tons of freight and the beasts were so huge that they made the pulleys just creak. They were over 12 feet long, as big around as three cows, had no feet but toenails on their flippers or flappers, no head but their body just suddenly ended with a hole for a mouth and big bristles all around it similar to a currycomb in coarseness; no ears but huge tusks of ivory. They are the most repulsive looking animals imaginable and tho I have always read about them I never expect such disagreeable looking creatures. They had a rough brown hairy skin and some of them looked warty. They must have weighed two ton at least. Ere we got them back to Nome to the natives they were getting extremely odiferous-in fact, you could scarcely stay on the ship with any degree of comfort unless you had per chance lost your sense of smell.
|
||||||
but I am going to write now however and never
|
|
||||||
the less. But I wish I could talk (I can yet but I
|
|
||||||
mean to tell you all) instead and see ole Unc Pete
|
|
||||||
and Polly sit up and listen and that little black
|
|
||||||
rascal of yours would fairly sparkle with
|
|
||||||
listening. Can't I see him listening now to all the
|
|
||||||
yarns we told last summer?
|
|
||||||
|
|
||||||
You see, we-Miss Saville and I, took a trip north
|
Then we went north to a few minutes beyond the 70th degree of latitude and thot for awhile we would go to Wrangell Island where some men from Steffonsons ship were supposed to be stranded but we didn't get there and instead stopped at a small native village at Cape Serdz in Siberia. These Eskimo were very primitive. One white squaw man lived there and had for 23 years. He was a Swede--who else could. Their houses were circular and built up with dirt 2 or 3 feet and then skins were stretched over it and weighted down with rocks. Inside, the room was partitioned off at the sides with skins for sleeping quarters. In the main part they had the fire on the ground and the fish drying on lines and the skins hanging around and the dogs and babies and children. They wore skin clothes entirely. The women's were made like bloomers and were heavily padded for warmth. They wore high mukluks and really looked very comfortable. The babies were in fur skins with the fur inside and they looked like little Teddy bears with faces. I guess they had never seen white women, not so many at one time anyway. We went ashore in 2 life boats and a launch pulling them. On the launch was a six piece band playing and the rear of the last life boat was the movie man. 'Twas very thrilling.
|
||||||
on the Buford and it was very interesting. We
|
|
||||||
went north thru the Bering Strait into the Arctic and as far as the Ice Pack. There the captain
|
|
||||||
of our craft and some other mighty hunters went out first in kayaks and later in row boats and
|
|
||||||
shot seven walrus. When they also took a movie man and camera, so you will likely see all
|
|
||||||
this in the movies before I get to tell you. They came back on board and the ship went up
|
|
||||||
along the icebergs and the walrus were 'heisted' on board by the use of the crane which loads
|
|
||||||
tons of freight and the beasts were so huge that they made the pulleys just creak. They were
|
|
||||||
over 12 feet long, as big around as three cows, had no feet but toenails on their flippers or
|
|
||||||
flappers, no head but their body just suddenly ended with a hole for a mouth and big bristles
|
|
||||||
all around it similar to a currycomb in coarseness; no ears but huge tusks of ivory. They are
|
|
||||||
the most repulsive looking animals imaginable and tho I have always read about them I never
|
|
||||||
expect such disagreeable looking creatures. They had a rough brown hairy skin and some of
|
|
||||||
them looked warty. They must have weighed two ton at least. Ere we got them back to Nome
|
|
||||||
to the natives they were getting extremely odiferous—in fact, you could scarcely stay on the
|
|
||||||
ship with any degree of comfort unless you had per chance lost your sense of smell.
|
|
||||||
|
|
||||||
Then we went north to a few minutes beyond the 70th degree of latitude and thot for awhile
|
The other place we stopped was at Whalen, a trading post in Siberia. There these 'towerists' went wild. They rushed helter-skelter, hither and thither, here and there, trying to find something to buy. Prices raised right before your eyes. One would but[sic] something for $1.00 and the next might have to pay $2.00, $4.00 or $10.00. That made no difference. They had to have it. One man I was sort of taking care of, tho he had his son along for the purpose, bought 2 ivory tusks, 1 pup, 2 moccasins, 3 or 4 billikens, 6 or 8 ivory and silver rings, one fishing line, hooks, floats, etc. and two bird slings. The slings have rocks at the end and the little natives throw them at the flocks of geese and ducks which fly close over the village and the slings entangle their wings and legs, sometimes more than one, and they can't fly. They come down and the natives capture them. There was more junk brot aboard than baggage, I do believe. And they say that at the first stop it was worse than here. The red flag was flying over Whalen and the Russian soldiers were there-a few, one or two or three, I forget the number.
|
||||||
we would go to Wrangell Island where some men from Steffonsons ship were supposed to be
|
|
||||||
stranded but we didn't get there and instead stopped at a small native village at Cape Serdz in
|
|
||||||
Siberia. These Eskimo were very primitive. One white squaw man lived there and had for 23
|
|
||||||
years. He was a Swede--who else could. Their houses were circular and built up with dirt 2 or
|
|
||||||
3 feet and then skins were stretched over it and weighted down with rocks. Inside, the room
|
|
||||||
was partitioned off at the sides with skins for sleeping quarters. In the main part they had the
|
|
||||||
fire on the ground and the fish drying on lines and the skins hanging around and the dogs and
|
|
||||||
babies and children. They wore skin clothes entirely. The women's were made like bloomers
|
|
||||||
and were heavily padded for warmth. They wore high mukluks and really looked very
|
|
||||||
comfortable. The babies were in fur skins with the fur inside and they looked like little Teddy
|
|
||||||
bears with faces. I guess they had never seen white women, not so many at one time anyway.
|
|
||||||
We went ashore in 2 life boats and a launch pulling them. On the launch was a six piece band
|
|
||||||
playing and the rear of the last life boat was the movie man. 'Twas very thrilling.
|
|
||||||
|
|
||||||
The other place we stopped was at Whalen, a trading post in Siberia. There these 'towerists'
|
We got home yesterday morning at 5 a.m. but missed the first lighter in so had to stay out until 2:30. The girls had prepared a big meal for us and invited up the Hartfords and then let us talk. Miss Saville talked quite a bit. Any how if you folks don't like this I don't care, it is all I had to write about and I know Buster'ud listen anyway and I'd soak ole Peter's head if he didn't and Polly would in my lap and I don't know much about the youngest one of yours so likely he would be squawling. But we did surely enjoy our trip and were gone just long enuf.
|
||||||
went wild. They rushed helter-skelter, hither and thither, here and there, trying to find
|
|
||||||
something to buy. Prices raised right before your eyes. One would [buy?] something for $1.00
|
|
||||||
and the next might have to pay $2.00, $4.00 or $10.00. That made no difference. They had to
|
|
||||||
have it. One man I was sort of taking care of, tho he had his son along for the purpose,
|
|
||||||
bought 2 ivory tusks, 1 pup, 2 moccasins, 3 or 4 billikens, 6 or 8 ivory and silver rings, one
|
|
||||||
fishing line, hooks, floats, etc. and two bird slings. The slings have rocks at the end and the
|
|
||||||
little natives throw them at the flocks of geese and ducks which fly close over the village and
|
|
||||||
the slings entangle their wings and legs, sometimes more than one, and they can't fly. They
|
|
||||||
come down and the natives capture them. There was more junk brot aboard than baggage, I
|
|
||||||
do believe. And they say that at the first stop it was worse than here. The red flag was flying
|
|
||||||
over Whalen and the Russian soldiers were there—a few, one or two or three, I forget the
|
|
||||||
number.
|
|
||||||
|
|
||||||
We got home yesterday morning at 5 a.m. but missed the first lighter in so had to stay out
|
[photograph of bundled children standing in snow with skis and poles]
|
||||||
until 2:30. The girls had prepared a big meal for us and invited up the Hartfords and then let
|
|
||||||
us talk. Miss Saville talked quite a bit. Any how if you folks don't like this I don't care, it is
|
|
||||||
all I had to write about and I know Buster'ud listen anyway and I'd soak ole Peter's head if he
|
|
||||||
didn't and Polly would in my lap and I don't know much about the youngest one of yours so
|
|
||||||
likely he would be squawling. But we did surely enjoy our trip and were gone just long enuf.
|
|
||||||
|
|
||||||
I expect there were 150 passengers on board and almost or more of the crew and helpers. We
|
I expect there were 150 passengers on board and almost or more of the crew and helpers. We had a stateroom down next to the kitchen and 'twas pretty fierce for odor at times.
|
||||||
had a stateroom down next to the kitchen and 'twas pretty fierce for odor at times.
|
|
||||||
|
|
||||||
I have had jobs nearly all summer but not very much in them. Next week, September 4,
|
I have had jobs nearly all summer but not very much in them. Next week, September 4, school opens. I wish they would wait for a week but you know these school men. Wouldn't make any special difference I suppose for I would just fritter away the time but still one likes to postpone the inevitable.
|
||||||
school opens. I wish they would wait for a week but you know these school men. Wouldn't
|
|
||||||
make any special difference I suppose for I would just fritter away the time but still one likes
|
|
||||||
to postpone the inevitable.
|
|
||||||
|
|
||||||
I just now stopped and re-read your letter and it sure was a bird. I don't especially blame Ray
|
I just now stopped and re-read your letter and it sure was a bird. I don't especially blame Ray for reading over your shoulder. It would seem, then that you have bright children. Maybe they do know something about Geography. But it is ridiculous to speak of Louis finishing the eighth grade. Why you and I were grown children when we finished and he is only a baby. I am rather afraid he doesn't know much. I quite remember your little timid Maurice and how he shifted his affection from his Aunt Om and yelled and howled and screamed steadily for a week while his mother went to S.S. Ask him is he recalls this little interview. Do you suppose he does?
|
||||||
for reading over your shoulder. It would seem, then that you have bright children. Maybe
|
|
||||||
they do know something about Geography. But it is ridiculous to speak of Louis finishing the
|
|
||||||
eighth grade. Why you and I were grown children when we finished and he is only a baby. I
|
|
||||||
am rather afraid he doesn't know much. I quite remember your little timid Maurice and how
|
|
||||||
he shifted his affection from his Aunt Om and yelled and howled and screamed steadily for a
|
|
||||||
week while his mother went to S.S. Ask him is he recalls this little interview. Do you suppose
|
|
||||||
he does?
|
|
||||||
|
|
||||||
Ever hear from Zen? or Inez? They don't seem to be very writing inclined tho Zen has done
|
Ever hear from Zen? or Inez? They don't seem to be very writing inclined tho Zen has done well this year. Even sent me a telegram a few weeks ago. Well, if anything else ever happens, I'll write again. Don't suppose it ever will, tho.
|
||||||
well this year. Even sent me a telegram a few weeks ago. Well, if anything else ever happens,
|
|
||||||
I'll write again. Don't suppose it ever will, tho.
|
|
||||||
|
|
||||||
Lots of love to all,
|
Lots of love to all,
|
||||||
|
|
||||||
|
|||||||
@@ -9,24 +9,25 @@ ISBILL & MOSER
|
|||||||
DEALERS IN
|
DEALERS IN
|
||||||
GENERAL MERCHANDISE
|
GENERAL MERCHANDISE
|
||||||
|
|
||||||
Vonore, Tenn., [handwritten: July 27-] 191[handwritten:2]
|
Vonore, Tenn., [handwritten: May 27-] 191[handwritten: 3]
|
||||||
|
|
||||||
[handwritten: Dear Uncle Aunt & Cousins
|
[handwritten: Dear Uncle Aunt & Cousins
|
||||||
I was at home a
|
I was at home a
|
||||||
few nights ago & saw a
|
few nights ago & saw a
|
||||||
letter from your folks, so
|
letter from your folks, so
|
||||||
I decided to write you
|
I decided to write you
|
||||||
a few lines myself if
|
a few lines myself as
|
||||||
I can [contributeful?] a
|
I am contemplating a
|
||||||
trip out west next summer
|
trip out west next summer
|
||||||
I want [illegible] to go
|
& want lots of [places?] to go
|
||||||
where I am from.
|
where I am [from?].
|
||||||
|
|
||||||
Am getting
|
Am getting
|
||||||
up in years & wondering.
|
up in years & wondering,
|
||||||
So youl [sic] see the object of
|
so you'll see the object of
|
||||||
my trip, is to get a wife
|
my trip, is to get a wife
|
||||||
If there is any old maid
|
If there is any old maid
|
||||||
or widow out there I
|
or widow out there I
|
||||||
want you to hire them
|
want you to [hie?] them
|
||||||
at our [illegible] at them
|
at [our find?] me at them
|
||||||
as soon as I get there.]
|
as soon as I get there.]
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ from transcription.db.models import Document
|
|||||||
from transcription.db.models import Job
|
from transcription.db.models import Job
|
||||||
from transcription.db.models import JobSource
|
from transcription.db.models import JobSource
|
||||||
from transcription.db.models import Source
|
from transcription.db.models import Source
|
||||||
|
from transcription.errors import ErrorCategory
|
||||||
from transcription.services.people import store_person_portrait
|
from transcription.services.people import store_person_portrait
|
||||||
from transcription.services.sources import source_mime_type
|
from transcription.services.sources import source_mime_type
|
||||||
from transcription.services.store import SourceStorageError
|
from transcription.services.store import SourceStorageError
|
||||||
|
from transcription.services.store import StoredSourceFile
|
||||||
from transcription.services.store import create_document_job
|
from transcription.services.store import create_document_job
|
||||||
from transcription.services.store import create_job_for_document
|
from transcription.services.store import create_job_for_document
|
||||||
from transcription.services.store import store_source_file
|
from transcription.services.store import store_source_file
|
||||||
@@ -152,3 +154,72 @@ async def test_source_storage_rejects_unsupported_format(tmp_path):
|
|||||||
|
|
||||||
with pytest.raises(SourceStorageError):
|
with pytest.raises(SourceStorageError):
|
||||||
await store_source_file(filename="page.txt", file_bytes=b"text", settings=settings)
|
await store_source_file(filename="page.txt", file_bytes=b"text", settings=settings)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_document_job_db_failure_maps_to_internal_category(tmp_path, monkeypatch):
|
||||||
|
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||||
|
stored_path = tmp_path / "documents" / "stored-file.pdf"
|
||||||
|
stored_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
stored_path.write_bytes(b"payload")
|
||||||
|
|
||||||
|
async def _fake_store_source_file(**_kwargs) -> StoredSourceFile:
|
||||||
|
return StoredSourceFile(
|
||||||
|
path=stored_path,
|
||||||
|
file_hash="f" * 64,
|
||||||
|
file_size_bytes=7,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _boom_create_records(**_kwargs):
|
||||||
|
raise RuntimeError("database unavailable")
|
||||||
|
|
||||||
|
monkeypatch.setattr("transcription.services.store.store_source_file", _fake_store_source_file)
|
||||||
|
monkeypatch.setattr("transcription.services.store._create_document_job_records", _boom_create_records)
|
||||||
|
|
||||||
|
with pytest.raises(SourceStorageError) as exc_info:
|
||||||
|
await create_document_job(
|
||||||
|
filename="single-page.pdf",
|
||||||
|
file_bytes=b"payload",
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.category == ErrorCategory.INFRA_PERSISTENT
|
||||||
|
assert exc_info.value.retriable is False
|
||||||
|
assert not stored_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_create_job_for_document_db_failure_maps_to_internal_category(async_session, tmp_path, monkeypatch):
|
||||||
|
document = Document(id=uuid4(), name="db-failure-doc")
|
||||||
|
async_session.add(document)
|
||||||
|
await async_session.commit()
|
||||||
|
|
||||||
|
settings = Settings(openrouter_api_key="test-key", upload_dir=tmp_path)
|
||||||
|
stored_path = tmp_path / "documents" / str(document.id) / "stored-file.pdf"
|
||||||
|
stored_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
stored_path.write_bytes(b"payload")
|
||||||
|
|
||||||
|
async def _fake_store_source_file(**_kwargs) -> StoredSourceFile:
|
||||||
|
return StoredSourceFile(
|
||||||
|
path=stored_path,
|
||||||
|
file_hash="f" * 64,
|
||||||
|
file_size_bytes=7,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _boom_create_records(**_kwargs):
|
||||||
|
raise RuntimeError("database unavailable")
|
||||||
|
|
||||||
|
monkeypatch.setattr("transcription.services.store.store_source_file", _fake_store_source_file)
|
||||||
|
monkeypatch.setattr("transcription.services.store._create_job_for_document_records", _boom_create_records)
|
||||||
|
|
||||||
|
with pytest.raises(SourceStorageError) as exc_info:
|
||||||
|
await create_job_for_document(
|
||||||
|
document_id=document.id,
|
||||||
|
source_files=[("single-page.pdf", b"payload")],
|
||||||
|
session=async_session,
|
||||||
|
settings=settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value.category == ErrorCategory.INFRA_PERSISTENT
|
||||||
|
assert exc_info.value.retriable is False
|
||||||
|
assert not stored_path.exists()
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ class TestWorkflowReliability:
|
|||||||
error_detail = next(attempt.error_detail for attempt in attempts if attempt.error_detail is not None)
|
error_detail = next(attempt.error_detail for attempt in attempts if attempt.error_detail is not None)
|
||||||
assert "timed out" in error_detail.lower()
|
assert "timed out" in error_detail.lower()
|
||||||
assert "20.0s" in error_detail
|
assert "20.0s" in error_detail
|
||||||
|
assert attempts[0].error_category == "external_timeout_error"
|
||||||
|
assert "retry the job" in error_detail.lower()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_timeout_duration_excludes_pre_call_setup(self, default_session_factory, monkeypatch):
|
async def test_timeout_duration_excludes_pre_call_setup(self, default_session_factory, monkeypatch):
|
||||||
|
|||||||
+18
-7
@@ -21,6 +21,8 @@ class TestErrorCategoryContract:
|
|||||||
assert ErrorCategory.NOT_FOUND.value == "not_found_error"
|
assert ErrorCategory.NOT_FOUND.value == "not_found_error"
|
||||||
assert ErrorCategory.CONFLICT.value == "conflict_error"
|
assert ErrorCategory.CONFLICT.value == "conflict_error"
|
||||||
assert ErrorCategory.EXTERNAL_PROVIDER.value == "external_provider_error"
|
assert ErrorCategory.EXTERNAL_PROVIDER.value == "external_provider_error"
|
||||||
|
assert ErrorCategory.EXTERNAL_TIMEOUT.value == "external_timeout_error"
|
||||||
|
assert ErrorCategory.PROCESSING.value == "processing_error"
|
||||||
assert ErrorCategory.INFRA_TRANSIENT.value == "infrastructure_transient_error"
|
assert ErrorCategory.INFRA_TRANSIENT.value == "infrastructure_transient_error"
|
||||||
assert ErrorCategory.INFRA_PERSISTENT.value == "infrastructure_persistent_error"
|
assert ErrorCategory.INFRA_PERSISTENT.value == "infrastructure_persistent_error"
|
||||||
assert ErrorCategory.INTERNAL_UNEXPECTED.value == "internal_unexpected_error"
|
assert ErrorCategory.INTERNAL_UNEXPECTED.value == "internal_unexpected_error"
|
||||||
@@ -49,11 +51,20 @@ class TestAppErrorHelpers:
|
|||||||
|
|
||||||
def test_envelope_categories_use_canonical_contract_values(self):
|
def test_envelope_categories_use_canonical_contract_values(self):
|
||||||
"""API/UI envelope categories are normalized to canonical short identifiers."""
|
"""API/UI envelope categories are normalized to canonical short identifiers."""
|
||||||
validation = AppError("x", category=ErrorCategory.USER_INPUT)
|
expected_mapping = {
|
||||||
timeout = AppError("x", category=ErrorCategory.INFRA_TRANSIENT)
|
ErrorCategory.VALIDATION: "validation",
|
||||||
internal = AppError("x", category=ErrorCategory.INTERNAL_UNEXPECTED)
|
ErrorCategory.USER_INPUT: "validation",
|
||||||
|
ErrorCategory.NOT_FOUND: "not_found",
|
||||||
|
ErrorCategory.CONFLICT: "conflict",
|
||||||
|
ErrorCategory.EXTERNAL_PROVIDER: "external",
|
||||||
|
ErrorCategory.EXTERNAL_TIMEOUT: "timeout",
|
||||||
|
ErrorCategory.INFRA_TRANSIENT: "timeout",
|
||||||
|
ErrorCategory.PROCESSING: "internal",
|
||||||
|
ErrorCategory.INFRA_PERSISTENT: "internal",
|
||||||
|
ErrorCategory.INTERNAL_UNEXPECTED: "internal",
|
||||||
|
}
|
||||||
|
|
||||||
assert canonical_error_category(validation) == "validation"
|
for category, expected in expected_mapping.items():
|
||||||
assert canonical_error_category(timeout) == "timeout"
|
err = AppError("x", category=category)
|
||||||
assert canonical_error_category(internal) == "internal"
|
assert canonical_error_category(err) == expected
|
||||||
assert build_error_envelope(validation).category == "validation"
|
assert build_error_envelope(err).category == expected
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ def test_display_error_category_uses_canonical_taxonomy():
|
|||||||
assert display_error_category(AppError("x", category=ErrorCategory.NOT_FOUND)) == "not_found"
|
assert display_error_category(AppError("x", category=ErrorCategory.NOT_FOUND)) == "not_found"
|
||||||
assert display_error_category(AppError("x", category=ErrorCategory.CONFLICT)) == "conflict"
|
assert display_error_category(AppError("x", category=ErrorCategory.CONFLICT)) == "conflict"
|
||||||
assert display_error_category(AppError("x", category=ErrorCategory.EXTERNAL_PROVIDER)) == "external"
|
assert display_error_category(AppError("x", category=ErrorCategory.EXTERNAL_PROVIDER)) == "external"
|
||||||
|
assert display_error_category(AppError("x", category=ErrorCategory.EXTERNAL_TIMEOUT)) == "timeout"
|
||||||
assert display_error_category(AppError("x", category=ErrorCategory.INFRA_TRANSIENT)) == "timeout"
|
assert display_error_category(AppError("x", category=ErrorCategory.INFRA_TRANSIENT)) == "timeout"
|
||||||
|
assert display_error_category(AppError("x", category=ErrorCategory.PROCESSING)) == "internal"
|
||||||
assert display_error_category(AppError("x", category=ErrorCategory.INFRA_PERSISTENT)) == "internal"
|
assert display_error_category(AppError("x", category=ErrorCategory.INFRA_PERSISTENT)) == "internal"
|
||||||
|
|||||||
Reference in New Issue
Block a user