sapling-py 4.1.0__tar.gz → 4.3.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.
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sapling-py
3
- Version: 4.1.0
3
+ Version: 4.3.0
4
4
  Summary: Sapling Python Client
5
- Home-page: https://sapling.ai
6
- Author: Sapling Intelligence
7
- Author-email: info@sapling.ai
5
+ Author-email: Sapling Intelligence <info@sapling.ai>
8
6
  License: Apache License 2.0
7
+ Project-URL: Homepage, https://sapling.ai
8
+ Project-URL: Documentation, https://sapling.readthedocs.io/
9
+ Project-URL: Repository, https://github.com/saplingai/sapling-py
9
10
  Classifier: Development Status :: 5 - Production/Stable
10
11
  Classifier: License :: OSI Approved :: Apache Software License
11
12
  Classifier: Programming Language :: Python :: 3
@@ -17,18 +18,7 @@ Requires-Dist: requests
17
18
  Provides-Extra: test
18
19
  Requires-Dist: pytest; extra == "test"
19
20
  Requires-Dist: responses; extra == "test"
20
- Dynamic: author
21
- Dynamic: author-email
22
- Dynamic: classifier
23
- Dynamic: description
24
- Dynamic: description-content-type
25
- Dynamic: home-page
26
- Dynamic: license
27
21
  Dynamic: license-file
28
- Dynamic: provides-extra
29
- Dynamic: requires-dist
30
- Dynamic: requires-python
31
- Dynamic: summary
32
22
 
33
23
  # Sapling Python Client
34
24
 
