python-pdffiller 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,75 @@
1
+ import json
2
+ import os
3
+
4
+ from pdffiller.cli.args import add_global_arguments
5
+ from pdffiller.cli.command import pdffiller_command, PdfFillerArgumentParser
6
+ from pdffiller.exceptions import (
7
+ AbortExecution,
8
+ CommandLineError,
9
+ FileNotExistsError,
10
+ PdfFillerException,
11
+ )
12
+ from pdffiller.io.output import cli_out_write, PdfFillerOutput
13
+ from pdffiller.pdf import Pdf
14
+ from pdffiller.typing import Any
15
+
16
+ from ..exit_codes import ERROR_ENCOUNTERED
17
+
18
+
19
+ def dump_fields_text_formatter(pdf: Pdf) -> None:
20
+ """Print output text for dump_fields command as simple text"""
21
+ for widget in pdf.schema:
22
+ cli_out_write("----------")
23
+ for key, value in widget.items():
24
+ if isinstance(value, list):
25
+ for subvalue in value:
26
+ cli_out_write(f"{key}: {subvalue}")
27
+ else:
28
+ cli_out_write(f"{key}: {value}")
29
+
30
+
31
+ def dump_fields_json_formatter(pdf: Pdf) -> None:
32
+ """Print output text for dump_fields command as simple text"""
33
+
34
+ cli_out_write(json.dumps(pdf.schema, indent=4, ensure_ascii=False))
35
+
36
+
37
+ @pdffiller_command(
38
+ group=None, # "Extract",
39
+ formatters={"text": dump_fields_text_formatter, "json": dump_fields_json_formatter},
40
+ )
41
+ def dump_data_fields(parser: PdfFillerArgumentParser, *args: Any) -> Any:
42
+ """
43
+ Dump form fields present in a pdf given its file path
44
+ """
45
+ options_group = parser.add_argument_group("options")
46
+ parser.add_argument(
47
+ "file",
48
+ metavar="INPUT_PATH",
49
+ type=str,
50
+ nargs="?",
51
+ help="""Path to the input PDF file.""",
52
+ )
53
+
54
+ add_global_arguments(options_group, True, parser)
55
+
56
+ opts = parser.parse_args(*args)
57
+
58
+ output = PdfFillerOutput()
59
+ if not opts.file:
60
+ raise CommandLineError("no input file given")
61
+
62
+ if not os.path.isfile(opts.file):
63
+ raise FileNotExistsError(opts.file)
64
+
65
+ try:
66
+ pdf = Pdf(opts.file)
67
+ return pdf
68
+ except PdfFillerException as exp:
69
+ output.error(str(exp))
70
+ except Exception as exg: # pylint: disable=broad-except # pragma: no cover
71
+ output.error(f"unexpected error when adding {opts.file} with the following error:")
72
+ output.error(exg)
73
+ raise AbortExecution(ERROR_ENCOUNTERED) from exg
74
+
75
+ return None
@@ -0,0 +1,142 @@
1
+ import html
2
+ import json
3
+ import os
4
+ import sys
5
+
6
+ import yaml
7
+
8
+ from pdffiller.cli.args import add_global_arguments
9
+ from pdffiller.cli.boolean_action import BooleanAction
10
+ from pdffiller.cli.command import pdffiller_command, PdfFillerArgumentParser
11
+ from pdffiller.cli.once_argument import OnceArgument
12
+ from pdffiller.exceptions import (
13
+ AbortExecution,
14
+ CommandLineError,
15
+ FileNotExistsError,
16
+ PdfFillerException,
17
+ )
18
+ from pdffiller.io.output import PdfFillerOutput
19
+ from pdffiller.pdf import Pdf
20
+ from pdffiller.typing import Any, Dict, Union
21
+
22
+ from ..exit_codes import ERROR_ENCOUNTERED
23
+
24
+
25
+ @pdffiller_command(
26
+ group=None,
27
+ )
28
+ def fill_form(parser: PdfFillerArgumentParser, *args: Any) -> Any:
29
+ """
30
+ Fill an input PDF's form fields with the data from
31
+ """
32
+ options_group = parser.add_argument_group("options")
33
+
34
+ options_group.add_argument(
35
+ "-d",
36
+ "--data",
37
+ metavar="DATA_PATH",
38
+ type=str,
39
+ help="""Path to the data file defining the field/value pairs.
40
+ It can be a json or yaml file format.
41
+ It can be also - to read data file from stdin with JSON format.
42
+ """,
43
+ action=OnceArgument,
44
+ )
45
+
46
+ options_group.add_argument(
47
+ "-i",
48
+ "--input-data",
49
+ metavar="DATA",
50
+ type=str,
51
+ help="""Input data with JSON format defining the field/value pairs.
52
+ """,
53
+ action=OnceArgument,
54
+ )
55
+
56
+ options_group.add_argument(
57
+ "-o",
58
+ "--output",
59
+ metavar="OUTPUT_PATH",
60
+ type=str,
61
+ help="""Path to the output PDF file.""",
62
+ action=OnceArgument,
63
+ )
64
+
65
+ options_group.add_argument(
66
+ "-f",
67
+ "--flatten",
68
+ action=BooleanAction,
69
+ default=False,
70
+ help="Use this option to merge an input PDF's interactive form fields"
71
+ "(and their data) with the PDF's pages. Defaults to False.",
72
+ )
73
+
74
+ parser.add_argument(
75
+ "file",
76
+ metavar="INPUT_PATH",
77
+ type=str,
78
+ nargs="?",
79
+ help="""Path to the input PDF file.""",
80
+ action=OnceArgument,
81
+ )
82
+
83
+ add_global_arguments(options_group, True, parser)
84
+
85
+ opts = parser.parse_args(*args)
86
+
87
+ output = PdfFillerOutput()
88
+ if not opts.file:
89
+ raise CommandLineError("no input file given")
90
+
91
+ if not opts.output:
92
+ raise CommandLineError("no output file path given")
93
+
94
+ if not opts.data and not opts.input_data:
95
+ raise CommandLineError("no data file path given")
96
+
97
+ input_data: Dict[str, Union[str, int, float, bool]] = {}
98
+ if opts.input_data:
99
+ try:
100
+ input_data = json.loads(opts.input_data)
101
+ except Exception as exg: # pylint: disable=broad-except
102
+ output.error("Failed to load json input data")
103
+ raise AbortExecution(ERROR_ENCOUNTERED) from exg
104
+ else:
105
+ if "-" != opts.data:
106
+ if not os.path.isfile(opts.file):
107
+ raise FileNotExistsError(opts.file)
108
+ if not os.path.isfile(opts.data):
109
+ raise FileNotExistsError(opts.data)
110
+
111
+ with open(opts.data, "r", encoding="utf-8") as stream:
112
+ try:
113
+ if os.path.splitext(opts.data)[1] in [".yaml", ".yml"]:
114
+ input_data = yaml.safe_load(stream)
115
+ else:
116
+ input_data = json.load(stream)
117
+ except Exception as exg: # pylint: disable=broad-except
118
+ output.error(f"Failed to load {opts.data} input data file")
119
+ raise AbortExecution(ERROR_ENCOUNTERED) from exg
120
+ elif not os.isatty(sys.stdin.fileno()):
121
+ try:
122
+ input_data = json.load(sys.stdin)
123
+ except Exception as exg: # pylint: disable=broad-except
124
+ output.error(f"Failed to load {opts.data} input data file : " + str(exg))
125
+ raise AbortExecution(ERROR_ENCOUNTERED) from exg
126
+
127
+ if isinstance(input_data, list) and isinstance(input_data[0], dict):
128
+ input_dict = {}
129
+ for field in input_data:
130
+ if "name" in field and "value" in field:
131
+ input_dict[html.unescape(field["name"])] = html.unescape(field["value"])
132
+ input_data = input_dict
133
+
134
+ try:
135
+ pdf = Pdf(opts.file)
136
+ pdf.fill(opts.file, opts.output, input_data, opts.flatten)
137
+ except PdfFillerException as exp:
138
+ output.error(str(exp))
139
+ except Exception as exg: # pylint: disable=broad-except # pragma: no cover
140
+ output.error(f"unexpected error when adding {opts.file} with the following error:")
141
+ output.error(exg)
142
+ raise AbortExecution(ERROR_ENCOUNTERED) from exg
@@ -0,0 +1,10 @@
1
+ # Exit codes for pdffiller command:
2
+ SUCCESS = 0 # 0: Success
3
+ ERROR_GENERAL = 1 # 1: General exception error
4
+ USER_CTRL_C = 2 # 2: Ctrl+C
5
+ USER_CTRL_BREAK = 3 # 3: Ctrl+Break
6
+ ERROR_SIGTERM = 4 # 4: SIGTERM
7
+ ERROR_UNEXPECTED = 5 # 5: Unexpected error
8
+ ERROR_ENCOUNTERED = 6 # 6: Error occurs during command execution
9
+ ERROR_COMMAND_NAME = 7 # 7: Action/command name missing
10
+ ERROR_SUBCOMMAND_NAME = 8 # 8: Sub-command name missing
@@ -0,0 +1,16 @@
1
+ import json
2
+
3
+ from pdffiller.io.output import cli_out_write
4
+ from pdffiller.typing import Any
5
+
6
+
7
+ def default_json_formatter(data: Any) -> None:
8
+ """Default JSON formatter"""
9
+ data_json = json.dumps(data, indent=4)
10
+ cli_out_write(data_json)
11
+
12
+
13
+ def default_text_formatter(data: Any) -> None:
14
+ """Default TEXT formatter"""
15
+ for key, value in data.items():
16
+ cli_out_write(f"{key}: {value}")
@@ -0,0 +1,19 @@
1
+ import argparse
2
+
3
+ from pdffiller.typing import Any, Optional, Sequence, Union
4
+
5
+
6
+ class OnceArgument(argparse.Action):
7
+ """Allows declaring a parameter that can have only one value."""
8
+
9
+ def __call__(
10
+ self,
11
+ parser: argparse.ArgumentParser,
12
+ namespace: argparse.Namespace,
13
+ values: Union[str, Any, Sequence[Any], None],
14
+ option_string: Optional[str] = None,
15
+ ) -> None:
16
+ if getattr(namespace, self.dest) is not None and self.default is None:
17
+ msg = f"{option_string or 'undefined'} can only be specified once"
18
+ raise argparse.ArgumentError(None, msg)
19
+ setattr(namespace, self.dest, values)
@@ -0,0 +1,10 @@
1
+ import argparse
2
+ import textwrap
3
+
4
+
5
+ class SmartFormatter(argparse.HelpFormatter):
6
+ """Text formatter for DbDumpToPG commands"""
7
+
8
+ def _fill_text(self, text: str, width: int, indent: str) -> str:
9
+ text = textwrap.dedent(text)
10
+ return "".join(indent + line for line in text.splitlines(True))
pdffiller/const.py ADDED
@@ -0,0 +1,22 @@
1
+ """ pdffiller constants. """
2
+
3
+ __all__ = [
4
+ "ENV_NO_COLOR",
5
+ "ENV_CLICOLOR_FORCE",
6
+ "ENV_PDFFILLER_COLOR_DARK",
7
+ ]
8
+
9
+ ENV_NO_COLOR = "NO_COLOR"
10
+ """ Disable ANSI colors. """
11
+
12
+ ENV_CLICOLOR_FORCE = "CLICOLOR_FORCE"
13
+ """ANSI colors should be enabled.
14
+
15
+ Different from 0 to enforce ANSI colors
16
+ """
17
+
18
+ ENV_PDFFILLER_COLOR_DARK = "PDFFILLER_COLOR_DARK"
19
+ """Use dark ANSI color scheme.
20
+
21
+ It must be different from 0 to enforce dark colors
22
+ """
@@ -0,0 +1,59 @@
1
+ from pdffiller.typing import Optional, PathLike
2
+
3
+
4
+ #
5
+ # Generic exception
6
+ #
7
+ class PdfFillerException(Exception):
8
+ """PdfFiller based exception object"""
9
+
10
+ def __init__(self, message: str) -> None:
11
+ Exception.__init__(self, message)
12
+ self.message = message
13
+
14
+
15
+ #
16
+ # Command-line utility exception
17
+ #
18
+
19
+
20
+ class AbortExecution(PdfFillerException):
21
+ """Abort but with success the current execution"""
22
+
23
+ def __init__(self, exitcode: int = 0) -> None:
24
+ self.exitcode = exitcode
25
+ PdfFillerException.__init__(self, "")
26
+
27
+
28
+ class CommandLineError(PdfFillerException):
29
+ """One command-line argument is not defined properly"""
30
+
31
+ def __init__(self, message: str) -> None:
32
+ PdfFillerException.__init__(self, message)
33
+
34
+
35
+ class InvalidCommandNameException(PdfFillerException):
36
+ """Invalid command or action name"""
37
+
38
+ def __init__(self, name: Optional[str] = None) -> None:
39
+ if name:
40
+ PdfFillerException.__init__(self, f"Unknown '{name}' command")
41
+ else:
42
+ PdfFillerException.__init__(self, "No command name given")
43
+
44
+
45
+ class InvalidSubCommandNameException(PdfFillerException):
46
+ """Invalid sub-command name"""
47
+
48
+ def __init__(self, name: Optional[str] = None) -> None:
49
+ if name:
50
+ PdfFillerException.__init__(self, f"Unknown '{name}' sub-command")
51
+ else:
52
+ PdfFillerException.__init__(self, "No sub-command name given")
53
+
54
+
55
+ class FileNotExistsError(PdfFillerException):
56
+ """File not found"""
57
+
58
+ def __init__(self, pathname: PathLike) -> None:
59
+ PdfFillerException.__init__(self, f"{pathname} : file not found")
File without changes
pdffiller/io/colors.py ADDED
@@ -0,0 +1,52 @@
1
+ import os
2
+
3
+ import colorama
4
+
5
+ from pdffiller import const
6
+ from pdffiller.typing import Any
7
+
8
+
9
+ def is_terminal(stream: Any) -> bool:
10
+ """Determine whether a stream is interactive or not.
11
+
12
+ :return: True if ``stream`` interactive else False
13
+ """
14
+ return hasattr(stream, "isatty") and stream.isatty()
15
+
16
+
17
+ def color_enabled(stream: object) -> bool:
18
+ """Determine whether a stream can support colorred output.
19
+
20
+ This function follows https://bixense.com/clicolors convention, so you can
21
+ define one of the following variable to enforce a mode, else the function
22
+ will just check if `stream` is interactive or not:
23
+ * :const:`~pdffiller.const.ENV_NO_COLOR`: No colors by just testing its existance
24
+ * :const:`~pdffiller.const.ENV_CLICOLOR_FORCE`: Force color if defined and
25
+ value is not **0**
26
+
27
+ :param stream: The stream to be tested.
28
+
29
+ :return: True if colorred output is enabled, else False
30
+ """
31
+
32
+ if os.getenv(const.ENV_NO_COLOR, "0") != "0":
33
+ # CLICOLOR_FORCE != 0, ANSI colors should be enabled no matter what.
34
+ return True
35
+
36
+ if os.getenv(const.ENV_CLICOLOR_FORCE) is not None:
37
+ # Enable fully colorred mode
38
+ return True
39
+
40
+ return is_terminal(stream)
41
+
42
+
43
+ def init_colorama(stream: object) -> None:
44
+ """Initialize colorama.
45
+ :param stream: The stream to be used to determine if colorred mode is supported not
46
+ """
47
+ if color_enabled(stream):
48
+ if os.getenv(const.ENV_CLICOLOR_FORCE, "0") != "0":
49
+ # Otherwise it is not really forced if colorama doesn't feel it
50
+ colorama.init(strip=False, convert=False)
51
+ else:
52
+ colorama.init()