sapling-py 4.3.0__tar.gz → 4.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sapling_py-4.3.0/sapling_py.egg-info → sapling_py-4.11.0}/PKG-INFO +18 -3
- {sapling_py-4.3.0 → sapling_py-4.11.0}/README.md +17 -2
- {sapling_py-4.3.0 → sapling_py-4.11.0}/sapling/client.py +256 -22
- sapling_py-4.11.0/sapling/version.py +1 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0/sapling_py.egg-info}/PKG-INFO +18 -3
- {sapling_py-4.3.0 → sapling_py-4.11.0}/sapling_py.egg-info/SOURCES.txt +3 -1
- {sapling_py-4.3.0 → sapling_py-4.11.0}/test/test_classify.py +23 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/test/test_client.py +122 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/test/test_extract.py +24 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/test/test_safety.py +23 -0
- sapling_py-4.11.0/test/test_simplify.py +95 -0
- sapling_py-4.11.0/test/test_translate.py +109 -0
- sapling_py-4.3.0/sapling/version.py +0 -1
- {sapling_py-4.3.0 → sapling_py-4.11.0}/LICENSE +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/pyproject.toml +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/sapling/__init__.py +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/sapling_py.egg-info/dependency_links.txt +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/sapling_py.egg-info/requires.txt +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/sapling_py.egg-info/top_level.txt +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/setup.cfg +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/setup.py +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/test/test_pii.py +0 -0
- {sapling_py-4.3.0 → sapling_py-4.11.0}/test/test_seo.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sapling-py
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.11.0
|
|
4
4
|
Summary: Sapling Python Client
|
|
5
5
|
Author-email: Sapling Intelligence <info@sapling.ai>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -31,8 +31,9 @@ Compare against leading grammar checking tools and APIs like Grammarly (Grammerl
|
|
|
31
31
|
|
|
32
32
|
Sapling is a toolkit for helping developers build language model-powered applications.
|
|
33
33
|
The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection,
|
|
34
|
-
zero-shot text classification, structured data extraction, PII detection
|
|
35
|
-
|
|
34
|
+
zero-shot text classification, structured data extraction, translation, PII detection
|
|
35
|
+
and redaction, content safety scoring, plain-language
|
|
36
|
+
simplification, SEO analysis, and more.
|
|
36
37
|
|
|
37
38
|
Key Features:
|
|
38
39
|
- **60% More Corrections**: Outperforms [competing solutions](https://sapling.ai/comparison/api#sapling) at similar accuracy levels using state-of-the-art generative AI models.
|
|
@@ -94,6 +95,20 @@ Get a production key by following [this documentation](https://sapling.ai/docs/a
|
|
|
94
95
|
|
|
95
96
|
Here's some more information on [request options and response structure](https://sapling.ai/docs/api/edits-overview).
|
|
96
97
|
|
|
98
|
+
## Batch requests
|
|
99
|
+
|
|
100
|
+
The analysis endpoints — `classify`, `extract`, `safety`, `langdetect`,
|
|
101
|
+
`translate`, `summarize`, `quality` and `aidetect` — also accept a list of
|
|
102
|
+
1-10 texts in
|
|
103
|
+
place of a single string. The batch is sent as one request (the same options
|
|
104
|
+
apply to every item) and returns `{'results': [...]}` with one
|
|
105
|
+
single-response-shaped dict per input, in order:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
scores = client.quality(['First draft.', 'Second draft.'], sentence_scores=True)
|
|
109
|
+
langs = client.langdetect(['Bonjour le monde', 'Hello world'])
|
|
110
|
+
```
|
|
111
|
+
|
|
97
112
|
## License
|
|
98
113
|
|
|
99
114
|
Copyright 2024--present Sapling Intelligence, Inc.
|
|
@@ -9,8 +9,9 @@ Compare against leading grammar checking tools and APIs like Grammarly (Grammerl
|
|
|
9
9
|
|
|
10
10
|
Sapling is a toolkit for helping developers build language model-powered applications.
|
|
11
11
|
The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection,
|
|
12
|
-
zero-shot text classification, structured data extraction, PII detection
|
|
13
|
-
|
|
12
|
+
zero-shot text classification, structured data extraction, translation, PII detection
|
|
13
|
+
and redaction, content safety scoring, plain-language
|
|
14
|
+
simplification, SEO analysis, and more.
|
|
14
15
|
|
|
15
16
|
Key Features:
|
|
16
17
|
- **60% More Corrections**: Outperforms [competing solutions](https://sapling.ai/comparison/api#sapling) at similar accuracy levels using state-of-the-art generative AI models.
|
|
@@ -72,6 +73,20 @@ Get a production key by following [this documentation](https://sapling.ai/docs/a
|
|
|
72
73
|
|
|
73
74
|
Here's some more information on [request options and response structure](https://sapling.ai/docs/api/edits-overview).
|
|
74
75
|
|
|
76
|
+
## Batch requests
|
|
77
|
+
|
|
78
|
+
The analysis endpoints — `classify`, `extract`, `safety`, `langdetect`,
|
|
79
|
+
`translate`, `summarize`, `quality` and `aidetect` — also accept a list of
|
|
80
|
+
1-10 texts in
|
|
81
|
+
place of a single string. The batch is sent as one request (the same options
|
|
82
|
+
apply to every item) and returns `{'results': [...]}` with one
|
|
83
|
+
single-response-shaped dict per input, in order:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
scores = client.quality(['First draft.', 'Second draft.'], sentence_scores=True)
|
|
87
|
+
langs = client.langdetect(['Bonjour le monde', 'Hello world'])
|
|
88
|
+
```
|
|
89
|
+
|
|
75
90
|
## License
|
|
76
91
|
|
|
77
92
|
Copyright 2024--present Sapling Intelligence, Inc.
|
|
@@ -430,8 +430,13 @@ class SaplingClient:
|
|
|
430
430
|
'''
|
|
431
431
|
Score a piece of text on how likely it was generated by AI.
|
|
432
432
|
|
|
433
|
-
:param text: Text to score for AI-generated content
|
|
434
|
-
|
|
433
|
+
:param text: Text to score for AI-generated content (up to 200,000
|
|
434
|
+
characters). Pass a list or tuple of 1-10 strings to score a batch
|
|
435
|
+
in one request (sent as the API's ``texts`` parameter; the same
|
|
436
|
+
options apply to every item, and the combined length is capped at
|
|
437
|
+
200,000 characters). A batch response is ``{'results': [...]}``
|
|
438
|
+
with one single-response-shaped dict per input, in order.
|
|
439
|
+
:type text: str | list[str]
|
|
435
440
|
:param sent_scores: If true, each sentence will also be scored individually.
|
|
436
441
|
:type sent_scores: bool
|
|
437
442
|
|
|
@@ -445,12 +450,94 @@ class SaplingClient:
|
|
|
445
450
|
url = f'{self.url_endpoint}aidetect'
|
|
446
451
|
data = {
|
|
447
452
|
'key': self.api_key,
|
|
448
|
-
'text': text,
|
|
449
453
|
}
|
|
454
|
+
# A list/tuple of texts is the batch form: one request, one result per
|
|
455
|
+
# item ({'results': [...]}), same options applied to every item.
|
|
456
|
+
if isinstance(text, (list, tuple)):
|
|
457
|
+
data['texts'] = list(text)
|
|
458
|
+
else:
|
|
459
|
+
data['text'] = text
|
|
450
460
|
if sent_scores is not None:
|
|
451
461
|
data['sent_scores'] = sent_scores
|
|
452
462
|
return self._request(url, data)
|
|
453
463
|
|
|
464
|
+
def simplify(
|
|
465
|
+
self,
|
|
466
|
+
text,
|
|
467
|
+
reading_level=None,
|
|
468
|
+
preserve_terms=None,
|
|
469
|
+
lang=None,
|
|
470
|
+
):
|
|
471
|
+
'''
|
|
472
|
+
Rewrites a text in plain language at a target reading level, keeping
|
|
473
|
+
all of its content, structure and language — nothing is summarized or
|
|
474
|
+
dropped. The response includes a deterministic before/after
|
|
475
|
+
readability block (Flesch-Kincaid grade and reading ease) when the
|
|
476
|
+
text's language supports it, so the improvement is verifiable.
|
|
477
|
+
|
|
478
|
+
Example::
|
|
479
|
+
|
|
480
|
+
client.simplify(
|
|
481
|
+
'The party of the first part shall remit payment within '
|
|
482
|
+
'thirty (30) days of receipt of the invoice.',
|
|
483
|
+
reading_level='plain',
|
|
484
|
+
preserve_terms=['invoice'],
|
|
485
|
+
)
|
|
486
|
+
# {'simplified': 'You must pay within 30 days of getting '
|
|
487
|
+
# 'the invoice.',
|
|
488
|
+
# 'reading_level': 'plain', 'lang': 'en',
|
|
489
|
+
# 'readability': {'before': {'grade': 12.3, 'ease': 42.1},
|
|
490
|
+
# 'after': {'grade': 5.8, 'ease': 78.4}}}
|
|
491
|
+
|
|
492
|
+
:param text: Text to simplify, up to 5,000 characters. Sent to the
|
|
493
|
+
model as submitted — markup and line structure are preserved, not
|
|
494
|
+
stripped.
|
|
495
|
+
:type text: str
|
|
496
|
+
:param reading_level: Target audience: ``'plain'`` (plain-language
|
|
497
|
+
style for a general audience, the API default), ``'elementary'``
|
|
498
|
+
(~US grade 3-5), ``'middle_school'`` (~grade 6-8) or
|
|
499
|
+
``'high_school'`` (~grade 9-10).
|
|
500
|
+
:type reading_level: str
|
|
501
|
+
:param preserve_terms: Up to 20 terms (each up to 100 characters) the
|
|
502
|
+
rewrite must keep exactly as written — product names, defined
|
|
503
|
+
legal terms. A rewrite that drops one is refused server-side
|
|
504
|
+
rather than returned.
|
|
505
|
+
:type preserve_terms: list[str]
|
|
506
|
+
:param lang: ISO 639-1 code used to pick the readability formula for
|
|
507
|
+
the before/after scores. Omit for auto-detection (falls back to
|
|
508
|
+
``'en'``). The rewrite itself always stays in the text's own
|
|
509
|
+
language.
|
|
510
|
+
:type lang: str
|
|
511
|
+
:raises TypeError: If ``preserve_terms`` is a single string rather
|
|
512
|
+
than a list/tuple of terms.
|
|
513
|
+
:rtype: dict
|
|
514
|
+
:return:
|
|
515
|
+
- simplified: The rewritten text, structure preserved.
|
|
516
|
+
- reading_level: The reading level that was applied.
|
|
517
|
+
- lang: The language the readability scores were computed for
|
|
518
|
+
(detected when not supplied).
|
|
519
|
+
- readability: ``{'before': {'grade', 'ease'}, 'after': {'grade',
|
|
520
|
+
'ease'}}`` Flesch-Kincaid scores, present when the language
|
|
521
|
+
supports readability scoring.
|
|
522
|
+
'''
|
|
523
|
+
# A bare string would silently be split into one-character terms;
|
|
524
|
+
# fail loudly instead (same guard as extract/classify).
|
|
525
|
+
if preserve_terms is not None and (
|
|
526
|
+
not isinstance(preserve_terms, Iterable)
|
|
527
|
+
or isinstance(preserve_terms, (str, bytes, dict))):
|
|
528
|
+
raise TypeError('preserve_terms must be a list of strings')
|
|
529
|
+
url = f'{self.url_endpoint}simplify'
|
|
530
|
+
data = {
|
|
531
|
+
'key': self.api_key,
|
|
532
|
+
'text': text,
|
|
533
|
+
}
|
|
534
|
+
if reading_level is not None:
|
|
535
|
+
data['reading_level'] = reading_level
|
|
536
|
+
if preserve_terms is not None:
|
|
537
|
+
data['preserve_terms'] = list(preserve_terms)
|
|
538
|
+
if lang is not None:
|
|
539
|
+
data['lang'] = lang
|
|
540
|
+
return self._request(url, data)
|
|
454
541
|
def chunk_text(
|
|
455
542
|
self,
|
|
456
543
|
text,
|
|
@@ -615,21 +702,55 @@ class SaplingClient:
|
|
|
615
702
|
def summarize(
|
|
616
703
|
self,
|
|
617
704
|
text,
|
|
705
|
+
length=None,
|
|
618
706
|
):
|
|
619
707
|
'''
|
|
620
|
-
Summarizes a longer document
|
|
708
|
+
Summarizes a longer document, webpage or email thread into a prose
|
|
709
|
+
summary plus key points.
|
|
621
710
|
|
|
622
|
-
|
|
623
|
-
|
|
711
|
+
Example::
|
|
712
|
+
|
|
713
|
+
client.summarize(
|
|
714
|
+
'Hi, I was charged twice for my order this month. ...',
|
|
715
|
+
length='short',
|
|
716
|
+
)
|
|
717
|
+
# {'result': 'The customer was double-charged and wants a refund.',
|
|
718
|
+
# 'summary': 'The customer was double-charged and wants a refund.',
|
|
719
|
+
# 'key_points': ['Charged twice for one order.',
|
|
720
|
+
# 'Requests a refund of the duplicate.'],
|
|
721
|
+
# 'length': 'short'}
|
|
722
|
+
|
|
723
|
+
:param text: Input document to summarize, plain text or HTML (tags
|
|
724
|
+
are stripped), up to 20,000 characters. Pass a list or tuple of
|
|
725
|
+
1-10 strings to summarize a batch in one request (sent as the
|
|
726
|
+
API's ``texts`` parameter; combined length up to 20,000
|
|
727
|
+
characters, same ``length`` option applied to every item). A
|
|
728
|
+
batch response is ``{'results': [...]}`` with one
|
|
729
|
+
single-response-shaped dict per input, in order.
|
|
730
|
+
:type text: str | list[str]
|
|
731
|
+
:param length: Optional summary length: ``'short'``, ``'medium'``
|
|
732
|
+
(the default) or ``'long'``.
|
|
733
|
+
:type length: str
|
|
624
734
|
:rtype: dict
|
|
625
735
|
:return:
|
|
626
|
-
-
|
|
736
|
+
- summary: The prose summary.
|
|
737
|
+
- result: Mirrors ``summary`` (the endpoint's original response
|
|
738
|
+
key, kept for backward compatibility).
|
|
739
|
+
- key_points: List of key-point strings.
|
|
740
|
+
- length: The length option the summary was generated with.
|
|
627
741
|
'''
|
|
628
742
|
url = self.url_endpoint + 'summarize'
|
|
629
743
|
data = {
|
|
630
744
|
'key': self.api_key,
|
|
631
|
-
'text': text,
|
|
632
745
|
}
|
|
746
|
+
# A list/tuple of texts is the batch form: one request, one result per
|
|
747
|
+
# item ({'results': [...]}), same options applied to every item.
|
|
748
|
+
if isinstance(text, (list, tuple)):
|
|
749
|
+
data['texts'] = list(text)
|
|
750
|
+
else:
|
|
751
|
+
data['text'] = text
|
|
752
|
+
if length is not None:
|
|
753
|
+
data['length'] = length
|
|
633
754
|
return self._request(url, data)
|
|
634
755
|
|
|
635
756
|
def extract(
|
|
@@ -672,8 +793,12 @@ class SaplingClient:
|
|
|
672
793
|
# 'missing': ['purchase_order']}
|
|
673
794
|
|
|
674
795
|
:param text: Document to extract from, plain text or HTML (tags are
|
|
675
|
-
stripped), up to 10,000 characters.
|
|
676
|
-
|
|
796
|
+
stripped), up to 10,000 characters. Pass a list or tuple of 1-10
|
|
797
|
+
strings to extract from a batch in one request (sent as the API's
|
|
798
|
+
``texts`` parameter; combined length up to 10,000 characters). A batch
|
|
799
|
+
response is ``{'results': [...]}`` with one single-response-shaped
|
|
800
|
+
dict per input, in order.
|
|
801
|
+
:type text: str | list[str]
|
|
677
802
|
:param fields: 1-20 fields to extract. Each entry is either a field name
|
|
678
803
|
(str) or a dict ``{'name': str, 'type': str, 'description': str,
|
|
679
804
|
'required': bool}`` where everything but ``name`` is optional. Names are
|
|
@@ -711,13 +836,85 @@ class SaplingClient:
|
|
|
711
836
|
url = self.url_endpoint + 'extract'
|
|
712
837
|
data = {
|
|
713
838
|
'key': self.api_key,
|
|
714
|
-
'text': text,
|
|
715
839
|
'fields': list(fields),
|
|
716
840
|
}
|
|
841
|
+
# A list/tuple of texts is the batch form: one request, one result per
|
|
842
|
+
# item ({'results': [...]}), same fields applied to every item.
|
|
843
|
+
if isinstance(text, (list, tuple)):
|
|
844
|
+
data['texts'] = list(text)
|
|
845
|
+
else:
|
|
846
|
+
data['text'] = text
|
|
717
847
|
if context is not None:
|
|
718
848
|
data['context'] = context
|
|
719
849
|
return self._request(url, data)
|
|
720
850
|
|
|
851
|
+
def translate(
|
|
852
|
+
self,
|
|
853
|
+
text,
|
|
854
|
+
target_lang,
|
|
855
|
+
source_lang=None,
|
|
856
|
+
formality=None,
|
|
857
|
+
):
|
|
858
|
+
'''
|
|
859
|
+
Translates text into a target language with an LLM, preserving line
|
|
860
|
+
breaks, markup and placeholders in place. The response reports the
|
|
861
|
+
language the text was actually written in (detected, not assumed from
|
|
862
|
+
the hint).
|
|
863
|
+
|
|
864
|
+
Example::
|
|
865
|
+
|
|
866
|
+
client.translate(
|
|
867
|
+
'Hello world.\\nSee you at 5pm.',
|
|
868
|
+
target_lang='fr',
|
|
869
|
+
)
|
|
870
|
+
# {'translation': 'Bonjour le monde.\\nÀ 17h.',
|
|
871
|
+
# 'source_lang': 'en', 'source_lang_name': 'English',
|
|
872
|
+
# 'target_lang': 'fr', 'target_lang_name': 'French'}
|
|
873
|
+
|
|
874
|
+
:param text: Text to translate, up to 5,000 characters. Markup and
|
|
875
|
+
whitespace are preserved, not stripped. Pass a list or tuple of
|
|
876
|
+
1-10 strings to translate a batch in one request (sent as the
|
|
877
|
+
API's ``texts`` parameter; combined length up to 5,000
|
|
878
|
+
characters, same target/source/formality applied to every item).
|
|
879
|
+
A batch response is ``{'results': [...]}`` with one
|
|
880
|
+
single-response-shaped dict per input, in order.
|
|
881
|
+
:type text: str | list[str]
|
|
882
|
+
:param target_lang: Language to translate into: an ISO 639 code
|
|
883
|
+
(``'fr'``, ``'zh-TW'``) or an English language name
|
|
884
|
+
(``'French'``). Region variants ``zh-CN``/``zh-TW``/``zh-HK``,
|
|
885
|
+
``pt-BR``/``pt-PT``, ``en-GB``/``en-US``, ``fr-CA`` and ``es-419``
|
|
886
|
+
are supported as targets.
|
|
887
|
+
:type target_lang: str
|
|
888
|
+
:param source_lang: Optional hint for the source language, same forms
|
|
889
|
+
as ``target_lang``. The text itself wins if they disagree.
|
|
890
|
+
:type source_lang: str
|
|
891
|
+
:param formality: Optional register: ``'formal'`` (vous/Sie/usted) or
|
|
892
|
+
``'informal'`` (tu/du/tú). Omit for the model default.
|
|
893
|
+
:type formality: str
|
|
894
|
+
:rtype: dict
|
|
895
|
+
:return:
|
|
896
|
+
- translation: The translated text, structure preserved.
|
|
897
|
+
- source_lang / source_lang_name: The detected source language
|
|
898
|
+
(``'und'`` and ``None`` when undetermined).
|
|
899
|
+
- target_lang / target_lang_name: The normalized target.
|
|
900
|
+
'''
|
|
901
|
+
url = self.url_endpoint + 'translate'
|
|
902
|
+
data = {
|
|
903
|
+
'key': self.api_key,
|
|
904
|
+
'target_lang': target_lang,
|
|
905
|
+
}
|
|
906
|
+
# A list/tuple of texts is the batch form: one request, one result per
|
|
907
|
+
# item ({'results': [...]}), same options applied to every item.
|
|
908
|
+
if isinstance(text, (list, tuple)):
|
|
909
|
+
data['texts'] = list(text)
|
|
910
|
+
else:
|
|
911
|
+
data['text'] = text
|
|
912
|
+
if source_lang is not None:
|
|
913
|
+
data['source_lang'] = source_lang
|
|
914
|
+
if formality is not None:
|
|
915
|
+
data['formality'] = formality
|
|
916
|
+
return self._request(url, data)
|
|
917
|
+
|
|
721
918
|
def tone(
|
|
722
919
|
self,
|
|
723
920
|
text,
|
|
@@ -779,8 +976,13 @@ class SaplingClient:
|
|
|
779
976
|
|
|
780
977
|
:param text: Text to compute a quality score for. The base score is computed
|
|
781
978
|
on the leading ~4,000 characters; the rubric evaluates the whole text
|
|
782
|
-
(up to 20,000 characters).
|
|
783
|
-
|
|
979
|
+
(up to 20,000 characters). Pass a list or tuple of 1-10 strings to
|
|
980
|
+
score a batch in one request (sent as the API's ``texts`` parameter;
|
|
981
|
+
the same options apply to every item, and with ``rubric`` the
|
|
982
|
+
combined length is capped at 20,000 characters). A batch response is
|
|
983
|
+
``{'results': [...]}`` with one single-response-shaped dict per
|
|
984
|
+
input, in order.
|
|
985
|
+
:type text: str | list[str]
|
|
784
986
|
:param sentence_scores: If true, also return a 1-5 score for each sentence
|
|
785
987
|
under `sentences`.
|
|
786
988
|
:type sentence_scores: bool
|
|
@@ -799,8 +1001,13 @@ class SaplingClient:
|
|
|
799
1001
|
url = self.url_endpoint + 'quality'
|
|
800
1002
|
data = {
|
|
801
1003
|
'key': self.api_key,
|
|
802
|
-
'text': text,
|
|
803
1004
|
}
|
|
1005
|
+
# A list/tuple of texts is the batch form: one request, one result per
|
|
1006
|
+
# item ({'results': [...]}), same options applied to every item.
|
|
1007
|
+
if isinstance(text, (list, tuple)):
|
|
1008
|
+
data['texts'] = list(text)
|
|
1009
|
+
else:
|
|
1010
|
+
data['text'] = text
|
|
804
1011
|
if sentence_scores is not None:
|
|
805
1012
|
data['sentence_scores'] = sentence_scores
|
|
806
1013
|
if rubric is not None:
|
|
@@ -818,7 +1025,11 @@ class SaplingClient:
|
|
|
818
1025
|
candidates and a per-segment breakdown for mixed-language text.
|
|
819
1026
|
|
|
820
1027
|
:param text: Text to identify the language for (up to 20,000 characters).
|
|
821
|
-
|
|
1028
|
+
Pass a list or tuple of 1-10 strings to detect a batch in one
|
|
1029
|
+
request (sent as the API's ``texts`` parameter; combined length up
|
|
1030
|
+
to 20,000 characters). A batch response is ``{'results': [...]}``
|
|
1031
|
+
with one single-response-shaped dict per input, in order.
|
|
1032
|
+
:type text: str | list[str]
|
|
822
1033
|
:param top_k: Number of language candidates to return, 1-10. Defaults to 3 server-side.
|
|
823
1034
|
:type top_k: int
|
|
824
1035
|
:param segments: If true, also detect the language of each line/sentence
|
|
@@ -836,8 +1047,13 @@ class SaplingClient:
|
|
|
836
1047
|
url = self.url_endpoint + 'langdetect'
|
|
837
1048
|
data = {
|
|
838
1049
|
'key': self.api_key,
|
|
839
|
-
'text': text,
|
|
840
1050
|
}
|
|
1051
|
+
# A list/tuple of texts is the batch form: one request, one result per
|
|
1052
|
+
# item ({'results': [...]}), same options applied to every item.
|
|
1053
|
+
if isinstance(text, (list, tuple)):
|
|
1054
|
+
data['texts'] = list(text)
|
|
1055
|
+
else:
|
|
1056
|
+
data['text'] = text
|
|
841
1057
|
if top_k is not None:
|
|
842
1058
|
data['top_k'] = top_k
|
|
843
1059
|
if segments is not None:
|
|
@@ -875,8 +1091,12 @@ class SaplingClient:
|
|
|
875
1091
|
Scores the provided text on seven content-safety categories: toxicity,
|
|
876
1092
|
profanity, harassment, hate_speech, self_harm, sexual, and violence.
|
|
877
1093
|
|
|
878
|
-
:param text: Text to score, up to 20,000 characters.
|
|
879
|
-
|
|
1094
|
+
:param text: Text to score, up to 20,000 characters. Pass a list or
|
|
1095
|
+
tuple of 1-10 strings to score a batch in one request (sent as the
|
|
1096
|
+
API's ``texts`` parameter; combined length up to 20,000
|
|
1097
|
+
characters). A batch response is ``{'results': [...]}`` with one
|
|
1098
|
+
single-response-shaped dict per input, in order.
|
|
1099
|
+
:type text: str | list[str]
|
|
880
1100
|
:param threshold: Score at or above which a category is flagged.
|
|
881
1101
|
Between 0 and 1 inclusive; the API defaults to 0.5.
|
|
882
1102
|
:type threshold: float
|
|
@@ -897,8 +1117,13 @@ class SaplingClient:
|
|
|
897
1117
|
url = self.url_endpoint + 'safety'
|
|
898
1118
|
data = {
|
|
899
1119
|
'key': self.api_key,
|
|
900
|
-
'text': text,
|
|
901
1120
|
}
|
|
1121
|
+
# A list/tuple of texts is the batch form: one request, one result per
|
|
1122
|
+
# item ({'results': [...]}), same options applied to every item.
|
|
1123
|
+
if isinstance(text, (list, tuple)):
|
|
1124
|
+
data['texts'] = list(text)
|
|
1125
|
+
else:
|
|
1126
|
+
data['text'] = text
|
|
902
1127
|
if threshold is not None:
|
|
903
1128
|
data['threshold'] = threshold
|
|
904
1129
|
if spans is not None:
|
|
@@ -1047,8 +1272,12 @@ class SaplingClient:
|
|
|
1047
1272
|
# 'multi_label': False}
|
|
1048
1273
|
|
|
1049
1274
|
:param text: Text to classify, plain text or HTML (tags are stripped), up to
|
|
1050
|
-
10,000 characters.
|
|
1051
|
-
|
|
1275
|
+
10,000 characters. Pass a list or tuple of 1-10 strings to classify a
|
|
1276
|
+
batch in one request (sent as the API's ``texts`` parameter; combined
|
|
1277
|
+
length up to 10,000 characters). A batch response is
|
|
1278
|
+
``{'results': [...]}`` with one single-response-shaped dict per input,
|
|
1279
|
+
in order.
|
|
1280
|
+
:type text: str | list[str]
|
|
1052
1281
|
:param labels: 2-20 candidate labels. Each entry is either a label name (str) or
|
|
1053
1282
|
a dict ``{'name': str, 'description': str}`` where ``description`` is
|
|
1054
1283
|
optional. Names are up to 50 characters and must be unique
|
|
@@ -1090,9 +1319,14 @@ class SaplingClient:
|
|
|
1090
1319
|
url = self.url_endpoint + 'classify'
|
|
1091
1320
|
data = {
|
|
1092
1321
|
'key': self.api_key,
|
|
1093
|
-
'text': text,
|
|
1094
1322
|
'labels': list(labels),
|
|
1095
1323
|
}
|
|
1324
|
+
# A list/tuple of texts is the batch form: one request, one result per
|
|
1325
|
+
# item ({'results': [...]}), same labels applied to every item.
|
|
1326
|
+
if isinstance(text, (list, tuple)):
|
|
1327
|
+
data['texts'] = list(text)
|
|
1328
|
+
else:
|
|
1329
|
+
data['text'] = text
|
|
1096
1330
|
if multi_label is not None:
|
|
1097
1331
|
data['multi_label'] = multi_label
|
|
1098
1332
|
if threshold is not None:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '4.11.0'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sapling-py
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.11.0
|
|
4
4
|
Summary: Sapling Python Client
|
|
5
5
|
Author-email: Sapling Intelligence <info@sapling.ai>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -31,8 +31,9 @@ Compare against leading grammar checking tools and APIs like Grammarly (Grammerl
|
|
|
31
31
|
|
|
32
32
|
Sapling is a toolkit for helping developers build language model-powered applications.
|
|
33
33
|
The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection,
|
|
34
|
-
zero-shot text classification, structured data extraction, PII detection
|
|
35
|
-
|
|
34
|
+
zero-shot text classification, structured data extraction, translation, PII detection
|
|
35
|
+
and redaction, content safety scoring, plain-language
|
|
36
|
+
simplification, SEO analysis, and more.
|
|
36
37
|
|
|
37
38
|
Key Features:
|
|
38
39
|
- **60% More Corrections**: Outperforms [competing solutions](https://sapling.ai/comparison/api#sapling) at similar accuracy levels using state-of-the-art generative AI models.
|
|
@@ -94,6 +95,20 @@ Get a production key by following [this documentation](https://sapling.ai/docs/a
|
|
|
94
95
|
|
|
95
96
|
Here's some more information on [request options and response structure](https://sapling.ai/docs/api/edits-overview).
|
|
96
97
|
|
|
98
|
+
## Batch requests
|
|
99
|
+
|
|
100
|
+
The analysis endpoints — `classify`, `extract`, `safety`, `langdetect`,
|
|
101
|
+
`translate`, `summarize`, `quality` and `aidetect` — also accept a list of
|
|
102
|
+
1-10 texts in
|
|
103
|
+
place of a single string. The batch is sent as one request (the same options
|
|
104
|
+
apply to every item) and returns `{'results': [...]}` with one
|
|
105
|
+
single-response-shaped dict per input, in order:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
scores = client.quality(['First draft.', 'Second draft.'], sentence_scores=True)
|
|
109
|
+
langs = client.langdetect(['Bonjour le monde', 'Hello world'])
|
|
110
|
+
```
|
|
111
|
+
|
|
97
112
|
## License
|
|
98
113
|
|
|
99
114
|
Copyright 2024--present Sapling Intelligence, Inc.
|
|
@@ -198,3 +198,26 @@ def test_classify_custom_hostname_and_pathname():
|
|
|
198
198
|
def test_classify_rejects_non_list_labels(client, bad_labels):
|
|
199
199
|
with pytest.raises(TypeError):
|
|
200
200
|
client.classify('I was charged twice.', bad_labels)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@responses.activate
|
|
204
|
+
def test_classify_batch_list_sent_as_texts(client):
|
|
205
|
+
# A list of texts is the batch form (SAP-395): sent as `texts`, no `text`
|
|
206
|
+
# key, and the {'results': [...]} body comes back untouched.
|
|
207
|
+
batch_response = {'results': [CLASSIFY_RESPONSE, MULTI_LABEL_RESPONSE]}
|
|
208
|
+
responses.add(responses.POST, BASE + 'classify', json=batch_response, status=200)
|
|
209
|
+
result = client.classify(['first ticket', 'second ticket'], LABELS)
|
|
210
|
+
assert result == batch_response
|
|
211
|
+
body = _last_request_body()
|
|
212
|
+
assert body == {'key': API_KEY, 'texts': ['first ticket', 'second ticket'],
|
|
213
|
+
'labels': LABELS}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@responses.activate
|
|
217
|
+
def test_classify_batch_tuple_sent_as_list(client):
|
|
218
|
+
responses.add(responses.POST, BASE + 'classify',
|
|
219
|
+
json={'results': [CLASSIFY_RESPONSE]}, status=200)
|
|
220
|
+
client.classify(('only ticket',), LABELS)
|
|
221
|
+
body = _last_request_body()
|
|
222
|
+
assert body['texts'] == ['only ticket']
|
|
223
|
+
assert 'text' not in body
|
|
@@ -168,6 +168,31 @@ def test_langdetect_options(client):
|
|
|
168
168
|
assert _last_request_body() == {'key': API_KEY, 'text': 'Hello', 'top_k': 1, 'segments': True}
|
|
169
169
|
|
|
170
170
|
|
|
171
|
+
@responses.activate
|
|
172
|
+
def test_langdetect_batch_list_sent_as_texts(client):
|
|
173
|
+
# A list of texts is the batch form (SAP-397): sent as `texts`, no `text`
|
|
174
|
+
# key, and the {'results': [...]} body comes back untouched.
|
|
175
|
+
batch_response = {'results': [
|
|
176
|
+
{'lang': 'en', 'name': 'English', 'conf': 0.99, 'candidates': []},
|
|
177
|
+
{'lang': 'es', 'name': 'Spanish', 'conf': 0.97, 'candidates': []},
|
|
178
|
+
]}
|
|
179
|
+
responses.add(responses.POST, BASE + 'langdetect', json=batch_response, status=200)
|
|
180
|
+
result = client.langdetect(['Hello there.', 'Hola.'], top_k=1)
|
|
181
|
+
assert result == batch_response
|
|
182
|
+
assert _last_request_body() == {'key': API_KEY, 'texts': ['Hello there.', 'Hola.'],
|
|
183
|
+
'top_k': 1}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@responses.activate
|
|
187
|
+
def test_langdetect_batch_tuple_sent_as_list(client):
|
|
188
|
+
responses.add(responses.POST, BASE + 'langdetect',
|
|
189
|
+
json={'results': [{'lang': 'en'}]}, status=200)
|
|
190
|
+
client.langdetect(('Hello there.',))
|
|
191
|
+
body = _last_request_body()
|
|
192
|
+
assert body['texts'] == ['Hello there.']
|
|
193
|
+
assert 'text' not in body
|
|
194
|
+
|
|
195
|
+
|
|
171
196
|
@responses.activate
|
|
172
197
|
def test_hostname_and_pathname_override():
|
|
173
198
|
client = SaplingClient(api_key=API_KEY, hostname='http://localhost:5000', pathname='/api/v1/')
|
|
@@ -193,3 +218,100 @@ def test_quality_options(client):
|
|
|
193
218
|
assert result == payload
|
|
194
219
|
assert _last_request_body() == {'key': API_KEY, 'text': 'Hello',
|
|
195
220
|
'sentence_scores': True, 'rubric': True}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@responses.activate
|
|
224
|
+
def test_quality_batch_list_sent_as_texts(client):
|
|
225
|
+
# A list of texts is the batch form (SAP-400): sent as `texts`, no `text`
|
|
226
|
+
# key, the same options applied to every item, and the {'results': [...]}
|
|
227
|
+
# body comes back untouched.
|
|
228
|
+
batch_response = {'results': [
|
|
229
|
+
{'score': 4.1},
|
|
230
|
+
{'score': 2.3},
|
|
231
|
+
]}
|
|
232
|
+
responses.add(responses.POST, BASE + 'quality', json=batch_response, status=200)
|
|
233
|
+
result = client.quality(['First document.', 'Second document.'],
|
|
234
|
+
sentence_scores=True)
|
|
235
|
+
assert result == batch_response
|
|
236
|
+
assert _last_request_body() == {'key': API_KEY,
|
|
237
|
+
'texts': ['First document.', 'Second document.'],
|
|
238
|
+
'sentence_scores': True}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@responses.activate
|
|
242
|
+
def test_quality_batch_tuple_sent_as_list(client):
|
|
243
|
+
responses.add(responses.POST, BASE + 'quality',
|
|
244
|
+
json={'results': [{'score': 3.0}]}, status=200)
|
|
245
|
+
client.quality(('First document.',))
|
|
246
|
+
body = _last_request_body()
|
|
247
|
+
assert body['texts'] == ['First document.']
|
|
248
|
+
assert 'text' not in body
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@responses.activate
|
|
252
|
+
def test_summarize_length_sent_only_when_given(client):
|
|
253
|
+
payload = {'result': 'Short.', 'summary': 'Short.', 'key_points': ['One.'],
|
|
254
|
+
'length': 'short'}
|
|
255
|
+
responses.add(responses.POST, BASE + 'summarize', json=payload, status=200)
|
|
256
|
+
result = client.summarize('A long document.', length='short')
|
|
257
|
+
assert result == payload
|
|
258
|
+
assert _last_request_body() == {'key': API_KEY, 'text': 'A long document.',
|
|
259
|
+
'length': 'short'}
|
|
260
|
+
|
|
261
|
+
responses.add(responses.POST, BASE + 'summarize', json=payload, status=200)
|
|
262
|
+
client.summarize('A long document.')
|
|
263
|
+
assert 'length' not in _last_request_body()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
@responses.activate
|
|
267
|
+
def test_summarize_batch_list_sent_as_texts(client):
|
|
268
|
+
# A list of texts is the batch form (SAP-399): sent as `texts`, no `text`
|
|
269
|
+
# key, and the {'results': [...]} body comes back untouched.
|
|
270
|
+
batch_response = {'results': [
|
|
271
|
+
{'result': 'One.', 'summary': 'One.', 'key_points': ['A.'], 'length': 'medium'},
|
|
272
|
+
{'result': 'Two.', 'summary': 'Two.', 'key_points': ['B.'], 'length': 'medium'},
|
|
273
|
+
]}
|
|
274
|
+
responses.add(responses.POST, BASE + 'summarize', json=batch_response, status=200)
|
|
275
|
+
result = client.summarize(['First document.', 'Second document.'])
|
|
276
|
+
assert result == batch_response
|
|
277
|
+
assert _last_request_body() == {'key': API_KEY,
|
|
278
|
+
'texts': ['First document.', 'Second document.']}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
@responses.activate
|
|
282
|
+
def test_summarize_batch_tuple_sent_as_list(client):
|
|
283
|
+
responses.add(responses.POST, BASE + 'summarize',
|
|
284
|
+
json={'results': [{'result': 'One.'}]}, status=200)
|
|
285
|
+
client.summarize(('First document.',), length='long')
|
|
286
|
+
body = _last_request_body()
|
|
287
|
+
assert body['texts'] == ['First document.']
|
|
288
|
+
assert body['length'] == 'long'
|
|
289
|
+
assert 'text' not in body
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@responses.activate
|
|
293
|
+
def test_aidetect_batch_list_sent_as_texts(client):
|
|
294
|
+
# A list of texts is the batch form (SAP-403): sent as `texts`, no `text`
|
|
295
|
+
# key, and the {'results': [...]} body comes back untouched.
|
|
296
|
+
batch_response = {'results': [
|
|
297
|
+
{'score': 0.91, 'text': 'Almost certainly generated.'},
|
|
298
|
+
{'score': 0.08, 'text': 'Plainly human prose.'},
|
|
299
|
+
]}
|
|
300
|
+
responses.add(responses.POST, BASE + 'aidetect', json=batch_response, status=200)
|
|
301
|
+
result = client.aidetect(['Almost certainly generated.', 'Plainly human prose.'],
|
|
302
|
+
sent_scores=False)
|
|
303
|
+
assert result == batch_response
|
|
304
|
+
assert _last_request_body() == {'key': API_KEY,
|
|
305
|
+
'texts': ['Almost certainly generated.',
|
|
306
|
+
'Plainly human prose.'],
|
|
307
|
+
'sent_scores': False}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@responses.activate
|
|
311
|
+
def test_aidetect_batch_tuple_sent_as_list(client):
|
|
312
|
+
responses.add(responses.POST, BASE + 'aidetect',
|
|
313
|
+
json={'results': [{'score': 0.5}]}, status=200)
|
|
314
|
+
client.aidetect(('One draft.',))
|
|
315
|
+
body = _last_request_body()
|
|
316
|
+
assert body['texts'] == ['One draft.']
|
|
317
|
+
assert 'text' not in body
|
|
@@ -211,3 +211,27 @@ def test_extract_accepts_any_iterable_of_fields(client):
|
|
|
211
211
|
responses.add(responses.POST, BASE + 'extract', json=EXTRACT_RESPONSE, status=200)
|
|
212
212
|
client.extract(TEXT, (name for name in ['invoice_number', 'total']))
|
|
213
213
|
assert _last_request_body()['fields'] == ['invoice_number', 'total']
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@responses.activate
|
|
217
|
+
def test_extract_batch_list_sent_as_texts(client):
|
|
218
|
+
# A list of texts is the batch form (SAP-395): sent as `texts`, no `text`
|
|
219
|
+
# key, and the {'results': [...]} body comes back untouched.
|
|
220
|
+
batch_response = {'results': [EXTRACT_RESPONSE, EXTRACT_RESPONSE]}
|
|
221
|
+
responses.add(responses.POST, BASE + 'extract', json=batch_response, status=200)
|
|
222
|
+
result = client.extract([TEXT, 'Invoice INV-2077 for Globex.'], FIELDS)
|
|
223
|
+
assert result == batch_response
|
|
224
|
+
body = _last_request_body()
|
|
225
|
+
assert body['texts'] == [TEXT, 'Invoice INV-2077 for Globex.']
|
|
226
|
+
assert 'text' not in body
|
|
227
|
+
assert body['fields'] == FIELDS
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@responses.activate
|
|
231
|
+
def test_extract_batch_tuple_sent_as_list(client):
|
|
232
|
+
responses.add(responses.POST, BASE + 'extract',
|
|
233
|
+
json={'results': [EXTRACT_RESPONSE]}, status=200)
|
|
234
|
+
client.extract((TEXT,), FIELDS)
|
|
235
|
+
body = _last_request_body()
|
|
236
|
+
assert body['texts'] == [TEXT]
|
|
237
|
+
assert 'text' not in body
|
|
@@ -54,3 +54,26 @@ def test_safety_spans_omitted_by_default(client):
|
|
|
54
54
|
client.safety('some text')
|
|
55
55
|
body = json.loads(responses.calls[-1].request.body)
|
|
56
56
|
assert 'spans' not in body
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@responses.activate
|
|
60
|
+
def test_safety_batch_list_sent_as_texts(client):
|
|
61
|
+
# A list of texts is the batch form (SAP-397): sent as `texts`, no `text`
|
|
62
|
+
# key, and the {'results': [...]} body comes back untouched.
|
|
63
|
+
batch_response = {'results': [{'flagged': True}, {'flagged': False}]}
|
|
64
|
+
responses.add(responses.POST, BASE + 'safety', json=batch_response, status=200)
|
|
65
|
+
result = client.safety(['first message', 'second message'], threshold=0.7)
|
|
66
|
+
assert result == batch_response
|
|
67
|
+
body = json.loads(responses.calls[-1].request.body)
|
|
68
|
+
assert body == {'key': API_KEY, 'texts': ['first message', 'second message'],
|
|
69
|
+
'threshold': 0.7}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@responses.activate
|
|
73
|
+
def test_safety_batch_tuple_sent_as_list(client):
|
|
74
|
+
responses.add(responses.POST, BASE + 'safety',
|
|
75
|
+
json={'results': [{'flagged': False}]}, status=200)
|
|
76
|
+
client.safety(('only message',))
|
|
77
|
+
body = json.loads(responses.calls[-1].request.body)
|
|
78
|
+
assert body['texts'] == ['only message']
|
|
79
|
+
assert 'text' not in body
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import responses
|
|
5
|
+
|
|
6
|
+
from sapling import SaplingClient
|
|
7
|
+
|
|
8
|
+
API_KEY = 'a' * 32
|
|
9
|
+
BASE = 'https://api.sapling.ai/api/v1/'
|
|
10
|
+
|
|
11
|
+
TEXT = ('The party of the first part shall remit payment within thirty (30) '
|
|
12
|
+
'days of receipt of the invoice.')
|
|
13
|
+
|
|
14
|
+
SIMPLIFY_RESPONSE = {
|
|
15
|
+
'simplified': 'You must pay within 30 days of getting the invoice.',
|
|
16
|
+
'reading_level': 'plain',
|
|
17
|
+
'lang': 'en',
|
|
18
|
+
'readability': {
|
|
19
|
+
'before': {'grade': 12.3, 'ease': 42.1},
|
|
20
|
+
'after': {'grade': 5.8, 'ease': 78.4},
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def client():
|
|
27
|
+
return SaplingClient(api_key=API_KEY)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _last_request_body():
|
|
31
|
+
return json.loads(responses.calls[-1].request.body)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@responses.activate
|
|
35
|
+
def test_simplify_returns_json_and_sends_key_and_text(client):
|
|
36
|
+
responses.add(responses.POST, BASE + 'simplify',
|
|
37
|
+
json=SIMPLIFY_RESPONSE, status=200)
|
|
38
|
+
result = client.simplify(TEXT)
|
|
39
|
+
assert result == SIMPLIFY_RESPONSE
|
|
40
|
+
body = _last_request_body()
|
|
41
|
+
assert body == {'key': API_KEY, 'text': TEXT}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@responses.activate
|
|
45
|
+
def test_simplify_posts_to_simplify_endpoint(client):
|
|
46
|
+
responses.add(responses.POST, BASE + 'simplify',
|
|
47
|
+
json=SIMPLIFY_RESPONSE, status=200)
|
|
48
|
+
client.simplify(TEXT)
|
|
49
|
+
assert responses.calls[-1].request.url == BASE + 'simplify'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@responses.activate
|
|
53
|
+
def test_simplify_omits_optional_arguments_when_not_given(client):
|
|
54
|
+
responses.add(responses.POST, BASE + 'simplify',
|
|
55
|
+
json=SIMPLIFY_RESPONSE, status=200)
|
|
56
|
+
client.simplify(TEXT)
|
|
57
|
+
body = _last_request_body()
|
|
58
|
+
assert 'reading_level' not in body
|
|
59
|
+
assert 'preserve_terms' not in body
|
|
60
|
+
assert 'lang' not in body
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@responses.activate
|
|
64
|
+
def test_simplify_sends_optional_arguments(client):
|
|
65
|
+
responses.add(responses.POST, BASE + 'simplify',
|
|
66
|
+
json=SIMPLIFY_RESPONSE, status=200)
|
|
67
|
+
client.simplify(TEXT, reading_level='middle_school',
|
|
68
|
+
preserve_terms=['invoice', 'Sapling'], lang='en')
|
|
69
|
+
body = _last_request_body()
|
|
70
|
+
assert body['reading_level'] == 'middle_school'
|
|
71
|
+
assert body['preserve_terms'] == ['invoice', 'Sapling']
|
|
72
|
+
assert body['lang'] == 'en'
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@responses.activate
|
|
76
|
+
def test_simplify_accepts_a_tuple_of_preserve_terms(client):
|
|
77
|
+
responses.add(responses.POST, BASE + 'simplify',
|
|
78
|
+
json=SIMPLIFY_RESPONSE, status=200)
|
|
79
|
+
client.simplify(TEXT, preserve_terms=('invoice',))
|
|
80
|
+
assert _last_request_body()['preserve_terms'] == ['invoice']
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_simplify_rejects_non_list_preserve_terms(client):
|
|
84
|
+
for bad in ('invoice', b'invoice', {'term': 'invoice'}, 5):
|
|
85
|
+
with pytest.raises(TypeError):
|
|
86
|
+
client.simplify(TEXT, preserve_terms=bad)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@responses.activate
|
|
90
|
+
def test_simplify_http_error_raises(client):
|
|
91
|
+
responses.add(responses.POST, BASE + 'simplify',
|
|
92
|
+
json={'msg': 'Unexpected error simplifying text.'},
|
|
93
|
+
status=502)
|
|
94
|
+
with pytest.raises(Exception):
|
|
95
|
+
client.simplify(TEXT)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
import responses
|
|
5
|
+
|
|
6
|
+
from sapling import SaplingClient, SaplingError
|
|
7
|
+
|
|
8
|
+
API_KEY = 'a' * 32
|
|
9
|
+
BASE = 'https://api.sapling.ai/api/v1/'
|
|
10
|
+
|
|
11
|
+
TEXT = 'Hello world.\nSee you at 5pm.'
|
|
12
|
+
|
|
13
|
+
TRANSLATE_RESPONSE = {
|
|
14
|
+
'translation': 'Bonjour le monde.\nÀ 17h.',
|
|
15
|
+
'source_lang': 'en',
|
|
16
|
+
'source_lang_name': 'English',
|
|
17
|
+
'target_lang': 'fr',
|
|
18
|
+
'target_lang_name': 'French',
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
def client():
|
|
24
|
+
return SaplingClient(api_key=API_KEY)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _last_request_body():
|
|
28
|
+
return json.loads(responses.calls[-1].request.body)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@responses.activate
|
|
32
|
+
def test_translate_returns_json_and_sends_key(client):
|
|
33
|
+
responses.add(responses.POST, BASE + 'translate',
|
|
34
|
+
json=TRANSLATE_RESPONSE, status=200)
|
|
35
|
+
result = client.translate(TEXT, target_lang='fr')
|
|
36
|
+
assert result == TRANSLATE_RESPONSE
|
|
37
|
+
body = _last_request_body()
|
|
38
|
+
assert body == {'key': API_KEY, 'text': TEXT, 'target_lang': 'fr'}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@responses.activate
|
|
42
|
+
def test_translate_posts_to_translate_endpoint(client):
|
|
43
|
+
responses.add(responses.POST, BASE + 'translate',
|
|
44
|
+
json=TRANSLATE_RESPONSE, status=200)
|
|
45
|
+
client.translate(TEXT, target_lang='fr')
|
|
46
|
+
assert responses.calls[-1].request.url == BASE + 'translate'
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@responses.activate
|
|
50
|
+
def test_translate_optional_params_sent_only_when_given(client):
|
|
51
|
+
responses.add(responses.POST, BASE + 'translate',
|
|
52
|
+
json=TRANSLATE_RESPONSE, status=200)
|
|
53
|
+
client.translate(TEXT, target_lang='fr')
|
|
54
|
+
body = _last_request_body()
|
|
55
|
+
assert 'source_lang' not in body
|
|
56
|
+
assert 'formality' not in body
|
|
57
|
+
|
|
58
|
+
client.translate(TEXT, target_lang='fr',
|
|
59
|
+
source_lang='en', formality='formal')
|
|
60
|
+
body = _last_request_body()
|
|
61
|
+
assert body['source_lang'] == 'en'
|
|
62
|
+
assert body['formality'] == 'formal'
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@responses.activate
|
|
66
|
+
def test_translate_accepts_names_and_region_variants(client):
|
|
67
|
+
responses.add(responses.POST, BASE + 'translate',
|
|
68
|
+
json=dict(TRANSLATE_RESPONSE, target_lang='zh-TW',
|
|
69
|
+
target_lang_name='Traditional Chinese'),
|
|
70
|
+
status=200)
|
|
71
|
+
result = client.translate(TEXT, target_lang='Traditional Chinese')
|
|
72
|
+
assert result['target_lang'] == 'zh-TW'
|
|
73
|
+
assert _last_request_body()['target_lang'] == 'Traditional Chinese'
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@responses.activate
|
|
77
|
+
def test_translate_http_error_raises_sapling_error(client):
|
|
78
|
+
responses.add(responses.POST, BASE + 'translate',
|
|
79
|
+
json={'msg': 'Unknown language: "Klingon".'}, status=400)
|
|
80
|
+
with pytest.raises(SaplingError):
|
|
81
|
+
client.translate(TEXT, target_lang='Klingon')
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@responses.activate
|
|
85
|
+
def test_translate_batch_list_sent_as_texts(client):
|
|
86
|
+
# A list of texts is the batch form (SAP-398): sent as `texts`, no `text`
|
|
87
|
+
# key, and the {'results': [...]} body comes back untouched.
|
|
88
|
+
batch_response = {'results': [TRANSLATE_RESPONSE,
|
|
89
|
+
dict(TRANSLATE_RESPONSE,
|
|
90
|
+
translation='À demain.')]}
|
|
91
|
+
responses.add(responses.POST, BASE + 'translate',
|
|
92
|
+
json=batch_response, status=200)
|
|
93
|
+
result = client.translate(['Hello world.', 'See you tomorrow.'],
|
|
94
|
+
target_lang='fr', formality='formal')
|
|
95
|
+
assert result == batch_response
|
|
96
|
+
body = _last_request_body()
|
|
97
|
+
assert body == {'key': API_KEY,
|
|
98
|
+
'texts': ['Hello world.', 'See you tomorrow.'],
|
|
99
|
+
'target_lang': 'fr', 'formality': 'formal'}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@responses.activate
|
|
103
|
+
def test_translate_batch_tuple_sent_as_list(client):
|
|
104
|
+
responses.add(responses.POST, BASE + 'translate',
|
|
105
|
+
json={'results': [TRANSLATE_RESPONSE]}, status=200)
|
|
106
|
+
client.translate(('Hello world.',), target_lang='fr')
|
|
107
|
+
body = _last_request_body()
|
|
108
|
+
assert body['texts'] == ['Hello world.']
|
|
109
|
+
assert 'text' not in body
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '4.3.0'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|