@@ -40,7 +30,9 @@ Compare against leading grammar checking tools and APIs like Grammarly (Grammerl
40
30
  ## Overview
41
31
 
42
32
  Sapling is a toolkit for helping developers build language model-powered applications.
43
- The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection, and more.
33
+ The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection,
34
+ zero-shot text classification, structured data extraction, PII detection and redaction, content safety
35
+ scoring, SEO analysis, and more.
44
36
 
45
37
  Key Features:
46
38
  - **60% More Corrections**: Outperforms [competing solutions](https://sapling.ai/comparison/api#sapling) at similar accuracy levels using state-of-the-art generative AI models.
@@ -52,12 +44,19 @@ Key Features:
52
44
 
53
45
  ## Installation
54
46
 
55
- Install the `sapling-py` package with [pip](https://pip.pypa.io/en/stable/installation/)
47
+ Install the `sapling-py` package from PyPI with
48
+ [pip](https://pip.pypa.io/en/stable/installation/):
56
49
 
57
- ```
50
+ ```sh
58
51
  python -m pip install sapling-py
59
52
  ```
60
53
 
54
+ Or add it to a [uv](https://docs.astral.sh/uv/) project:
55
+
56
+ ```sh
57
+ uv add sapling-py
58
+ ```
59
+
61
60
  ## Documentation
62
61
  Documentation for the client is available at [Read the Docs](https://sapling.readthedocs.io/) and
63
62
  documentation for the HTTP API is available at [Sapling.ai](https://sapling.ai/docs).
@@ -8,7 +8,9 @@ Compare against leading grammar checking tools and APIs like Grammarly (Grammerl
8
8
  ## Overview
9
9
 
10
10
  Sapling is a toolkit for helping developers build language model-powered applications.
11
- The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection, and more.
11
+ The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection,
12
+ zero-shot text classification, structured data extraction, PII detection and redaction, content safety
13
+ scoring, SEO analysis, and more.
12
14
 
13
15
  Key Features:
14
16
  - **60% More Corrections**: Outperforms [competing solutions](https://sapling.ai/comparison/api#sapling) at similar accuracy levels using state-of-the-art generative AI models.
@@ -20,12 +22,19 @@ Key Features:
20
22
 
21
23
  ## Installation
22
24
 
23
- Install the `sapling-py` package with [pip](https://pip.pypa.io/en/stable/installation/)
25
+ Install the `sapling-py` package from PyPI with
26
+ [pip](https://pip.pypa.io/en/stable/installation/):
24
27
 
25
- ```
28
+ ```sh
26
29
  python -m pip install sapling-py
27
30
  ```
28
31
 
32
+ Or add it to a [uv](https://docs.astral.sh/uv/) project:
33
+
34
+ ```sh
35
+ uv add sapling-py
36
+ ```
37
+
29
38
  ## Documentation
30
39
  Documentation for the client is available at [Read the Docs](https://sapling.readthedocs.io/) and
31
40
  documentation for the HTTP API is available at [Sapling.ai](https://sapling.ai/docs).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sapling-py"
7
+ dynamic = ["version"]
8
+ description = "Sapling Python Client"
9
+ readme = "README.md"
10
+ requires-python = ">=3.7"
11
+ license = { text = "Apache License 2.0" }
12
+ authors = [
13
+ { name = "Sapling Intelligence", email = "info@sapling.ai" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 5 - Production/Stable",
17
+ "License :: OSI Approved :: Apache Software License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ ]
21
+ dependencies = [
22
+ "requests",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ test = [
27
+ "pytest",
28
+ "responses",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://sapling.ai"
33
+ Documentation = "https://sapling.readthedocs.io/"
34
+ Repository = "https://github.com/saplingai/sapling-py"
35
+
36
+ [tool.setuptools.dynamic]
37
+ version = { attr = "sapling.version.__version__" }
38
+
39
+ [tool.setuptools.packages.find]
40
+ include = ["sapling", "sapling.*"]
41
+ exclude = ["test", "test.*"]
42
+ namespaces = false
@@ -1,5 +1,6 @@
1
1
  import requests
2
2
  import uuid
3
+ from collections.abc import Iterable
3
4
 
4
5
 
5
6
  class SaplingError(Exception):
@@ -631,6 +632,92 @@ class SaplingClient:
631
632
  }
632
633
  return self._request(url, data)
633
634
 
635
+ def extract(
636
+ self,
637
+ text,
638
+ fields,
639
+ context=None,
640
+ ):
641
+ '''
642
+ Extracts structured data from unstructured text: you name the fields you
643
+ want, the API returns their values pulled from the document. Every value is
644
+ grounded in a verbatim span of the input, so this is extraction rather than
645
+ summarization or inference; a field the text does not state is reported
646
+ missing rather than guessed.
647
+
648
+ Example::
649
+
650
+ client.extract(
651
+ 'Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.',
652
+ fields=[
653
+ 'invoice_number',
654
+ {'name': 'total', 'type': 'number', 'description': 'Amount due',
655
+ 'required': True},
656
+ {'name': 'due_date', 'type': 'date'},
657
+ 'purchase_order',
658
+ ],
659
+ context='A vendor invoice',
660
+ )
661
+ # {'data': {'invoice_number': 'INV-1042', 'total': 1299.0,
662
+ # 'due_date': '2026-03-05', 'purchase_order': None},
663
+ # 'fields': [{'name': 'invoice_number', 'type': 'string',
664
+ # 'value': 'INV-1042', 'evidence': 'Invoice INV-1042',
665
+ # 'found': True},
666
+ # {'name': 'total', 'type': 'number', 'value': 1299.0,
667
+ # 'evidence': 'total $1,299.00', 'found': True},
668
+ # {'name': 'due_date', 'type': 'date', 'value': '2026-03-05',
669
+ # 'evidence': 'due March 5, 2026', 'found': True},
670
+ # {'name': 'purchase_order', 'type': 'string', 'value': None,
671
+ # 'evidence': '', 'found': False}],
672
+ # 'missing': ['purchase_order']}
673
+
674
+ :param text: Document to extract from, plain text or HTML (tags are
675
+ stripped), up to 10,000 characters.
676
+ :type text: str
677
+ :param fields: 1-20 fields to extract. Each entry is either a field name
678
+ (str) or a dict ``{'name': str, 'type': str, 'description': str,
679
+ 'required': bool}`` where everything but ``name`` is optional. Names are
680
+ up to 50 characters and must be unique (case-insensitive). ``type`` is
681
+ one of ``'string'`` (the default), ``'number'``, ``'integer'``,
682
+ ``'boolean'``, ``'date'`` or ``'list'``. ``description`` is up to 200
683
+ characters and sharpens ambiguous fields. ``required`` is a hint to the
684
+ model; a required field the text does not state is still reported
685
+ missing, never guessed.
686
+ :type fields: list[str | dict]
687
+ :raises TypeError: If ``fields`` is None, a single string or a dict rather
688
+ than a list/tuple of fields.
689
+ :param context: Up to 500 characters describing what the document is or how
690
+ to read ambiguous fields (e.g. ``'A vendor invoice'``).
691
+ :type context: str
692
+ :rtype: dict
693
+ :return:
694
+ - data: ``{field name: value}`` map. Every requested field is present,
695
+ with ``None`` where the text did not state it. ``date`` values are
696
+ ``YYYY-MM-DD`` strings and ``list`` values are lists of strings.
697
+ - fields: One dict per requested field, in the order requested, with
698
+ ``name``, ``type``, ``value``, the verbatim ``evidence`` span the value
699
+ came from (``''`` when not found) and ``found``.
700
+ - missing: Names of the fields the text did not yield, in request order.
701
+ Finding nothing is a normal, successful response.
702
+ '''
703
+ # A bare string would silently be split into one-character field names,
704
+ # a dict would be reduced to its keys, and a non-iterable (None, an int)
705
+ # would raise an opaque "'int' object is not iterable" from list().
706
+ # Fail loudly with a message that names the right shape instead.
707
+ if (not isinstance(fields, Iterable)
708
+ or isinstance(fields, (str, bytes, dict))):
709
+ raise TypeError('fields must be a list of field names or '
710
+ '{name, type, description, required} dicts')
711
+ url = self.url_endpoint + 'extract'
712
+ data = {
713
+ 'key': self.api_key,
714
+ 'text': text,
715
+ 'fields': list(fields),
716
+ }
717
+ if context is not None:
718
+ data['context'] = context
719
+ return self._request(url, data)
720
+
634
721
  def tone(
635
722
  self,
636
723
  text,
@@ -682,43 +769,79 @@ class SaplingClient:
682
769
  def quality(
683
770
  self,
684
771
  text,
772
+ sentence_scores=None,
773
+ rubric=None,
685
774
  ):
686
775
  '''
687
776
  Computes a quality score for the provided text based on how "surprising"
688
777
  the text is to the model. Lower surprisal corresponds to higher scores.
778
+ Optionally also returns a per-sentence breakdown and an LLM-judged rubric.
689
779
 
690
- :param text: Text to compute a quality score for.
780
+ :param text: Text to compute a quality score for. The base score is computed
781
+ on the leading ~4,000 characters; the rubric evaluates the whole text
782
+ (up to 20,000 characters).
691
783
  :type text: str
784
+ :param sentence_scores: If true, also return a 1-5 score for each sentence
785
+ under `sentences`.
786
+ :type sentence_scores: bool
787
+ :param rubric: If true, also return an LLM-judged rubric under `rubric`
788
+ (billed at the Tone rate).
789
+ :type rubric: bool
692
790
  :rtype: dict
693
791
  :return:
694
792
  - score: A score from 1 (low quality) to 5 (high quality).
793
+ - sentences: If sentence_scores is set, list of {start, end, text, score} in
794
+ document order; start/end are character offsets into the text.
795
+ - rubric: If rubric is set, {overall, dimensions: {clarity, coherence,
796
+ correctness, concision}, summary, issues: [{quote, dimension, note,
797
+ suggestion, start, end}]}; scores are integers 1-5.
695
798
  '''
696
799
  url = self.url_endpoint + 'quality'
697
800
  data = {
698
801
  'key': self.api_key,
699
802
  'text': text,
700
803
  }
804
+ if sentence_scores is not None:
805
+ data['sentence_scores'] = sentence_scores
806
+ if rubric is not None:
807
+ data['rubric'] = rubric
701
808
  return self._request(url, data)
702
809
 
703
810
  def langdetect(
704
811
  self,
705
812
  text,
813
+ top_k=None,
814
+ segments=None,
706
815
  ):
707
816
  '''
708
- Identifies the language of the provided text.
817
+ Identifies the language of the provided text, with optional runner-up
818
+ candidates and a per-segment breakdown for mixed-language text.
709
819
 
710
- :param text: Text to identify the language for.
820
+ :param text: Text to identify the language for (up to 20,000 characters).
711
821
  :type text: str
822
+ :param top_k: Number of language candidates to return, 1-10. Defaults to 3 server-side.
823
+ :type top_k: int
824
+ :param segments: If true, also detect the language of each line/sentence
825
+ of the text and return them under `segments`.
826
+ :type segments: bool
712
827
  :rtype: dict
713
828
  :return:
714
- - lang: The detected language code.
829
+ - lang: The detected language code (mostly ISO 639-1).
830
+ - name: English name of the detected language.
715
831
  - conf: Confidence score for the detection result.
832
+ - candidates: List of {lang, name, conf} for the top languages, most likely first.
833
+ - segments: If segments is set, list of {start, end, text, lang, name, conf, candidates}
834
+ in document order; start/end are character offsets into the text.
716
835
  '''
717
836
  url = self.url_endpoint + 'langdetect'
718
837
  data = {
719
838
  'key': self.api_key,
720
839
  'text': text,
721
840
  }
841
+ if top_k is not None:
842
+ data['top_k'] = top_k
843
+ if segments is not None:
844
+ data['segments'] = segments
722
845
  return self._request(url, data)
723
846
 
724
847
  def profanity(
@@ -741,3 +864,239 @@ class SaplingClient:
741
864
  'text': text,
742
865
  }
743
866
  return self._request(url, data)
867
+
868
+ def safety(
869
+ self,
870
+ text,
871
+ threshold=None,
872
+ spans=None,
873
+ ):
874
+ '''
875
+ Scores the provided text on seven content-safety categories: toxicity,
876
+ profanity, harassment, hate_speech, self_harm, sexual, and violence.
877
+
878
+ :param text: Text to score, up to 20,000 characters.
879
+ :type text: str
880
+ :param threshold: Score at or above which a category is flagged.
881
+ Between 0 and 1 inclusive; the API defaults to 0.5.
882
+ :type threshold: float
883
+ :param spans: When True, the response also includes the offending
884
+ passages as spans, each with its text, start/end character offsets
885
+ into the submitted text, per-passage category scores, and
886
+ flagged_categories. The API defaults to False.
887
+ :type spans: bool
888
+ :rtype: dict
889
+ :return:
890
+ - scores: A probability from 0 to 1 for each category.
891
+ - flagged: True if any category scored at or above the threshold.
892
+ - flagged_categories: Categories that scored at or above the threshold.
893
+ - threshold: The threshold that was applied.
894
+ - spans: Only with spans=True — the offending passages, most
895
+ severe first, as {text, start, end, scores, flagged_categories}.
896
+ '''
897
+ url = self.url_endpoint + 'safety'
898
+ data = {
899
+ 'key': self.api_key,
900
+ 'text': text,
901
+ }
902
+ if threshold is not None:
903
+ data['threshold'] = threshold
904
+ if spans is not None:
905
+ data['spans'] = spans
906
+ return self._request(url, data)
907
+
908
+ def pii(
909
+ self,
910
+ text,
911
+ types=None,
912
+ redact=None,
913
+ ):
914
+ '''
915
+ Detects personally identifiable information (PII) in the provided text and
916
+ optionally returns a redacted copy.
917
+
918
+ Detection is deterministic (pattern matching plus checksum validation, no ML)
919
+ and every entity comes with exact character offsets into ``text``, so pass
920
+ raw text (not HTML) and apply the spans to your own copy of the document.
921
+ Person names and street addresses are not detected.
922
+
923
+ :param text: Text to scan for PII.
924
+ :type text: str
925
+ :param types: Optional list of PII types to detect. Defaults to all types:
926
+ ``email``, ``phone``, ``ssn``, ``credit_card``, ``ip_address``, ``iban``,
927
+ ``us_bank_routing``.
928
+ :type types: list[str]
929
+ :param redact: If True, the response also includes ``redacted_text`` with every
930
+ detected entity replaced by a placeholder such as ``[EMAIL]``.
931
+ :type redact: bool
932
+ :rtype: dict
933
+ :return:
934
+ - entities: List of ``{type, text, start, end, replacement}`` dicts, sorted
935
+ by ``start``; spans never overlap.
936
+ - flagged: True if any PII was detected.
937
+ - types: Sorted list of the PII types found.
938
+ - redacted_text: Only present when ``redact`` is True.
939
+ '''
940
+ url = self.url_endpoint + 'pii'
941
+ data = {
942
+ 'key': self.api_key,
943
+ 'text': text,
944
+ }
945
+ if types is not None:
946
+ if (not isinstance(types, Iterable)
947
+ or isinstance(types, (str, bytes, dict))):
948
+ raise TypeError('types must be a list of entity type names')
949
+ data['types'] = list(types)
950
+ if redact is not None:
951
+ data['redact'] = redact
952
+ return self._request(url, data)
953
+
954
+ def seo(
955
+ self,
956
+ text,
957
+ keywords=None,
958
+ suggestions=None,
959
+ lang=None,
960
+ ):
961
+ '''
962
+ Analyzes the provided page or article text for SEO: deterministic content
963
+ statistics and readability, target-keyword usage, the most frequent terms, and
964
+ (optionally) LLM-generated titles, meta descriptions, a URL slug, and focus
965
+ keywords.
966
+
967
+ :param text: Page or article body, plain text or HTML (block tags become line
968
+ breaks, inline tags are removed), up to 20,000 characters.
969
+ :type text: str
970
+ :param keywords: Optional list of up to 10 target keyword phrases, most
971
+ important first (the first is the primary keyword). Each 1-100 characters.
972
+ Measured in the text and steered into the suggestions.
973
+ :type keywords: list[str]
974
+ :param suggestions: If False, only the deterministic ``stats``, ``keywords`` and
975
+ ``top_terms`` are returned (free); no ``suggestions`` are generated. The API
976
+ defaults to True (billed at the Tone rate).
977
+ :type suggestions: bool
978
+ :param lang: ISO 639-1 language code of the text (a region subtag such as
979
+ ``pt-BR`` is allowed, or ``auto``). The API defaults to ``en``. Only selects
980
+ the readability formulas: ``flesch_reading_ease`` and
981
+ ``flesch_kincaid_grade`` are computed for ``en``, ``de``, ``es``, ``fr``,
982
+ ``it``, ``nl``, ``pl`` and ``ru`` and are None for any other language.
983
+ :type lang: str
984
+ :rtype: dict
985
+ :return:
986
+ - stats: ``{chars, words, sentences, paragraphs, reading_time_min,
987
+ flesch_reading_ease, flesch_kincaid_grade}``; the two readability fields
988
+ are None for languages without readability support.
989
+ - keywords: One ``{keyword, count, density, in_first_100_words}`` dict per
990
+ submitted target keyword, in the same order; empty when none were sent.
991
+ - top_terms: Up to 10 most frequent content unigrams/bigrams as
992
+ ``{term, count}`` dicts (lower-cased, English stopwords removed).
993
+ - suggestions: Only present when ``suggestions`` is not False:
994
+ ``{titles, meta_descriptions, slug, keywords}`` where ``titles`` and
995
+ ``meta_descriptions`` are lists of strings, ``slug`` is a kebab-case
996
+ string (or None when no ASCII slug could be derived, e.g. purely CJK
997
+ content) and ``keywords`` is a list of focus-keyword phrases.
998
+ '''
999
+ url = self.url_endpoint + 'seo'
1000
+ data = {
1001
+ 'key': self.api_key,
1002
+ 'text': text,
1003
+ }
1004
+ if keywords is not None:
1005
+ if (not isinstance(keywords, Iterable)
1006
+ or isinstance(keywords, (str, bytes, dict))):
1007
+ raise TypeError('keywords must be a list of keyword strings')
1008
+ data['keywords'] = list(keywords)
1009
+ if suggestions is not None:
1010
+ data['suggestions'] = suggestions
1011
+ if lang is not None:
1012
+ data['lang'] = lang
1013
+ return self._request(url, data)
1014
+
1015
+ def classify(
1016
+ self,
1017
+ text,
1018
+ labels,
1019
+ multi_label=None,
1020
+ threshold=None,
1021
+ context=None,
1022
+ ):
1023
+ '''
1024
+ Classifies the provided text into one (or several) of the labels you supply.
1025
+ Zero-shot: no training data or setup is needed, just the label names and,
1026
+ optionally, a short description per label.
1027
+
1028
+ Example::
1029
+
1030
+ client.classify(
1031
+ 'I was charged twice for my last invoice.',
1032
+ labels=[
1033
+ 'billing',
1034
+ {'name': 'technical issue', 'description': 'Bugs, crashes, errors'},
1035
+ 'shipping',
1036
+ 'other',
1037
+ ],
1038
+ context='Support tickets for a SaaS billing product',
1039
+ )
1040
+ # {'label': 'billing',
1041
+ # 'labels': ['billing'],
1042
+ # 'scores': [{'label': 'billing', 'score': 0.86},
1043
+ # {'label': 'technical issue', 'score': 0.07},
1044
+ # {'label': 'shipping', 'score': 0.04},
1045
+ # {'label': 'other', 'score': 0.03}],
1046
+ # 'rationale': 'The customer reports being charged twice on one invoice.',
1047
+ # 'multi_label': False}
1048
+
1049
+ :param text: Text to classify, plain text or HTML (tags are stripped), up to
1050
+ 10,000 characters.
1051
+ :type text: str
1052
+ :param labels: 2-20 candidate labels. Each entry is either a label name (str) or
1053
+ a dict ``{'name': str, 'description': str}`` where ``description`` is
1054
+ optional. Names are up to 50 characters and must be unique
1055
+ (case-insensitive); descriptions are up to 200 characters and help the
1056
+ model with borderline decisions.
1057
+ :type labels: list[str | dict]
1058
+ :raises TypeError: If ``labels`` is None, a single string or a dict rather
1059
+ than a list/tuple of labels.
1060
+ :param multi_label: If False (the API default), exactly one label applies and
1061
+ the scores form a probability distribution over the labels. If True, zero,
1062
+ one or several labels may apply and each score is an independent 0-1
1063
+ probability.
1064
+ :type multi_label: bool
1065
+ :param threshold: Multi-label cut-off, 0-1: ``labels`` in the response contains
1066
+ every label with ``score >= threshold``. The API defaults to 0.5. Ignored in
1067
+ single-label mode.
1068
+ :type threshold: float
1069
+ :param context: Up to 500 characters describing what the texts are or how to
1070
+ decide (e.g. ``'Support tickets for a SaaS billing product'``). Guidance
1071
+ only; the model can still only answer with the labels you provide.
1072
+ :type context: str
1073
+ :rtype: dict
1074
+ :return:
1075
+ - label: Best-matching label name (always one of the input labels).
1076
+ - labels: Single-label mode: ``[label]``. Multi-label mode: every label
1077
+ with ``score >= threshold``, descending by score; may be empty when none
1078
+ of the labels apply.
1079
+ - scores: One ``{label, score}`` dict per input label, sorted descending
1080
+ by score (0-1, rounded to 4 decimals).
1081
+ - rationale: One sentence naming the evidence in the text (may be empty).
1082
+ - multi_label: The mode that was used.
1083
+ '''
1084
+ # A bare string would silently be split into one-character labels and
1085
+ # None would raise an opaque TypeError from list(); fail loudly instead.
1086
+ if (not isinstance(labels, Iterable)
1087
+ or isinstance(labels, (str, bytes, dict))):
1088
+ raise TypeError('labels must be a list of label names or '
1089
+ '{name, description} dicts')
1090
+ url = self.url_endpoint + 'classify'
1091
+ data = {
1092
+ 'key': self.api_key,
1093
+ 'text': text,
1094
+ 'labels': list(labels),
1095
+ }
1096
+ if multi_label is not None:
1097
+ data['multi_label'] = multi_label
1098
+ if threshold is not None:
1099
+ data['threshold'] = threshold
1100
+ if context is not None:
1101
+ data['context'] = context
1102
+ return self._request(url, data)
@@ -0,0 +1 @@
1
+ __version__ = '4.3.0'
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sapling-py
3
- Version: 4.1.0
3
+ Version: 4.3.0
4
4
  Summary: Sapling Python Client
5
- Home-page: https://sapling.ai
6
- Author: Sapling Intelligence
7
- Author-email: info@sapling.ai
5
+ Author-email: Sapling Intelligence <info@sapling.ai>
8
6
  License: Apache License 2.0
7
+ Project-URL: Homepage, https://sapling.ai
8
+ Project-URL: Documentation, https://sapling.readthedocs.io/
9
+ Project-URL: Repository, https://github.com/saplingai/sapling-py
9
10
  Classifier: Development Status :: 5 - Production/Stable
10
11
  Classifier: License :: OSI Approved :: Apache Software License
11
12
  Classifier: Programming Language :: Python :: 3
@@ -17,18 +18,7 @@ Requires-Dist: requests
17
18
  Provides-Extra: test
18
19
  Requires-Dist: pytest; extra == "test"
19
20
  Requires-Dist: responses; extra == "test"
20
- Dynamic: author
21
- Dynamic: author-email
22
- Dynamic: classifier
23
- Dynamic: description
24
- Dynamic: description-content-type
25
- Dynamic: home-page
26
- Dynamic: license
27
21
  Dynamic: license-file
28
- Dynamic: provides-extra
29
- Dynamic: requires-dist
30
- Dynamic: requires-python
31
- Dynamic: summary
32
22
 
33
23
  # Sapling Python Client
34
24
 
@@ -40,7 +30,9 @@ Compare against leading grammar checking tools and APIs like Grammarly (Grammerl
40
30
  ## Overview
41
31
 
42
32
  Sapling is a toolkit for helping developers build language model-powered applications.
43
- The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection, and more.
33
+ The API provides spelling and grammar checking, autocomplete, tone detection, rephrasing, AI detection,
34
+ zero-shot text classification, structured data extraction, PII detection and redaction, content safety
35
+ scoring, SEO analysis, and more.
44
36
 
45
37
  Key Features:
46
38
  - **60% More Corrections**: Outperforms [competing solutions](https://sapling.ai/comparison/api#sapling) at similar accuracy levels using state-of-the-art generative AI models.
@@ -52,12 +44,19 @@ Key Features:
52
44
 
53
45
  ## Installation
54
46
 
55
- Install the `sapling-py` package with [pip](https://pip.pypa.io/en/stable/installation/)
47
+ Install the `sapling-py` package from PyPI with
48
+ [pip](https://pip.pypa.io/en/stable/installation/):
56
49
 
57
- ```
50
+ ```sh
58
51
  python -m pip install sapling-py
59
52
  ```
60
53
 
54
+ Or add it to a [uv](https://docs.astral.sh/uv/) project:
55
+
56
+ ```sh
57
+ uv add sapling-py
58
+ ```
59
+
61
60
  ## Documentation
62
61
  Documentation for the client is available at [Read the Docs](https://sapling.readthedocs.io/) and
63
62
  documentation for the HTTP API is available at [Sapling.ai](https://sapling.ai/docs).
@@ -1,5 +1,6 @@
1
1
  LICENSE
2
2
  README.md
3
+ pyproject.toml
3
4
  setup.py
4
5
  sapling/__init__.py
5
6
  sapling/client.py
@@ -9,5 +10,9 @@ sapling_py.egg-info/SOURCES.txt
9
10
  sapling_py.egg-info/dependency_links.txt
10
11
  sapling_py.egg-info/requires.txt
11
12
  sapling_py.egg-info/top_level.txt
12
- test/__init__.py
13
- test/test_client.py
13
+ test/test_classify.py
14
+ test/test_client.py
15
+ test/test_extract.py
16
+ test/test_pii.py
17
+ test/test_safety.py
18
+ test/test_seo.py