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.
- pdffiller/__init__.py +4 -0
- pdffiller/__main__.py +6 -0
- pdffiller/_version.py +6 -0
- pdffiller/cli/__init__.py +0 -0
- pdffiller/cli/args.py +28 -0
- pdffiller/cli/boolean_action.py +35 -0
- pdffiller/cli/cli.py +260 -0
- pdffiller/cli/command.py +291 -0
- pdffiller/cli/commands/__init__.py +0 -0
- pdffiller/cli/commands/dump_data_fields.py +75 -0
- pdffiller/cli/commands/fill_form.py +142 -0
- pdffiller/cli/exit_codes.py +10 -0
- pdffiller/cli/formatters.py +16 -0
- pdffiller/cli/once_argument.py +19 -0
- pdffiller/cli/smart_formatter.py +10 -0
- pdffiller/const.py +22 -0
- pdffiller/exceptions.py +59 -0
- pdffiller/io/__init__.py +0 -0
- pdffiller/io/colors.py +52 -0
- pdffiller/io/output.py +335 -0
- pdffiller/pdf.py +488 -0
- pdffiller/py.typed.py +0 -0
- pdffiller/typing.py +59 -0
- pdffiller/utils.py +36 -0
- pdffiller/widgets/__init__.py +0 -0
- pdffiller/widgets/base.py +107 -0
- pdffiller/widgets/checkbox.py +52 -0
- pdffiller/widgets/radio.py +37 -0
- pdffiller/widgets/text.py +82 -0
- python_pdffiller-1.0.0.dist-info/METADATA +138 -0
- python_pdffiller-1.0.0.dist-info/RECORD +36 -0
- python_pdffiller-1.0.0.dist-info/WHEEL +5 -0
- python_pdffiller-1.0.0.dist-info/entry_points.txt +2 -0
- python_pdffiller-1.0.0.dist-info/licenses/AUTHORS.rst +7 -0
- python_pdffiller-1.0.0.dist-info/licenses/COPYING +22 -0
- python_pdffiller-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
"""
|
pdffiller/exceptions.py
ADDED
|
@@ -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")
|
pdffiller/io/__init__.py
ADDED
|
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()
|