\r\n `;\r\n })\r\n .join('');\r\n}\r\n\r\nfunction transform() {\r\n files.forEach(file => {\r\n let data = file.data;\r\n // Initialize HTML strings for mens_products and womens_products\r\n let mensProductsHtml = '
'
+ ),
+ custom_table_style = None,
+ unordered_list_style = None,
+ ordered_list_style = None,
+ use_tag_style = None,
+ ignore_cell_margin = None,
+ ignore_empty_p = None
+))
+
+# tag
+collection.add(cop.elements.Html(
+ name="html_img",
+ value=(
+ ''
+ ),
+ custom_table_style = None,
+ unordered_list_style = None,
+ ordered_list_style = None,
+ use_tag_style = None,
+ ignore_cell_margin = None,
+ ignore_empty_p = None
+))
+
+# server
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key="YOUR_API_KEY")
+)
+template = cop.Resource.from_local_file("./data/html_temp.docx")
+
+# print job
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=template
+)
+
+# save output
+response = printjob.execute()
+response.to_file("./output/output")
diff --git a/BeginerGuide/UsingMarkdown/data/template.docx b/BeginerGuide/UsingMarkdown/data/template.docx
new file mode 100644
index 0000000..8ce0e7d
Binary files /dev/null and b/BeginerGuide/UsingMarkdown/data/template.docx differ
diff --git a/BeginerGuide/UsingMarkdown/output/output.docx b/BeginerGuide/UsingMarkdown/output/output.docx
new file mode 100644
index 0000000..09488e0
Binary files /dev/null and b/BeginerGuide/UsingMarkdown/output/output.docx differ
diff --git a/BeginerGuide/UsingMarkdown/usingMarkdown.py b/BeginerGuide/UsingMarkdown/usingMarkdown.py
new file mode 100644
index 0000000..4244090
--- /dev/null
+++ b/BeginerGuide/UsingMarkdown/usingMarkdown.py
@@ -0,0 +1,90 @@
+import sys
+sys.path.insert(0, "C:/Users/em8ee/OneDrive/Documents/cloudofficeprint-python")
+import cloudofficeprint as cop
+
+
+# Create main data collection
+collection = cop.elements.ElementCollection()
+
+#markdown content
+markdown_text = """
+# Heading level 1
+
+## Heading level 2
+
+===============
+
+I just love **bold text**.
+
+Italicized text is the *cat's meow*.
+
+1. First item
+2. Second item
+3. Third item
+4. Fourth item
+
+---
+
+* First item
+* Second item
+* Third item
+* Fourth item
+
+| Syntax | Description |
+| --------- | ----------- |
+| Header | Title |
+| Paragraph | Text |
+
+The world is flat. We now know that the world is round.
+"""
+# Adding markdown content
+collection.add(cop.elements.MarkdownContent("markdowncontent", markdown_text))
+
+# Create and add customer names
+cust_names = cop.elements.ElementCollection("cust_names")
+customers = [
+ {"first": "Albert",
+ "cust_name_bold": "**Albert**"},
+ {"first": "Edward",
+ "cust_name_bold": "**Edward**"},
+ {"first": "Eugene",
+ "cust_name_bold": "**Eugene**"},
+ {"first": "Fiorello",
+ "cust_name_bold": "**Fiorello**"},
+ {"first": "Frank",
+ "cust_name_bold": "**Frank**"},
+ {"first": "John",
+ "cust_name_bold": "**John**"},
+ {"first": "William",
+ "cust_name_bold": "**William**"}
+]
+
+customer_collections = []
+for customer in customers:
+ cust_collection = cop.elements.ElementCollection()
+ cust_collection.add(cop.elements.Property("first", customer["first"]))
+ cust_collection.add(cop.elements.Property("cust_name_bold", customer["cust_name_bold"]))
+ customer_collections.append(cust_collection)
+
+# create array structure
+customers_loop = cop.elements.ForEach("cust_names", customer_collections)
+collection.add(customers_loop)
+
+
+# Server configuration
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key="YOUR_API_KEY")
+)
+
+# Load template and create print job
+template = cop.Resource.from_local_file("./data/template.docx")
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=template
+)
+
+# Execute and save
+response = printjob.execute()
+response.to_file("./output/output")
\ No newline at end of file
diff --git a/BeginerGuide/UsingOtherPptxTag/UsingOtherPPTX.py b/BeginerGuide/UsingOtherPptxTag/UsingOtherPPTX.py
new file mode 100644
index 0000000..d28796f
--- /dev/null
+++ b/BeginerGuide/UsingOtherPptxTag/UsingOtherPPTX.py
@@ -0,0 +1,58 @@
+# Install cloudofficeprint using pip install cloudofficeprint
+#Import the cloudofficeprint libary.
+import sys
+sys.path.insert(0, "PATH_TO_COP_DIR")
+import cloudofficeprint as cop
+
+# Main object that holds the data
+collection = cop.elements.ElementCollection()
+# Create the title element and add it to the element collection
+
+# ---------------------using autoLink-----------
+autoLink = cop.elements.AutoLink(
+ name='autoLink',
+ value='sample text with hyperlinks like https://www.cloudofficeprint.com/docs/python/index.html . COP link is https://www.cloudofficeprint.com/index.html contact us in info@cloudofficeprint.com ',
+ )
+collection.add(autoLink)
+
+# ----------------HyperLink---------------
+hyperlink = cop.elements.Hyperlink(
+ name='hyperlink',
+ url='https://www.cloudofficeprint.com/index.html',
+ text='COP_link'
+ )
+collection.add(hyperlink)
+insert1 = cop.elements.Insert(
+ name="fileToInsert",
+ value="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"
+)
+collection.add(insert1)
+image = cop.elements.Image.from_url(
+ name="image1",
+ url_source="https://picsum.photos/300/200",
+ width="80px",
+ height="60px",
+ alt_text="Random image",
+ wrap_text="square",
+ rotation=0,
+ transparency="10%",
+ url="https://example.com"
+)
+collection.add(image)
+
+# configure server
+# For running on localhost you do not need api_key else replace below "YOUR_API_KEY" with your api key.
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key = "YOUR_API_KEY")
+)
+# Create print job
+# PrintJob combines template, data, server and an optional output configuration
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=cop.Resource.from_local_file("./data/other_temp.pptx")
+)
+# Execute print job and save response to file
+response = printjob.execute()
+response.to_file("./output/other_output.pptx")
\ No newline at end of file
diff --git a/BeginerGuide/UsingOtherPptxTag/data/other_temp.pptx b/BeginerGuide/UsingOtherPptxTag/data/other_temp.pptx
new file mode 100644
index 0000000..0fb31b0
Binary files /dev/null and b/BeginerGuide/UsingOtherPptxTag/data/other_temp.pptx differ
diff --git a/BeginerGuide/UsingOtherPptxTag/output/other_output.pptx b/BeginerGuide/UsingOtherPptxTag/output/other_output.pptx
new file mode 100644
index 0000000..7d8f62a
Binary files /dev/null and b/BeginerGuide/UsingOtherPptxTag/output/other_output.pptx differ
diff --git a/BeginerGuide/UsingOtherXlsxTags/data/codes_temp.xlsx b/BeginerGuide/UsingOtherXlsxTags/data/codes_temp.xlsx
new file mode 100644
index 0000000..0cf677a
Binary files /dev/null and b/BeginerGuide/UsingOtherXlsxTags/data/codes_temp.xlsx differ
diff --git a/BeginerGuide/UsingOtherXlsxTags/output/output.xlsx b/BeginerGuide/UsingOtherXlsxTags/output/output.xlsx
new file mode 100644
index 0000000..a3b3962
Binary files /dev/null and b/BeginerGuide/UsingOtherXlsxTags/output/output.xlsx differ
diff --git a/BeginerGuide/UsingOtherXlsxTags/usingCodes.py b/BeginerGuide/UsingOtherXlsxTags/usingCodes.py
new file mode 100644
index 0000000..6ef942b
--- /dev/null
+++ b/BeginerGuide/UsingOtherXlsxTags/usingCodes.py
@@ -0,0 +1,130 @@
+# Install cloudofficeprint using pip install cloudofficeprint
+import cloudofficeprint as cop #Import the cloudofficeprint libary.
+# Main object that holds the data
+collection = cop.elements.ElementCollection()
+
+# -----------------barcode----------
+barcode = cop.elements.BarCode(
+ name='barcode_name',
+ data='cloudofficeprint',
+ type='code128',
+ extra_options='includetext guardwhitespace'
+ )
+collection.add(barcode)
+
+# --------------------------qrcode-----------
+qrcode = cop.elements.QRCode(
+ name='qrcode_name',
+ data='https://www.cloudofficeprint.com/index.html',
+ type='qrcode'
+ )
+# you can add multiple options for qrcode using variable_name.options
+# for ex:
+# if variable name is qrcode
+# qrcode.logo('background Image')
+
+collection.add(qrcode)
+
+# ---------------------wifi_qr_code------------
+wifi = cop.elements.WiFiQRCode(
+ name='wifi_code_name',
+ ssid='test_wifi_network',
+ wifi_encryption='WPA',
+ wifi_password='my_wifi_password',
+ wifi_hidden=False
+ )
+collection.add(wifi)
+
+# ----------------------telephone_qr_code---------
+telephone_number = cop.elements.TelephoneNumberQRCode(
+ name='telephone_number_name',
+ number='9823038377'
+ )
+collection.add(telephone_number)
+
+# --------------------email_qr_code-----------
+email = cop.elements.EmailQRCode(
+ name='email_name',
+ receiver='info@cloudofficeprint.com',
+ cc='cc',
+ bcc='bcc',
+ subject='test subject',
+ body='Hi there,/n I would like to know about cloudofficeprint./nThank you/n'
+ )
+collection.add(email)
+
+#---------------sms_qr_code----------------
+sms = cop.elements.SMSQRCode(
+ name='sms_qr_code',
+ receiver='9823038377',
+ sms_body='this is test message body'
+ )
+collection.add(sms)
+
+# --------------url_qr_code --------------
+url = cop.elements.URLQRCode(
+ name='urlQr_code',
+ url='https://www.cloudofficeprint.com/index.html'
+ )
+collection.add(url)
+
+# ---------------v_card_qrcode------------
+v_card = cop.elements.VCardQRCode(
+ name='vcard_name',
+ first_name='first_name',
+ last_name='last_name',
+ email='email',
+ website='website'
+ )
+collection.add(v_card)
+
+# ---------------me_card-------------
+me_card = cop.elements.MeCardQRCode(
+ name='me_card_name',
+ first_name='first_name',
+ last_name='last_name',
+ nickname='nickname',
+ email='email',
+ contact_primary='contact_primary',
+ contact_secondary='contact_secondary',
+ contact_tertiary='contact_tertiary',
+ website='website',
+ birthday='birthday',
+ notes='notes'
+ )
+collection.add(me_card)
+
+# -----------------geo_location----------
+geolocation = cop.elements.GeolocationQRCode(
+ name='geolocatin_qr_code_name',
+ latitude='27.608683',
+ longitude='85.360287',
+ altitude='1400'
+ )
+collection.add(geolocation)
+
+# ---------------event----------------
+event = cop.elements.EventQRCode(
+ name='event_qr_code_name',
+ summary='summary',
+ startdate='startdate',
+ enddate='enddate'
+ )
+collection.add(event)
+
+# configure server
+# For running on localhost you do not need api_key else replace below "YOUR_API_KEY" with your api key.
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key = "YOUR_API_KEY")
+)
+# Create print job
+# PrintJob combines template, data, server and an optional output configuration
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=cop.Resource.from_local_file("C:/Users/em8ee/OneDrive/Documents/cloudofficeprint-python/BeginerGuide/UsingOtherXlsxTags/data/codes_temp.xlsx"),
+)
+# Execute print job and save response to file
+response = printjob.execute()
+response.to_file("C:/Users/em8ee/OneDrive/Documents/cloudofficeprint-python/BeginerGuide/UsingOtherXlsxTags/output/output.xlsx")
\ No newline at end of file
diff --git a/BeginerGuide/UsingPageBreak/data/pagebreak_temp.docx b/BeginerGuide/UsingPageBreak/data/pagebreak_temp.docx
new file mode 100644
index 0000000..f665825
Binary files /dev/null and b/BeginerGuide/UsingPageBreak/data/pagebreak_temp.docx differ
diff --git a/BeginerGuide/UsingPageBreak/output/output.docx b/BeginerGuide/UsingPageBreak/output/output.docx
new file mode 100644
index 0000000..71f0073
Binary files /dev/null and b/BeginerGuide/UsingPageBreak/output/output.docx differ
diff --git a/BeginerGuide/UsingPageBreak/usingPageBreak.py b/BeginerGuide/UsingPageBreak/usingPageBreak.py
new file mode 100644
index 0000000..d8d4512
--- /dev/null
+++ b/BeginerGuide/UsingPageBreak/usingPageBreak.py
@@ -0,0 +1,45 @@
+import sys
+sys.path.insert(0, "Path_To_Dir")
+import cloudofficeprint as cop
+
+
+# Create main data collection
+collection = cop.elements.ElementCollection()
+
+# Add customer data
+customer1 = cop.elements.ElementCollection()
+customer1.add(cop.elements.Property("cust_first_name", "John"))
+customer1.add(cop.elements.Property("cust_last_name", "Dulles"))
+customer1.add(cop.elements.PageBreak("pageBreak", True))
+
+customer2 = cop.elements.ElementCollection()
+customer2.add(cop.elements.Property("cust_first_name", "William"))
+customer2.add(cop.elements.Property("cust_last_name", "Hartsfield"))
+customer2.add(cop.elements.PageBreak("pageBreak", True))
+
+customer3 = cop.elements.ElementCollection()
+customer3.add(cop.elements.Property("cust_first_name", "Edward"))
+customer3.add(cop.elements.Property("cust_last_name", "Logan"))
+customer3.add(cop.elements.PageBreak("pageBreak", False))
+
+# Create customers loop
+customers_loop = cop.elements.ForEach("customers", [customer1, customer2, customer3])
+collection.add(customers_loop)
+
+# Server configuration
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key="YOUR_API_KEY")
+)
+
+# Load template and create print job
+template = cop.Resource.from_local_file("./data/pagebreak_temp.docx")
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=template
+)
+
+# Execute and save
+response = printjob.execute()
+response.to_file("./output/output.docx")
\ No newline at end of file
diff --git a/BeginerGuide/UsingPdfInclude/UsingPdfInclude.py b/BeginerGuide/UsingPdfInclude/UsingPdfInclude.py
new file mode 100644
index 0000000..8290796
--- /dev/null
+++ b/BeginerGuide/UsingPdfInclude/UsingPdfInclude.py
@@ -0,0 +1,46 @@
+#Import the cloudofficeprint libary.
+import base64
+import sys
+sys.path.insert(0, "./cloudofficeprint-python")
+import cloudofficeprint as cop
+
+# Read the image file and encode it to base64
+# Make sure to change the path to the image file as per your system
+with open("./data/view.png", "rb") as img:
+ # Encode the image to base64
+ b64_img = base64.b64encode(img.read()).decode("utf-8")
+
+
+# Create element collection
+collection = cop.elements.ElementCollection()
+
+# include element
+include1 = cop.elements.PdfInclude(
+ name="view",
+ value="",
+ filename="view.pdf",
+ mime_type="image/png",
+ file_content=b64_img,
+ file_source="base64"
+ )
+collection.add(include1)
+
+# Configure the Server
+server = cop.config.Server(
+ url="http://localhost:8010/",
+ config=cop.config.ServerConfig(api_key="YOUR_API_KEY")
+)
+
+# Load the DOCX Template
+template = cop.Resource.from_local_file("./data/include_temp.docx")
+
+# Create and Run the PrintJob
+printjob = cop.PrintJob(
+ data=collection,
+ template=template,
+ server=server,
+ output_config=cop.config.OutputConfig(filetype="pdf")
+)
+response = printjob.execute()
+response.to_file("./output/output")
+
diff --git a/BeginerGuide/UsingPdfInclude/data/include_temp.docx b/BeginerGuide/UsingPdfInclude/data/include_temp.docx
new file mode 100644
index 0000000..6c9d2fe
Binary files /dev/null and b/BeginerGuide/UsingPdfInclude/data/include_temp.docx differ
diff --git a/BeginerGuide/UsingPdfInclude/data/view.png b/BeginerGuide/UsingPdfInclude/data/view.png
new file mode 100644
index 0000000..877f196
Binary files /dev/null and b/BeginerGuide/UsingPdfInclude/data/view.png differ
diff --git a/BeginerGuide/UsingPdfInclude/output/output.pdf b/BeginerGuide/UsingPdfInclude/output/output.pdf
new file mode 100644
index 0000000..e932418
Binary files /dev/null and b/BeginerGuide/UsingPdfInclude/output/output.pdf differ
diff --git a/BeginerGuide/UsingShapeRemove/UsingShapeRemove.py b/BeginerGuide/UsingShapeRemove/UsingShapeRemove.py
new file mode 100644
index 0000000..2e4bd95
--- /dev/null
+++ b/BeginerGuide/UsingShapeRemove/UsingShapeRemove.py
@@ -0,0 +1,33 @@
+import sys
+sys.path.insert(0,"PATH_TO_COP_DIR")
+import cloudofficeprint as cop
+
+# Create main data collection
+collection = cop.elements.ElementCollection()
+greeting = cop.elements.PptxShapeRemove("greeting", "Hello World, Thank you for using AOP")
+collection.add(greeting)
+
+# The remove property will be false, so any shape with {remove?} tag will be removed in the tenmlate
+remove = cop.elements.PptxShapeRemove("remove", False)
+collection.add(remove)
+
+# Add a quote that will be shown
+quote = cop.elements.PptxShapeRemove("toShow", "When in doubt, look intelligent. - GARRISON KEILLOR")
+collection.add(quote)
+
+# Configure server
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key="YOUR_API_KEY")
+)
+
+# Create print job
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=cop.Resource.from_local_file("./data/shapeRemove_temp.pptx")
+)
+
+# Execute print job and save response to file
+response = printjob.execute()
+response.to_file("./output/output.pptx")
\ No newline at end of file
diff --git a/BeginerGuide/UsingShapeRemove/data/shapeRemove_temp.pptx b/BeginerGuide/UsingShapeRemove/data/shapeRemove_temp.pptx
new file mode 100644
index 0000000..79663d6
Binary files /dev/null and b/BeginerGuide/UsingShapeRemove/data/shapeRemove_temp.pptx differ
diff --git a/BeginerGuide/UsingShapeRemove/output/output.pptx b/BeginerGuide/UsingShapeRemove/output/output.pptx
new file mode 100644
index 0000000..3985a72
Binary files /dev/null and b/BeginerGuide/UsingShapeRemove/output/output.pptx differ
diff --git a/BeginerGuide/UsingSheetProtection/UsingSheetProtection.py b/BeginerGuide/UsingSheetProtection/UsingSheetProtection.py
new file mode 100644
index 0000000..1ede8a3
--- /dev/null
+++ b/BeginerGuide/UsingSheetProtection/UsingSheetProtection.py
@@ -0,0 +1,37 @@
+# Install cloudofficeprint using pip install cloudofficeprint
+import cloudofficeprint as cop
+
+# Main object that holds the data
+collection = cop.elements.ElementCollection()
+
+# Use SheetProtection to protect the sheet with a password
+sheet_protection = cop.elements.SheetProtection(
+ name="protectTag",
+ password="123",
+ formatCells=False,
+ insertRows=False,
+ deleteRows=False
+)
+collection.add(sheet_protection)
+
+fname = cop.elements.Property(name="cust_first_name", value="john")
+collection.add(fname)
+
+lname = cop.elements.Property(name="cust_last_name", value="doe")
+collection.add(lname)
+
+# configure server
+# For running on localhost you do not need api_key else replace below "YOUR_API_KEY" with your api key.
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key = "YOUR_API_KEY")
+)
+# Create print job
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=cop.Resource.from_local_file("./data/temp.xlsx"),
+)
+# Execute print job and save response to file
+response = printjob.execute()
+response.to_file("./output/output.xlsx")
\ No newline at end of file
diff --git a/BeginerGuide/UsingSheetProtection/data/temp.xlsx b/BeginerGuide/UsingSheetProtection/data/temp.xlsx
new file mode 100644
index 0000000..d05bdaf
Binary files /dev/null and b/BeginerGuide/UsingSheetProtection/data/temp.xlsx differ
diff --git a/BeginerGuide/UsingSheetProtection/output/output.xlsx b/BeginerGuide/UsingSheetProtection/output/output.xlsx
new file mode 100644
index 0000000..e20299c
Binary files /dev/null and b/BeginerGuide/UsingSheetProtection/output/output.xlsx differ
diff --git a/BeginerGuide/UsingSpanTag/UsingSpan.py b/BeginerGuide/UsingSpanTag/UsingSpan.py
new file mode 100644
index 0000000..e15c7cd
--- /dev/null
+++ b/BeginerGuide/UsingSpanTag/UsingSpan.py
@@ -0,0 +1,47 @@
+import sys
+sys.path.insert(0, "Path_To_Dir")
+import cloudofficeprint as cop
+
+# Create main data collection
+collection = cop.elements.ElementCollection()
+
+collection.add(cop.elements.Property("cust_first_name", "John"))
+collection.add(cop.elements.Property("cust_last_name", "Doe"))
+
+# Create a span element for the first cell that will span 2 rows and 3 columns
+span1 = cop.elements.Span(
+ name="span",
+ value="This cell will span 2 rows and 3 columns",
+ columns=3,
+ rows=2
+)
+
+# Create a span element for the second cell that will span 3 rows and 4 columns
+span2 = cop.elements.Span(
+ name="testSpan",
+ value="This cell will span 3 rows and 4 columns",
+ columns=4,
+ rows=3
+)
+
+# Add spans to collection
+collection.add(span1)
+collection.add(span2)
+
+# Server configuration
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key="YOUR_API_KEY")
+)
+
+# Load template and create print job
+template = cop.Resource.from_local_file("./data/span_temp.xlsx")
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=template
+)
+
+# Execute and save
+response = printjob.execute()
+response.to_file("./output/output")
\ No newline at end of file
diff --git a/BeginerGuide/UsingSpanTag/data/span_temp.xlsx b/BeginerGuide/UsingSpanTag/data/span_temp.xlsx
new file mode 100644
index 0000000..5312ba7
Binary files /dev/null and b/BeginerGuide/UsingSpanTag/data/span_temp.xlsx differ
diff --git a/BeginerGuide/UsingSpanTag/output/output.xlsx b/BeginerGuide/UsingSpanTag/output/output.xlsx
new file mode 100644
index 0000000..dff2a8e
Binary files /dev/null and b/BeginerGuide/UsingSpanTag/output/output.xlsx differ
diff --git a/BeginerGuide/UsingTOC/data/toc_temp.docx b/BeginerGuide/UsingTOC/data/toc_temp.docx
new file mode 100644
index 0000000..382b009
Binary files /dev/null and b/BeginerGuide/UsingTOC/data/toc_temp.docx differ
diff --git a/BeginerGuide/UsingTOC/output/output.docx b/BeginerGuide/UsingTOC/output/output.docx
new file mode 100644
index 0000000..93347bc
Binary files /dev/null and b/BeginerGuide/UsingTOC/output/output.docx differ
diff --git a/BeginerGuide/UsingTOC/usingTOC.py b/BeginerGuide/UsingTOC/usingTOC.py
new file mode 100644
index 0000000..2b98ef4
--- /dev/null
+++ b/BeginerGuide/UsingTOC/usingTOC.py
@@ -0,0 +1,100 @@
+import sys
+sys.path.insert(0, "C:/Users/em8ee/OneDrive/Documents/cloudofficeprint-python")
+import cloudofficeprint as cop
+
+# Create main data collection
+collection = cop.elements.ElementCollection()
+
+# Add table of contents
+toc = cop.elements.TableOfContents(
+ name="toc_demo",
+ title="Table of Contents - Generated by AOP",
+ depth=2,
+ tab_leader="hyphen"
+)
+collection.add(toc)
+
+# Add customer data
+customers = [
+ {
+ "cust_f_name": "John",
+ "cust_l_name": "Dulles",
+ "city": "Sterling",
+ "addr_1": "45020 Aviation Drive",
+ "state": "VA",
+ "email": "john.dulles@email.com",
+ "phone_number_1": "703-555-2143",
+ "phone_number_2": "703-555-8967",
+ "url": "http://www.johndulles.com"
+ },
+
+ {
+ "cust_f_name": "Fiorello",
+ "cust_l_name": "LaGuardia",
+ "city": "Flushing",
+ "addr_1": "Hangar Center",
+ "addr_2": "Third Floor",
+ "state": "NY",
+ "phone_number_1": "212-555-3923"
+ },
+ {
+ "cust_f_name": "Albert",
+ "cust_l_name": "Lambert",
+ "city": "St. Louis",
+ "addr_1": "10701 Lambert International Blvd.",
+ "state": "MO",
+ "phone_number_1": "314-555-4022"
+
+ }
+
+
+]
+
+customer_collections = []
+for customer in customers:
+ cust = cop.elements.ElementCollection()
+ for key, value in customer.items():
+ cust.add(cop.elements.Property(key, value))
+ customer_collections.append(cust)
+
+# customers to main collection
+customers_loop = cop.elements.ForEach("cust_list", customer_collections)
+collection.add(customers_loop)
+
+# Add the HTML content
+html_content = """
+
Heading 1 from html
+
Heading 2 from html
+
Heading 3 from html
+AOP also handles the headings from html
+"""
+collection.add(cop.elements.Html(
+ name="sample_html",
+ value=html_content,
+ custom_table_style=None,
+ unordered_list_style=None,
+ ordered_list_style=None,
+ use_tag_style=True,
+ ignore_cell_margin=None,
+ ignore_empty_p=None
+))
+
+# Server configuration
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key="YOUR_API_KEY")
+)
+
+# Load template
+template = cop.Resource.from_local_file("./data/toc_temp.docx")
+
+# Create and execute print job
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=template
+)
+
+# Save output
+response = printjob.execute()
+response.to_file("./output/output")
\ No newline at end of file
diff --git a/BeginerGuide/imageTag/data/img_temp.docx b/BeginerGuide/imageTag/data/img_temp.docx
new file mode 100644
index 0000000..a4bdaed
Binary files /dev/null and b/BeginerGuide/imageTag/data/img_temp.docx differ
diff --git a/BeginerGuide/imageTag/local_img/UC_Logo.svg b/BeginerGuide/imageTag/local_img/UC_Logo.svg
new file mode 100644
index 0000000..b0a8931
--- /dev/null
+++ b/BeginerGuide/imageTag/local_img/UC_Logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/BeginerGuide/imageTag/output/ouput.docx b/BeginerGuide/imageTag/output/ouput.docx
new file mode 100644
index 0000000..7188cb8
Binary files /dev/null and b/BeginerGuide/imageTag/output/ouput.docx differ
diff --git a/BeginerGuide/imageTag/usingImage.py b/BeginerGuide/imageTag/usingImage.py
new file mode 100644
index 0000000..0cba9c4
--- /dev/null
+++ b/BeginerGuide/imageTag/usingImage.py
@@ -0,0 +1,65 @@
+import sys
+sys.path.insert(0, "PATH_TO_COP_DIR")
+import cloudofficeprint as cop
+
+# Create data collection
+collection = cop.elements.ElementCollection()
+
+# Add sample properties
+collection.add(cop.elements.Property(name="title", value="Image Example"))
+collection.add(cop.elements.Property(name="description", value="This images are dynamically loaded using Cloud Office Print"))
+
+# 1) URL based image
+image = cop.elements.Image.from_url(
+ name="image_name",
+ url_source="https://picsum.photos/300/200",
+ width="80px",
+ height="60px",
+ alt_text="Random image",
+ wrap_text="square",
+ rotation=0,
+ transparency="10%",
+ url="https://example.com"
+)
+collection.add(image)
+
+# 2) SVG with density
+image_svg = cop.elements.Image.from_url(
+ name="img_svg",
+ url_source="https://upload.wikimedia.org/wikipedia/commons/4/4f/SVG_Logo.svg",
+ width="200px",
+ density=300, # 300 dpi
+ alt_text="SVG logo",
+)
+collection.add(image_svg)
+
+# 3) Local file image
+collection.add(
+ cop.elements.Image.from_file(
+ name="img_file",
+ path="./local_img/UC_Logo.svg",
+ width="150px",
+ wrap_text="square",
+ alt_text=" Uc logo"
+ )
+)
+
+#server
+server = cop.config.Server(
+ "http://localhost:8010/",
+ cop.config.ServerConfig(api_key="YOUR_API_KEY")
+)
+
+# Load template
+template = cop.Resource.from_local_file("./data/img_temp.docx")
+
+# print job
+printjob = cop.PrintJob(
+ data=collection,
+ server=server,
+ template=template
+)
+
+#save output
+response = printjob.execute()
+response.to_file("./output/ouput")
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..f304438
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+set -e
+
+
+# Clean old build folders
+echo "[1] Cleaning previous build artifacts (build/, dist/, *.egg-info)..."
+rm -rf build/ dist/ *.egg-info
+
+# Remove any previously generated docs for this package
+echo "[2] Removing old auto-generated docs (docs/cloudofficeprint/)..."
+rm -rf docs/cloudofficeprint
+
+# Generate documentation via pdoc
+echo "[3] Generating documentation with pdoc into docs/cloudofficeprint/..."
+# Make sure pdoc is installed: pip install pdoc3
+pdoc --html --force --output-dir docs/ cloudofficeprint
+
+# Build source (+sdist) and wheel (+bdist_wheel)
+echo "[4] Building source and wheel distributions (python setup.py sdist bdist_wheel)..."
+python setup.py sdist bdist_wheel
+
+# Verify the newly built distributions with twine
+echo "[5] Verifying distributions with twine check dist/*..."
+twine check dist/*
+
+echo
+echo " wow ,completed all steps."
diff --git a/cloudofficeprint/__init__.py b/cloudofficeprint/__init__.py
index da838d5..0592011 100644
--- a/cloudofficeprint/__init__.py
+++ b/cloudofficeprint/__init__.py
@@ -95,7 +95,9 @@
from .printjob import PrintJob
from .resource import Resource
+from .template import Template
from .response import Response
+from .transformation import TransformationFunction
# specify what is imported on "from cloudofficeprint import *"
# but that shouldn't really be used anyway
@@ -106,5 +108,7 @@
"own_utils",
"PrintJob",
"Resource",
- "Response"
+ "Template",
+ "Response",
+ "transformation",
]
diff --git a/cloudofficeprint/config/__init__.py b/cloudofficeprint/config/__init__.py
index 1b43102..f96a40f 100644
--- a/cloudofficeprint/config/__init__.py
+++ b/cloudofficeprint/config/__init__.py
@@ -10,3 +10,4 @@
from .output import *
from .pdf import *
from .server import *
+from .request_option import *
\ No newline at end of file
diff --git a/cloudofficeprint/config/output.py b/cloudofficeprint/config/output.py
index e4ddd0a..4c109a6 100644
--- a/cloudofficeprint/config/output.py
+++ b/cloudofficeprint/config/output.py
@@ -2,6 +2,7 @@
from typing import Dict
from .cloud import CloudAccessToken
from .pdf import PDFOptions
+from .request_option import requestOptions
class OutputConfig:
@@ -17,16 +18,34 @@ def __init__(self,
cloud_access_token: CloudAccessToken = None,
server_directory: str = None,
pdf_options: PDFOptions = None,
- append_per_page: bool = None,):
- """
+ append_per_page: bool = None,
+ prepend_per_page: bool = None,
+ output_polling: bool = None,
+ secret_key: str = None,
+ request_option: requestOptions = None,
+ update_toc: bool = None,
+ output_locale: str = None,
+ return_output : bool = None,
+ output_read_password: str = None,
+ ):
+ """If the parameters are not provided default value will be used.
+
Args:
- filetype (str, optional): The file type (as extension) to use for the output. Defaults to None (set to template-type in printjob.py).
+ filetype (str, optional): The file type (as extension) to use for the output. Defaults to None (set to template-type in printjob.py). Defaults to None.
encoding (str, optional): Encoding of output file. Either "raw" or "base64". Defaults to "raw".
converter (str, optional): The pdf converter to use. Can be "libreoffice", "officetopdf" or any custom defined converter. Custom converters are configurated in the Cloud Office Print server's `aop_config.json` file. Defaults to "libreoffice".
cloud_access_token (CloudAccessToken, optional): Access token used to access various cloud services for output storage. Defaults to None.
server_directory (str, optional): Base directory to save output files into. Can only be used if the server allows to save on disk. The specific output path for each file is appended to the base path. Defaults to None.
pdf_options (PDFOptions, optional): Optional PDF options. Defaults to None.
- append_per_page (bool, optional): Ability to prepend/append file after each page of output.
+ append_per_page (bool, optional): Ability to append file after each page of output. Defaults to None.
+ prepend_per_page (bool, optional): Ability to prepend file after each page of output. Defaults to None.
+ output_polling (bool, optional): A unique link for each request is sent back, which can be used later to download the output file. Defaults to None.
+ secret_key (str, optional): A secret key can be specified to encrypt the file stored on the server (used with output polling). Defaults to None.
+ request_option (requestOptions, optional): AOP makes a call to the given option with response/output of the current request. Defaults to None.
+ update_toc (bool, optional): Update table of contents of Word document.
+ output_locale (str, optional): Locale/language setting for output formatting (e.g. "nep", "en_us"). Defaults to None.
+ output_read_password (str, optional): Password to encrypt and protect the output document (PDF, DOCX, etc).
+ return_output (bool, optional): When True, both saves files to server directory and returns the output. Defaults to None.
"""
self.filetype: str = filetype
self.converter: str = converter
@@ -35,6 +54,14 @@ def __init__(self,
self.pdf_options: PDFOptions = pdf_options
self.encoding = encoding
self.append_per_page = append_per_page
+ self.prepend_per_page = prepend_per_page
+ self.output_polling = output_polling
+ self.secret_key = secret_key
+ self.request_option = request_option
+ self.update_toc = update_toc
+ self.output_locale: str = output_locale
+ self.output_read_password: str = output_read_password
+ self.return_output: bool= return_output
@property
def json(self) -> str:
@@ -69,6 +96,22 @@ def as_dict(self) -> Dict:
result.update(self.pdf_options.as_dict)
if self.append_per_page is not None:
result["output_append_per_page"] = self.append_per_page
+ if self.prepend_per_page is not None:
+ result["output_prepend_per_page"] = self.prepend_per_page
+ if self.output_polling is not None:
+ result['output_polling'] = self.output_polling
+ if self.secret_key is not None:
+ result['secret_key'] = self.secret_key
+ if self.update_toc is not None:
+ result['update_toc'] = self.update_toc
+ if self.output_locale is not None:
+ result["output_locale"] = self.output_locale
+ if self.output_read_password is not None:
+ result["output_read_password"] = self.output_read_password
+ if self.request_option is not None:
+ result['request_option'] = self.request_option.as_dict
+ if self.return_output is not None :
+ result['return_output'] = self.return_output
return result
@property
diff --git a/cloudofficeprint/config/pdf.py b/cloudofficeprint/config/pdf.py
index 0400a04..c4436a7 100644
--- a/cloudofficeprint/config/pdf.py
+++ b/cloudofficeprint/config/pdf.py
@@ -1,5 +1,7 @@
import json
-from typing import Union, Iterable, Dict, Mapping
+from typing import Union, Dict, Mapping
+
+from ..resource import Base64Resource, ServerPathResource, URLResource
class PDFOptions:
@@ -9,38 +11,50 @@ class PDFOptions:
All of them are optional, which is why passing an instance of this class in an OutputConfig is also optional.
"""
- def __init__(self,
- read_password: str = None,
- watermark: str = None,
- watermark_font_size: int = None,
- watermark_opacity: int = None,
- watermark_color: str = None,
- watermark_font: str = None,
- page_width: Union[str, int] = None,
- page_height: Union[str, int] = None,
- even_page: bool = None,
- merge_making_even: bool = None,
- modify_password: str = None,
- password_protection_flag: int = None,
- lock_form: bool = None,
- copies: int = None,
- page_margin: Union[int, dict] = None,
- landscape: bool = None,
- page_format: str = None,
- merge: bool = None,
- sign_certificate: str = None,
- sign_certificate_password: str = None,
- identify_form_fields: bool = None,
- split: bool = None,
- remove_last_page: bool = None):
+ def __init__(
+ self,
+ read_password: str = None,
+ watermark: str = None,
+ watermark_font_size: int = None,
+ watermark_opacity: int = None,
+ watermark_color: str = None,
+ watermark_font: str = None,
+ page_width: Union[str, int] = None,
+ page_height: Union[str, int] = None,
+ even_page: bool = None,
+ merge_making_even: bool = None,
+ modify_password: str = None,
+ password_protection_flag: int = None,
+ lock_form: bool = None,
+ copies: int = None,
+ page_margin: Union[int, dict] = None,
+ landscape: bool = None,
+ page_format: str = None,
+ merge: bool = None,
+ sign_certificate: str = None,
+ sign_certificate_password: str = None,
+ identify_form_fields: bool = None,
+ split: bool = None,
+ remove_last_page: bool = None,
+ sign_certificate_txt: str = None,
+ watermark_rotation: int = None,
+ convert_to_pdfa: str = None,
+ attachment_name: str = None,
+ convert_attachment_to_json: bool = None,
+ insert_barcode: bool = None,
+ page_number_start_at: str = None,
+ batch_selector: str = None,
+ batch_size: int = None,
+ batch_condition: str = None
+ ):
"""
Args:
read_password (str, optional): The password needed to open the PDF. Defaults to None.
- watermark (str, optional): Setting this generates a diagonal custom watermark on every page in the PDF file. Defaults to None.
- watermark_color (str, optional): You can specify to change watermark color. Accepts css colors. Defaults to black.
- watermark_font (str, optional): You can specify to change the font of watermark. Defaults to Aerial.
- watermark_opacity (int, optional): You can specify to change the opacity of watermark. Should be in percentage
- watermark_font_size (int, optional): You can specify to change the font size of watemark. Should be a number(px) ie: 45 .
+ watermark (str, optional): Requires PDF output, generates a diagonal custom watermark on every page of the PDF file. Defaults to None.
+ watermark_font_size (int, optional): Requires PDF output, specifies the size of watermark text specified, should be a number in px, i.e. 45. Defaults to None.
+ watermark_opacity (int, optional): Requires PDF output, specifies the opacity of the watermark text specified, should be as a percentage, i.e. 45. Defaults to None.
+ watermark_color (str, optional): Requires PDF output, specifies the font of the watermark specified, with a default of "black". Defaults to None.
+ watermark_font (str, optional): Requires PDF output, specifies the font of the watermark text specified, with a default of "Arial". Defaults to None.
page_width (Union[str, int], optional): Only for HTML to PDF. Page width in px, mm, cm, in. No unit means px. Defaults to None.
page_height (Union[str, int], optional): Only for HTML to PDF. Page height in px, mm, cm, in. No unit means px. Defaults to None.
even_page (bool, optional): If you want your output to have even pages, for example printing on both sides after merging, you can set this to be true. Defaults to None.
@@ -54,34 +68,55 @@ def __init__(self,
page_format (str, optional): Only for HTML to PDF. The page format: "a4" (default) or "letter". Defaults to None.
merge (bool, optional): If True: instead of returning back a zip file for multiple output, merge it. Defaults to None.
sign_certificate (str, optional): Signing certificate for the output PDF (pkcs #12 .p12/.pfx) as a base64 string, URL, FTP location or a server path. The function read_file_as_base64() from file_utils.py can be used to read local .p12 or .pfx file as base64. Defaults to None.
- sign_certificate_password (str, optional): It is possible to sign certificate with password.
+ sign_certificate_password (str, optional): If you are signing with a password protected certificate, you can specify the password as a plain string. Defaults to None.
identify_form_fields (bool, optional): Identify the form fields in a PDF-form by filling the name of each field into the respective field. Defaults to None.
split (bool, optional): You can specify to split a PDF in separate files. You will get one file per page in a zip file. Defaults to None.
- remove_last_page (bool, optional): You can specify to remove the last page from output file, this is helpful when the last page of output is blank.
+ remove_last_page (bool, optional): Remove the last page from the given PDF document. Defaults to None.
+ sign_certificate_txt (str, optional): Add custom text in any language to the signature field
+ watermark_rotation (int, optional): Requires PDF output, specifies the angle of watermark text specified, should be a number, i.e. 45. Defaults to None.
+ convert_to_pdfa (str, optional): For generating PDF/A format. While converting using openoffice converter, specifying it will create PDF/A format, values can be either 1b or 2b which are the variants of PDF/A specification.
+ attachment_name (str, optional): To retrieve specific attachment. output_type must be get_attachments.
+ convert_attachment_to_json (bool, optional): To retrieve data of the XML attachment as a JSON. output_type must be get_attachments.
+ insert_barcode (bool, optional): To insert barcode in pdf.
+ page_number_start_at (str, optional): Provide start of the page number. Defaults to None.
+ batch_selector (str, optional): Specifies the hierarchy of keys to split data into batches. Defaults to None.
+ batch_size (int, optional): Number of batches or files to split into. Defaults to None.
+ batch_condition (str, optional): Condition to determine batch allocation. Defaults to Non
"""
self.read_password: str = read_password
self.watermark: str = watermark
- self.watermark_font: str = watermark_font
- self.watermark_font_size: str = watermark_font_size
+ self.watermark_font_size: int = watermark_font_size
+ self.watermark_opacity: int = watermark_opacity
self.watermark_color: str = watermark_color
- self.watermark_opacity: str = watermark_opacity
+ self.watermark_font: str = watermark_font
self.page_width: Union[str, int] = page_width
self.page_height: Union[str, int] = page_height
self.even_page: bool = even_page
- self.merge_making_even: bool = merge_making_even
self.modify_password: str = modify_password
+ self.merge_making_even: bool = merge_making_even
self.password_protection_flag: int = password_protection_flag
self.lock_form: bool = lock_form
self.copies: int = copies
+ self.page_margin: Union[int, dict] = page_margin
+ self._landscape: bool = landscape
self.page_format: str = page_format
self.merge: bool = merge
- self.page_margin: Union[int, dict] = page_margin
self.sign_certificate: str = sign_certificate
self.sign_certificate_password: str = sign_certificate_password
- self._landscape: bool = landscape
self.identify_form_fields: bool = identify_form_fields
self.split: bool = split
- self.remove_last_page = remove_last_page
+ self.remove_last_page: bool = remove_last_page
+ self.sign_certificate_txt: str = sign_certificate_txt
+ self.watermark_rotation: int = watermark_rotation
+ self.convert_to_pdfa: str = convert_to_pdfa
+ self.attachment_name: str = attachment_name
+ self.convert_attachment_to_json: bool = convert_attachment_to_json
+ self.insert_barcode: bool = insert_barcode
+ self.page_number_start_at = page_number_start_at
+ self.batch_selector : str = batch_selector
+ self.batch_size: int = batch_size
+ self.batch_condition : str = batch_condition
+
def __str__(self) -> str:
"""Get the string representation of these PDF options.
@@ -116,6 +151,8 @@ def as_dict(self) -> Dict:
result["output_even_page"] = self.even_page
if self.merge_making_even is not None:
result["output_merge_making_even"] = self.merge_making_even
+ if self.remove_last_page is not None:
+ result["output_remove_last_page"] = self.remove_last_page
if self.modify_password is not None:
result["output_modify_password"] = self.modify_password
if self.read_password is not None:
@@ -132,6 +169,8 @@ def as_dict(self) -> Dict:
result["output_watermark_opacity"] = self.watermark_opacity
if self.watermark_font_size is not None:
result["output_watermark_size"] = self.watermark_font_size
+ if self.watermark_rotation is not None:
+ result["output_watermark_rotation"] = self.watermark_rotation
if self.lock_form is not None:
result["lock_form"] = self.lock_form
if self.copies is not None:
@@ -139,6 +178,9 @@ def as_dict(self) -> Dict:
if self.page_margin is not None:
# For Cloud Office Print versions later than 21.1.1, output_page_margin will also be supported
result["page_margin"] = self.page_margin
+ if self._landscape is not None:
+ # For Cloud Office Print versions later than 21.1.1, output_page_orientation will also be supported
+ result["page_orientation"] = self.page_orientation
if self.page_width is not None:
result["output_page_width"] = self.page_width
if self.page_height is not None:
@@ -147,21 +189,63 @@ def as_dict(self) -> Dict:
result["output_page_format"] = self.page_format
if self.merge is not None:
result["output_merge"] = self.merge
- if self._landscape is not None:
- # For Cloud Office Print versions later than 21.1.1, output_page_orientation will also be supported
- result["page_orientation"] = self.page_orientation
+ if self.split is not None:
+ result["output_split"] = self.split
+ if self.identify_form_fields is not None:
+ result["identify_form_fields"] = self.identify_form_fields
if self.sign_certificate is not None:
result["output_sign_certificate"] = self.sign_certificate
if self.sign_certificate_password is not None:
- result['output_sign_certificate_password'] = self.sign_certificate_password
- if self.identify_form_fields is not None:
- result["identify_form_fields"] = self.identify_form_fields
- if self.split is not None:
- result['output_split'] = self.split
- if self.remove_last_page is not None:
- result['output_remove_last_page'] = self.remove_last_page
+ result["output_sign_certificate_password"] = self.sign_certificate_password
+ if self.sign_certificate_txt is not None:
+ result["output_sign_certificate_txt"] = self.sign_certificate_txt
+ if self.convert_to_pdfa is not None:
+ result["output_convert_to_pdfa"] = self.convert_to_pdfa
+ if self.attachment_name is not None:
+ result["output_attachment_name"] = self.attachment_name
+ if self.convert_attachment_to_json is not None:
+ result["output_convert_attachment_to_json"] = self.convert_attachment_to_json
+ if self.insert_barcode is not None:
+ result["output_insert_barcode"] = self.insert_barcode
+ if self.page_number_start_at is not None:
+ result['output_page_number_start_at'] = self.page_number_start_at
+ if self.batch_selector is not None:
+ result["batch_selector"] = self.batch_selector
+ if self.batch_size is not None:
+ result["batch_size"] = self.batch_size
+ if self.batch_condition is not None:
+ result["batch_condition"] = self.batch_condition
return result
+ def set_watermark(
+ self,
+ text: str = None,
+ color: str = None,
+ font: str = None,
+ opacity: int = None,
+ size: int = None,
+ rotation: int = None,
+ ):
+ """Set watermark
+
+ Set a diagonal custom watermark on every page in the PDF file with a specific text, color, font, opacity and size.
+ Setting all to None will remove the watermark.
+
+ Args:
+ text (str, optional): Requires PDF output, generates a diagonal custom watermark on every page of the PDF file. Defaults to None.
+ color (str, optional): Requires PDF output, specifies the font of the watermark specified, with a default of "black". Defaults to None.
+ font (str, optional): Requires PDF output, specifies the font of the watermark text specified, with a default of "Arial". Defaults to None.
+ opacity (int, optional): Requires PDF output, specifies the opacity of the watermark text specified, should be as a percentage, i.e. 45. Defaults to None.
+ size (int, optional): Requires PDF output, specifies the size of watermark text specified, should be a number in px, i.e. 45. Defaults to None.
+ rotation (int, optional): Requires PDF output, specifies the angle of watermark text specified, should be a number in px, i.e. 45. Defaults to None.
+ """
+ self.watermark = text
+ self.watermark_color = color
+ self.watermark_font = font
+ self.watermark_opacity = opacity
+ self.watermark_font_size = size
+ self.watermark_rotation = rotation
+
def set_page_margin_at(self, value: int, position: str = None):
"""Set page_margin
@@ -177,9 +261,7 @@ def set_page_margin_at(self, value: int, position: str = None):
self.page_margin[position] = value
elif self.page_margin is None:
# page margin not yet defined, set it to a dict with this position defined
- self.page_margin = {
- position: value
- }
+ self.page_margin = {position: value}
else:
# page margin defined but no dict, convert to dict first
current = self.page_margin
@@ -187,7 +269,7 @@ def set_page_margin_at(self, value: int, position: str = None):
"top": current,
"bottom": current,
"left": current,
- "right": current
+ "right": current,
}
self.page_margin[position] = value
else:
@@ -210,3 +292,17 @@ def page_orientation(self, value: str):
value (str): the page orientation
"""
self._landscape = value == "landscape"
+
+ def sign(
+ self,
+ certificate: Union[Base64Resource, ServerPathResource, URLResource],
+ password: str = None,
+ ):
+ """Sign the output PDF with a certificate file.
+
+ Args:
+ certificate (str): Resource of the certificate file.
+ password (str): password of the certificate. Defaults to None.
+ """
+ self.sign_certificate = certificate.data
+ self.sign_certificate_password = password
diff --git a/cloudofficeprint/config/request_option.py b/cloudofficeprint/config/request_option.py
new file mode 100644
index 0000000..04481aa
--- /dev/null
+++ b/cloudofficeprint/config/request_option.py
@@ -0,0 +1,15 @@
+class requestOptions:
+ def __init__(self,
+ url: str,
+ extraHeaders: object,
+ ):
+ super()
+ self.url = url
+ self.extraHeaders = extraHeaders
+
+ @property
+ def as_dict(self):
+ result = {}
+ result['url'] = self.url
+ result['extra_headers'] = self.extraHeaders
+ return result
diff --git a/cloudofficeprint/config/server.py b/cloudofficeprint/config/server.py
index 2d6645d..003241a 100644
--- a/cloudofficeprint/config/server.py
+++ b/cloudofficeprint/config/server.py
@@ -47,9 +47,7 @@ def _dict(self) -> Dict[str, str]:
class Command:
"""Command object with a single command for the Cloud Office Print server."""
- def __init__(self,
- command: str,
- parameters: Mapping[str, str] = None):
+ def __init__(self, command: str, parameters: Mapping[str, str] = None):
"""
Args:
command (str): The name of the command to execute. This command should be present in the aop_config.json file.
@@ -65,9 +63,7 @@ def _dict(self) -> Dict[str, str]:
Returns:
Dict[str, str]: dict representation of this command
"""
- result = {
- "command": self.command
- }
+ result = {"command": self.command}
if self.parameters:
result["command_parameters"] = self.parameters
@@ -96,13 +92,15 @@ def _dict_post(self) -> Dict[str, str]:
class Commands:
"""Command hook configuration class."""
- def __init__(self,
- post_process: Command = None,
- post_process_return: bool = None,
- post_process_delete_delay: int = None,
- pre_conversion: Command = None,
- post_conversion: Command = None,
- post_merge: Command = None):
+ def __init__(
+ self,
+ post_process: Command = None,
+ post_process_return: bool = None,
+ post_process_delete_delay: int = None,
+ pre_conversion: Command = None,
+ post_conversion: Command = None,
+ post_merge: Command = None,
+ ):
"""
Args:
post_process (Command, optional): Command to run after the given request has been processed but before returning back the output file. Defaults to None.
@@ -152,13 +150,15 @@ def _dict(self) -> Dict:
class ServerConfig:
"""Class for configuring the server options."""
- def __init__(self,
- api_key: str = None,
- logging: Mapping = None,
- printer: Printer = None,
- commands: Commands = None,
- proxies: Dict[str, str] = None,
- cop_remote_debug: bool = False):
+ def __init__(
+ self,
+ api_key: str = None,
+ logging: Mapping = None,
+ printer: Printer = None,
+ commands: Commands = None,
+ proxies: Dict[str, str] = None,
+ cop_remote_debug: bool = False,
+ ):
"""
Args:
api_key (str, optional): API key to use for communicating with a Cloud Office Print server. Defaults to None.
@@ -226,10 +226,9 @@ def url(self, value: str):
Args:
value (str): URL at which to contact the server
"""
- if (urlparse(value).scheme == ''):
+ if urlparse(value).scheme == "":
self._url = "http://" + value
- logging.warning(
- f'No scheme found in "{value}", assuming "{self._url}".')
+ logging.warning(f'No scheme found in "{value}", assuming "{self._url}".')
else:
self._url = value
@@ -240,8 +239,10 @@ def is_reachable(self) -> bool:
bool: whether the server at `Server.url` is reachable
"""
try:
- r = requests.get(urljoin(
- self.url, "marco"), proxies=self.config.proxies if self.config is not None else None)
+ r = requests.get(
+ urljoin(self.url, "marco"),
+ proxies=self.config.proxies if self.config is not None else None,
+ )
return r.text == "polo"
except requests.exceptions.ConnectionError:
return False
@@ -268,8 +269,7 @@ def _raise_if_unreachable(self):
ConnectionError: raise error if server is unreachable
"""
if not self.is_reachable():
- raise ConnectionError(
- f"Could not reach server at {self.url}")
+ raise ConnectionError(f"Could not reach server at {self.url}")
def get_version_soffice(self) -> str:
"""Sends a GET request to server-url/soffice.
@@ -278,7 +278,10 @@ def get_version_soffice(self) -> str:
str: current version of Libreoffice installed on the server.
"""
self._raise_if_unreachable()
- return requests.get(urljoin(self.url, 'soffice'), proxies=self.config.proxies if self.config is not None else None).text
+ return requests.get(
+ urljoin(self.url, "soffice"),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
def get_version_officetopdf(self) -> str:
"""Sends a GET request to server-url/officetopdf.
@@ -287,7 +290,10 @@ def get_version_officetopdf(self) -> str:
str: current version of OfficeToPdf installed on the server. (Only available if the server runs in Windows environment).
"""
self._raise_if_unreachable()
- return requests.get(urljoin(self.url, 'officetopdf'), proxies=self.config.proxies if self.config is not None else None).text
+ return requests.get(
+ urljoin(self.url, "officetopdf"),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
def get_supported_template_mimetypes(self) -> Dict:
"""Sends a GET request to server-url/supported_template_mimetypes.
@@ -296,7 +302,12 @@ def get_supported_template_mimetypes(self) -> Dict:
Dict: JSON of the mime types of templates that Cloud Office Print supports.
"""
self._raise_if_unreachable()
- return json.loads(requests.get(urljoin(self.url, 'supported_template_mimetypes'), proxies=self.config.proxies if self.config is not None else None).text)
+ return json.loads(
+ requests.get(
+ urljoin(self.url, "supported_template_mimetypes"),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
+ )
def get_supported_output_mimetypes(self, input_type: str) -> Dict:
"""Sends a GET request to server-url/supported_output_mimetypes?template=input_type.
@@ -309,7 +320,14 @@ def get_supported_output_mimetypes(self, input_type: str) -> Dict:
Dict: JSON of the supported output types for the given template extension.
"""
self._raise_if_unreachable()
- return json.loads(requests.get(urljoin(self.url, 'supported_output_mimetypes' + f'?template={input_type}'), proxies=self.config.proxies if self.config is not None else None).text)
+ return json.loads(
+ requests.get(
+ urljoin(
+ self.url, "supported_output_mimetypes" + f"?template={input_type}"
+ ),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
+ )
def get_supported_prepend_mimetypes(self) -> Dict:
"""Sends a GET request to server-url/supported_prepend_mimetypes.
@@ -318,7 +336,12 @@ def get_supported_prepend_mimetypes(self) -> Dict:
Dict: JSON of the supported prepend file mime types.
"""
self._raise_if_unreachable()
- return json.loads(requests.get(urljoin(self.url, 'supported_prepend_mimetypes'), proxies=self.config.proxies if self.config is not None else None).text)
+ return json.loads(
+ requests.get(
+ urljoin(self.url, "supported_prepend_mimetypes"),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
+ )
def get_supported_append_mimetypes(self) -> Dict:
"""Sends a GET request to server-url/supported_append_mimetypes.
@@ -327,7 +350,29 @@ def get_supported_append_mimetypes(self) -> Dict:
Dict: JSON of the supported append file mime types.
"""
self._raise_if_unreachable()
- return json.loads(requests.get(urljoin(self.url, 'supported_append_mimetypes'), proxies=self.config.proxies if self.config is not None else None).text)
+ return json.loads(
+ requests.get(
+ urljoin(self.url, "supported_append_mimetypes"),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
+ )
+
+ def verify_template_hash(self, hashcode: str) -> bool:
+ """Sends a GET request to server-url/verify_template_hash?hash=hashcode.
+
+ Args:
+ hashcode (str): md5 hash of file
+
+ Returns:
+ bool: whether the hash is valid and present in cache.
+ """
+ self._raise_if_unreachable()
+ return json.loads(
+ requests.get(
+ urljoin(self.url, "verify_template_hash" + f"?hash={hashcode}"),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
+ )["valid"]
def get_version_cop(self) -> str:
"""Sends a GET request to server-url/version.
@@ -336,4 +381,27 @@ def get_version_cop(self) -> str:
str: the version of Cloud Office Print that the server runs.
"""
self._raise_if_unreachable()
- return requests.get(urljoin(self.url, 'version'), proxies=self.config.proxies if self.config is not None else None).text
+ return requests.get(
+ urljoin(self.url, "version"),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
+
+ def check_ipp(self, ipp_url: str, version: str) -> Dict:
+ """Sends a GET request to server-url/ipp_check?ipp_url=ipp_url&version=version.
+
+ Args:
+ ippURL (str): the URL of the IPP printer.
+ version (str): the version of the IPP printer.
+
+ Returns:
+ Dict: the status of the IPP printer.
+ """
+ self._raise_if_unreachable()
+ return json.loads(
+ requests.get(
+ urljoin(
+ self.url, "ipp_check" + f"?ipp_url={ipp_url}&version={version}"
+ ),
+ proxies=self.config.proxies if self.config is not None else None,
+ ).text
+ )
diff --git a/cloudofficeprint/elements/__init__.py b/cloudofficeprint/elements/__init__.py
index a10fdfc..888ee71 100644
--- a/cloudofficeprint/elements/__init__.py
+++ b/cloudofficeprint/elements/__init__.py
@@ -9,3 +9,4 @@
from .loops import *
from .pdf import *
from .rest_source import *
+from .form import *
diff --git a/cloudofficeprint/elements/charts.py b/cloudofficeprint/elements/charts.py
index 9b04e12..0855b92 100644
--- a/cloudofficeprint/elements/charts.py
+++ b/cloudofficeprint/elements/charts.py
@@ -193,7 +193,11 @@ def __init__(self,
background_opacity: int = None,
title: str = None,
title_style: ChartTextStyle = None,
- grid: bool = None):
+ grid: bool = None,
+ holeSize: int = None,
+ firstSliceAngle: int = None,
+ enableAreaTransparency: bool = None
+ ):
"""
Args:
x_axis (ChartAxisOptions, optional): The options for the x-axis. Defaults to None.
@@ -210,6 +214,9 @@ def __init__(self,
title (str, optional): The title of the chart. Defaults to None.
title_style (ChartTextStyle, optional): The styling for the title of the chart. Defaults to None.
grid (bool, optional): Whether or not the chart should have a grid. Defaults to None.
+ holeSize (int, optional): Hole size for doughnut chart (0-100).
+ firstSLiceAngle (int , optional): Angle of first slice for dough chart (0-360). Must be specified for holeSize option to work
+ enableAreaTransparency (bool, option): Whether to make area chart transparent.
"""
self._legend_options: dict = None
self._data_labels_options: dict = None
@@ -226,6 +233,9 @@ def __init__(self,
self.title: str = title
self.title_style: ChartTextStyle = title_style
self.grid: bool = grid
+ self.holeSize: int = holeSize
+ self.firstSliceAngle: int = firstSliceAngle
+ self.enableAreaTransparency: bool = enableAreaTransparency
def set_legend(self, position: str = 'r', style: ChartTextStyle = None):
"""Setter for the legend of the chart.
@@ -325,6 +335,12 @@ def as_dict(self) -> Dict:
result["titleStyle"] = self.title_style.as_dict
if self.grid is not None:
result["grid"] = self.grid
+ if self.firstSliceAngle is not None:
+ result["firstSliceAngle"] = self.firstSliceAngle
+ if self.holeSize is not None:
+ result["holeSize"] = self.holeSize
+ if self.enableAreaTransparency is not None:
+ result["enableAreaTransparency"] = self.enableAreaTransparency
if self._legend_options is not None:
result["legend"] = self._legend_options
if self._data_labels_options is not None:
@@ -655,7 +671,7 @@ def from_dataframe(cls, data: 'pandas.DataFrame', name: str = None) -> 'StockSer
# better to have a series for every possible chart for future-proofing, in case their options diverge later
BarSeries = BarStackedSeries = BarStackedPercentSeries = ColumnSeries = ColumnStackedSeries = ColumnStackedPercentSeries = ScatterSeries = XYSeries
-RadarSeries = LineSeries
+RadarSeries = LineStackedSeries = LineSeries
class Chart(Element, ABC):
@@ -715,6 +731,26 @@ def as_dict(self) -> Dict:
"lines": [line.as_dict for line in self.lines],
"type": "line"
})
+
+class LineStackedChart(Chart):
+ """Class for a line chart"""
+
+ def __init__(self, name: str, lines: Tuple[Union[LineStackedSeries, XYSeries]], options: ChartOptions = None):
+ """
+ Args:
+ name (str): The name of the chart.
+ lines (Tuple[Union[LineStackedSeries, XYSeries]]): Iterable of line series.
+ options (Union[ChartOptions, dict], optional): The options for the chart. Defaults to None.
+ """
+ super().__init__(name, options)
+ self.lines: Tuple[Union[LineStackedSeries, XYSeries]] = lines
+
+ @property
+ def as_dict(self) -> Dict:
+ return self._get_dict({
+ "lines": [line.as_dict for line in self.lines],
+ "type": "lineStacked"
+ })
class BarChart(Chart):
@@ -947,6 +983,25 @@ def as_dict(self) -> Dict:
"areas": [area.as_dict for area in self.areas],
"type": "area"
})
+class AreaStackedChart(Chart):
+ """Class for an area stacked chart"""
+
+ def __init__(self, name: str, areas: Tuple[Union[AreaSeries, XYSeries]], options: ChartOptions = None):
+ """
+ Args:
+ name (str): The name of the chart.
+ areas (Tuple[Union[AreaSeries, XYSeries]]): Iterable of area series.
+ options (Union[ChartOptions, dict], optional): The options for the chart. Defaults to None.
+ """
+ super().__init__(name, options)
+ self.areas: Tuple[Union[AreaSeries, XYSeries]] = areas
+
+ @property
+ def as_dict(self) -> Dict:
+ return self._get_dict({
+ "areas": [area.as_dict for area in self.areas],
+ "type": "areaStacked"
+ })
class ScatterChart(Chart):
diff --git a/cloudofficeprint/elements/elements.py b/cloudofficeprint/elements/elements.py
index fff9c51..e7482f5 100644
--- a/cloudofficeprint/elements/elements.py
+++ b/cloudofficeprint/elements/elements.py
@@ -39,25 +39,172 @@ def _dict_suffixes(self) -> Dict:
class CellStyleDocx(CellStyle):
"""Cell styling settings for docx templates"""
- def __init__(self, cell_background_color: str = None, width: Union[int, str] = None):
+ def __init__(
+ self,
+ cell_background_color: str = None,
+ width: Union[int, str] = None,
+ preserve_total_width_of_table: str = None,
+ border: str = None,
+ border_top: str = None,
+ border_bottom: str = None,
+ border_left: str = None,
+ border_right: str = None,
+ border_diagonal_down: str = None,
+ border_diagonal_up: str = None,
+ border_color: str = None,
+ border_top_color: str = None,
+ border_bottom_color: str = None,
+ border_left_color: str = None,
+ border_right_color: str = None,
+ border_diagonal_up_color: str = None,
+ border_diagonal_down_color: str = None,
+ border_size: Union[int, str] = None,
+ border_top_size: Union[int, str] = None,
+ border_bottom_size: Union[int, str] = None,
+ border_left_size: Union[int, str] = None,
+ border_right_size: Union[int, str] = None,
+ border_diagonal_up_size: Union[int, str] = None,
+ border_diagonal_down_size: Union[int, str] = None,
+ border_space: Union[int, str] = None,
+ border_top_space: Union[int, str] = None,
+ border_bottom_space: Union[int, str] = None,
+ border_left_space: Union[int, str] = None,
+ border_right_space: Union[int, str] = None,
+ border_diagonal_up_space: Union[int, str] = None,
+ border_diagonal_down_space: Union[int, str] = None,
+ ):
"""
Args:
cell_background_color (str, optional): The background color of the cell. Defaults to None.
width (Union[int, str], optional): The width of the cell. Defaults to None.
+ preserve_total_width_of_table (str, optional): Keeps table width constant by redistributing removed column's width to others.
+ border (str, optional): Applies the specified border style to all table edges (top, bottom, left, right).
+ border_top (str, optional): Applies the specified border style to the top edge of the table.
+ border_bottom (str, optional): Applies the specified border style to the bottom edge of the table.
+ border_left (str, optional): Applies the specified border style to the left edge of the table.
+ border_right (str, optional): Applies the specified border style to the right edge of the table.
+ border_diagonal_down (str, optional): Applies the specified border style to the diagonal line going from the top-left to the bottom-right corner.
+ border_diagonal_up (str, optional): Applies the specified border style to the diagonal line going from the bottom-left to the top-right corner.
+ border_color (str, optional): Sets the color of the borders (top, bottom, left, right).
+ border_top_color (str, optional): Sets the color of the top border.
+ border_bottom_color (str, optional): Sets the color of the bottom border.
+ border_left_color (str, optional): Sets the color of the left border.
+ border_right_color (str, optional): Sets the color of the right border.
+ border_diagonal_up_color (str, optional): Sets the color of the diagonal up border.
+ border_diagonal_down_color (str, optional): Sets the color of the diagonal down border.
+ border_size (Union[int, str], optional): Sets the width of the borders (top, bottom, left, right) in points.
+ border_top_size (Union[int, str], optional): Sets the width of the top border in points.
+ border_bottom_size (Union[int, str], optional): Sets the width of the bottom border in points.
+ border_left_size (Union[int, str], optional): Sets the width of the left border in points.
+ border_right_size (Union[int, str], optional): Sets the width of the right border in points.
+ border_diagonal_up_size (Union[int, str], optional): Sets the width of the diagonal up border in points.
+ border_diagonal_down_size (Union[int, str], optional): Sets the width of the diagonal down border in points.
+ border_space (Union[int, str], optional): Sets the spacing between the content and borders (top, bottom, left, right) in points.
+ border_top_space (Union[int, str], optional): Sets the spacing between the content and the top border in points.
+ border_bottom_space (Union[int, str], optional): Sets the spacing between the content and the bottom border in points.
+ border_left_space (Union[int, str], optional): Sets the spacing between the content and the left border in points.
+ border_right_space (Union[int, str], optional): Sets the spacing between the content and the right border in points.
+ border_diagonal_up_space (Union[int, str], optional): Sets the spacing between the content and the diagonal up border in points.
+ border_diagonal_down_space (Union[int, str], optional): Sets the spacing between the content and the diagonal down border in points.
"""
super().__init__()
self.cell_background_color: str = cell_background_color
self.width: Union[int, str] = width
-
+ self.preserve_total_width_of_table: str = preserve_total_width_of_table
+ self.border: str = border
+ self.border_top: str = border_top
+ self.border_bottom: str = border_bottom
+ self.border_left: str = border_left
+ self.border_right: str = border_right
+ self.border_diagonal_down: str = border_diagonal_down
+ self.border_diagonal_up: str = border_diagonal_up
+ self.border_color: str = border_color
+ self.border_top_color: str = border_top_color
+ self.border_bottom_color: str = border_bottom_color
+ self.border_left_color: str = border_left_color
+ self.border_right_color: str = border_right_color
+ self.border_diagonal_up_color: str = border_diagonal_up_color
+ self.border_diagonal_down_color: str = border_diagonal_down_color
+ self.border_size: Union[int, str] = border_size
+ self.border_top_size: Union[int, str] = border_top_size
+ self.border_bottom_size: Union[int, str] = border_bottom_size
+ self.border_left_size: Union[int, str] = border_left_size
+ self.border_right_size: Union[int, str] = border_right_size
+ self.border_diagonal_up_size: Union[int, str] = border_diagonal_up_size
+ self.border_diagonal_down_size: Union[int, str] = border_diagonal_down_size
+ self.border_space: Union[int, str] = border_space
+ self.border_top_space: Union[int, str] = border_top_space
+ self.border_bottom_space: Union[int, str] = border_bottom_space
+ self.border_left_space: Union[int, str] = border_left_space
+ self.border_right_space: Union[int, str] = border_right_space
+ self.border_diagonal_up_space: Union[int, str] = border_diagonal_up_space
+ self.border_diagonal_down_space: Union[int, str] = border_diagonal_down_space
+
@property
def _dict_suffixes(self):
result = super()._dict_suffixes
-
if self.cell_background_color is not None:
- result['_cell_background_color'] = self.cell_background_color
+ result["_cell_background_color"] = self.cell_background_color
if self.width is not None:
- result['_width'] = self.width
-
+ result["_width"] = self.width
+ if self.preserve_total_width_of_table is not None:
+ result["_preserve_total_width_of_table"] = self.preserve_total_width_of_table
+ if self.border is not None:
+ result["_border"] = self.border
+ if self.border_top is not None:
+ result["_border_top"] = self.border_top
+ if self.border_bottom is not None:
+ result["_border_bottom"] = self.border_bottom
+ if self.border_left is not None:
+ result["_border_left"] = self.border_left
+ if self.border_right is not None:
+ result["_border_right"] = self.border_right
+ if self.border_diagonal_down is not None:
+ result["_border_diagonal_down"] = self.border_diagonal_down
+ if self.border_diagonal_up is not None:
+ result["_border_diagonal_up"] = self.border_diagonal_up
+ if self.border_color is not None:
+ result["_border_color"] = self.border_color
+ if self.border_top_color is not None:
+ result["_border_top_color"] = self.border_top_color
+ if self.border_bottom_color is not None:
+ result["_border_bottom_color"] = self.border_bottom_color
+ if self.border_left_color is not None:
+ result["_border_left_color"] = self.border_left_color
+ if self.border_right_color is not None:
+ result["_border_right_color"] = self.border_right_color
+ if self.border_diagonal_up_color is not None:
+ result["_border_diagonal_up_color"] = self.border_diagonal_up_color
+ if self.border_diagonal_down_color is not None:
+ result["_border_diagonal_down_color"] = self.border_diagonal_down_color
+ if self.border_size is not None:
+ result["_border_size"] = self.border_size
+ if self.border_top_size is not None:
+ result["_border_top_size"] = self.border_top_size
+ if self.border_bottom_size is not None:
+ result["_border_bottom_size"] = self.border_bottom_size
+ if self.border_left_size is not None:
+ result["_border_left_size"] = self.border_left_size
+ if self.border_right_size is not None:
+ result["_border_right_size"] = self.border_right_size
+ if self.border_diagonal_up_size is not None:
+ result["_border_diagonal_up_size"] = self.border_diagonal_up_size
+ if self.border_diagonal_down_size is not None:
+ result["_border_diagonal_down_size"] = self.border_diagonal_down_size
+ if self.border_space is not None:
+ result["_border_space"] = self.border_space
+ if self.border_top_space is not None:
+ result["_border_top_space"] = self.border_top_space
+ if self.border_bottom_space is not None:
+ result["_border_bottom_space"] = self.border_bottom_space
+ if self.border_left_space is not None:
+ result["_border_left_space"] = self.border_left_space
+ if self.border_right_space is not None:
+ result["_border_right_space"] = self.border_right_space
+ if self.border_diagonal_up_space is not None:
+ result["_border_diagonal_up_space"] = self.border_diagonal_up_space
+ if self.border_diagonal_down_space is not None:
+ result["_border_diagonal_down_space"] = self.border_diagonal_down_space
return result
@@ -91,7 +238,12 @@ def __init__(
border_diagonal_color: str = None,
text_h_alignment: str = None,
text_v_alignment: str = None,
- text_rotation: Union[int, str] = None
+ text_rotation: Union[int, str] = None,
+ wrap_text: bool = False,
+ width: Union[int, str] = None,
+ height: Union[int, str] = None,
+ max_characters : Union[int, str] = None,
+ height_scaling : Union[int, str] = None,
):
"""
Args:
@@ -120,7 +272,12 @@ def __init__(
border_diagonal_color (str, optional): hex color e.g: #000000. Defaults to None.
text_h_alignment (str, optional): [top|bottom|center|justify]. Defaults to None.
text_v_alignment (str, optional): [top|bottom|center|justify]. Defaults to None.
- text_rotation (Union[int, str], optional): rotation of text value from 0-90 degrees. Defaults to None.
+ text_rotation (Union[int, str], optional): Rotation of text value from 0-90 degrees. Defaults to None.
+ wrap_text (bool, optional): Set to true for wrap text. When false (default )S,the property won't be included in the output.
+ width (Union[int, str], optional): Provide a custom width to the cell. Supported units: inch, cm, px, pt, em, Excel Units(eu)
+ height (Union[int, str], optional): Provide custom height to the cell. Supported units: inch, cm, px, pt, em, Excel Units(eu)
+ max_characters (Union[int, str], optional): Provide width for the cell.
+ height_scaling (Union[int, str], optional): Adjusts cell height for consistent rendering.
"""
super().__init__()
self.cell_locked: bool = cell_locked
@@ -149,69 +306,87 @@ def __init__(
self.text_h_alignment: str = text_h_alignment
self.text_v_alignment: str = text_v_alignment
self.text_rotation: Union[int, str] = text_rotation
+ self.wrap_text: bool = wrap_text
+ self.width: Union[int, str] = width
+ self.height: Union[int, str] = height
+ self.max_characters: Union[int, str] = max_characters
+ self.height_scaling: Union[int, str] = height_scaling
+
+
+
@property
def _dict_suffixes(self):
result = super()._dict_suffixes
if self.cell_locked is not None:
- result['_cell_locked'] = self.cell_locked
+ result["_cell_locked"] = self.cell_locked
if self.cell_hidden is not None:
- result['_cell_hidden'] = self.cell_hidden
+ result["_cell_hidden"] = self.cell_hidden
if self.cell_background is not None:
- result['_cell_background'] = self.cell_background
+ result["_cell_background"] = self.cell_background
if self.font_name is not None:
- result['_font_name'] = self.font_name
+ result["_font_name"] = self.font_name
if self.font_size is not None:
- result['_font_size'] = self.font_size
+ result["_font_size"] = self.font_size
if self.font_color is not None:
- result['_font_color'] = self.font_color
+ result["_font_color"] = self.font_color
if self.font_italic is not None:
- result['_font_italic'] = self.font_italic
+ result["_font_italic"] = self.font_italic
if self.font_bold is not None:
- result['_font_bold'] = self.font_bold
+ result["_font_bold"] = self.font_bold
if self.font_strike is not None:
- result['_font_strike'] = self.font_strike
+ result["_font_strike"] = self.font_strike
if self.font_underline is not None:
- result['_font_underline'] = self.font_underline
+ result["_font_underline"] = self.font_underline
if self.font_superscript is not None:
- result['_font_superscript'] = self.font_superscript
+ result["_font_superscript"] = self.font_superscript
if self.font_subscript is not None:
- result['_font_subscript'] = self.font_subscript
+ result["_font_subscript"] = self.font_subscript
if self.border_top is not None:
- result['_border_top'] = self.border_top
+ result["_border_top"] = self.border_top
if self.border_top_color is not None:
- result['_border_top_color'] = self.border_top_color
+ result["_border_top_color"] = self.border_top_color
if self.border_bottom is not None:
- result['_border_bottom'] = self.border_bottom
+ result["_border_bottom"] = self.border_bottom
if self.border_bottom_color is not None:
- result['_border_bottom_color'] = self.border_bottom_color
+ result["_border_bottom_color"] = self.border_bottom_color
if self.border_left is not None:
- result['_border_left'] = self.border_left
+ result["_border_left"] = self.border_left
if self.border_left_color is not None:
- result['_border_left_color'] = self.border_left_color
+ result["_border_left_color"] = self.border_left_color
if self.border_right is not None:
- result['_border_right'] = self.border_right
+ result["_border_right"] = self.border_right
if self.border_right_color is not None:
- result['_border_right_color'] = self.border_right_color
+ result["_border_right_color"] = self.border_right_color
if self.border_diagonal is not None:
- result['_border_diagonal'] = self.border_diagonal
+ result["_border_diagonal"] = self.border_diagonal
if self.border_diagonal_direction is not None:
- result['_border_diagonal_direction'] = self.border_diagonal_direction
+ result["_border_diagonal_direction"] = self.border_diagonal_direction
if self.border_diagonal_color is not None:
- result['_border_diagonal_color'] = self.border_diagonal_color
+ result["_border_diagonal_color"] = self.border_diagonal_color
if self.text_h_alignment is not None:
- result['_text_h_alignment'] = self.text_h_alignment
+ result["_text_h_alignment"] = self.text_h_alignment
if self.text_v_alignment is not None:
- result['_text_v_alignment'] = self.text_v_alignment
+ result["_text_v_alignment"] = self.text_v_alignment
if self.text_rotation is not None:
- result['_text_rotation'] = self.text_rotation
+ result["_text_rotation"] = self.text_rotation
+ if self.wrap_text is True:
+ result["_wrap_text"] = self.wrap_text
+ if self.width is not None:
+ result["_width"] = self.width
+ if self.height is not None:
+ result["_height"] = self.height
+ if self.max_characters is not None:
+ result["_max_characters"] = self.max_characters
+ if self.height_scaling is not None:
+ result["_height_scaling"] = self.height_scaling
return result
class Element(ABC):
- """ The abstract base class for elements."""
+ """The abstract base class for elements."""
def __init__(self, name: str):
"""
@@ -292,9 +467,7 @@ def available_tags(self) -> FrozenSet[str]:
@property
def as_dict(self) -> Dict:
- return {
- self.name: self.value
- }
+ return {self.name: self.value}
class CellStyleProperty(Property):
@@ -314,9 +487,7 @@ def available_tags(self) -> FrozenSet[str]:
@property
def as_dict(self) -> Dict:
- result = {
- self.name: self.value
- }
+ result = {self.name: self.value}
for suffix, value in self.cell_style._dict_suffixes.items():
result[self.name + suffix] = value
@@ -325,18 +496,56 @@ def as_dict(self) -> Dict:
class Html(Property):
- def __init__(self, name: str, value: str):
+ def __init__(
+ self, name: str,
+ value: str,
+ custom_table_style: str,
+ unordered_list_style: Union[str, int],
+ ordered_list_style: Union[str, int],
+ use_tag_style: bool,
+ ignore_cell_margin: bool,
+ ignore_empty_p: bool
+ ):
"""
Args:
name (str): The name for this property.
value (str): The value for this property.
+ custom_table_style (str): Specify custom table style
+ unordered_list_style (str): Create and customize ordered list
+ ordered_list_style (str): Create and customize unordered list
+ use_tag_style (bool): Use the styling from the template instead of default Word styling
+ ignore_cell_margin (bool): Ignore empty paragraphs within HTML content
+ ignore_empty_p (bool): Ignore the cell margins in an HTML table cell when the text content is large
"""
super().__init__(name, value)
-
+ self.custom_table_style: str = custom_table_style
+ self.unordered_list_style: Union[str, int] = unordered_list_style
+ self.ordered_list_style: Union[str, int] = ordered_list_style
+ self.use_tag_style: bool = use_tag_style
+ self.ignore_cell_margin: bool = ignore_cell_margin
+ self.ignore_empty_p: bool = ignore_empty_p
+
@property
def available_tags(self) -> FrozenSet[str]:
return frozenset({"{_" + self.name + "}"})
-
+
+ @property
+ def as_dict(self) -> Dict:
+ result = {self.name: self.value}
+
+ if self.custom_table_style is not None:
+ result[self.name + "_custom_table_style"] = self.custom_table_style
+ if self.unordered_list_style is not None:
+ result[self.name + "_unordered_list_style"] = self.unordered_list_style
+ if self.ordered_list_style is not None:
+ result[self.name + "_ordered_list_style"] = self.ordered_list_style
+ if self.use_tag_style is not None:
+ result[self.name + "_use_tag_style"] = self.use_tag_style
+ if self.ignore_cell_margin is not None:
+ result[self.name + "_ignore_cell_margin"] = self.ignore_cell_margin
+ if self.ignore_empty_p is not None:
+ result[self.name + "_ignore_empty_p"] = self.ignore_empty_p
+ return result
class RightToLeft(Property):
def __init__(self, name: str, value: str):
@@ -367,20 +576,47 @@ def available_tags(self) -> FrozenSet[str]:
class AutoLink(Property):
- """ This tag allows you to insert text into the document detecting links.
+ """ This tag allows you to insert text into the document detecting links.
+ For PPTX templates, additional styling options are available including:
+ - Custom font color for hyperlinks
+ - Custom underline color
+ - Option to preserve template styling
"""
- def __init__(self, name: str, value: str):
+
+ def __init__(self,
+ name: str,
+ value: str,
+ font_color: Union[str, None] = None,
+ underline_color: Union[str, None] = None,
+ preserve_tag_style: Union[bool, str, None] = None):
"""
Args:
name (str): The name for this element.
value (str): The value of the autoLink.
+ font_color (str, optional): PPTX-only The font color of autolink.
+ underline_color (str, optional): The underline color of autolink.
+ preserve_tag_style (str or bool, optional): Take the styling of hyperlink text defined in the template (blue and underlined by default).
"""
- super().__init__(name,value)
+ super().__init__(name, value)
+ self.value: str = value
+ self.font_color = font_color
+ self.underline_color = underline_color
+ self.preserve_tag_style = preserve_tag_style
@property
def available_tags(self) -> FrozenSet[str]:
return frozenset({"{*auto " + self.name + "}"})
-
+ @property
+ def as_dict(self) -> Dict :
+ result ={self.name:self.value}
+ if self.font_color is not None:
+ result[f"{self.name}_font_color"] = self.font_color
+ if self.underline_color is not None:
+ result[f"{self.name}_underline_color"] = self.underline_color
+ if self.preserve_tag_style is not None:
+ result[f"{self.name}_preserve_tag_style"] = self.preserve_tag_style
+ return result
+
class Hyperlink(Element):
def __init__(self, name: str, url: str, text: str = None):
"""
@@ -388,10 +624,16 @@ def __init__(self, name: str, url: str, text: str = None):
name (str): The name for this element.
url (str): The URL for the hyperlink.
text (str, optional): The text for the hyperlink. Defaults to None.
+ font_color (str, optional): PPTX ONLY -The font color of text for hyperlink. ( optional)
+ underline_color (str, optional): PPTX ONLY -The underline color of text for hyperlink. ( optional)
+ preserve_tag_style (str or bool, optional): For PPTX & Word- Take the styling of hyperlink text defined in the template (blue and underlined by default). ( optional)
"""
super().__init__(name)
self.url: str = url
- self.text: str = text
+ self.text: Union[str, None] = None
+ self.font_color: Union[str, None] = None
+ self.underline_color: Union[str, None] = None
+ self.preserve_tag_style: Union[str, bool, None] = None
@property
def available_tags(self) -> FrozenSet[str]:
@@ -399,18 +641,22 @@ def available_tags(self) -> FrozenSet[str]:
@property
def as_dict(self) -> Dict:
- result = {
- self.name: self.url
- }
+ result = {self.name: self.url}
if self.text is not None:
result[self.name + "_text"] = self.text
+ if (self.text is not None) and (self.font_color is not None):
+ result[self.text + "_font_color"] = self.font_color
+ if (self.text is not None) and (self.underline_color is not None):
+ result[self.text + "_underline_color"] = self.underline_color
+ if (self.preserve_tag_style is not None):
+ result[self.name + "_preserve_tag_style"] = self.preserve_tag_style
return result
-
-
class TableOfContents(Element):
- def __init__(self, name: str, title: str = None, depth: int = None, tab_leader: str = None):
+ def __init__(
+ self, name: str, title: str = None, depth: int = None, tab_leader: str = None
+ ):
"""
Args:
name (str): The name for this element.
@@ -477,7 +723,7 @@ def as_dict(self) -> Dict:
return {
self.name: self.value,
self.name + "_row_span": self.rows,
- self.name + "_col_span": self.columns
+ self.name + "_col_span": self.columns,
}
@@ -496,17 +742,19 @@ def available_tags(self) -> FrozenSet[str]:
class StyledProperty(Property):
- def __init__(self,
- name: str,
- value: str,
- font: str = None,
- font_size: Union[str, int] = None,
- font_color: str = None,
- bold: bool = None,
- italic: bool = None,
- underline: bool = None,
- strikethrough: bool = None,
- highlight_color: str = None):
+ def __init__(
+ self,
+ name: str,
+ value: str,
+ font: str = None,
+ font_size: Union[str, int] = None,
+ font_color: str = None,
+ bold: bool = None,
+ italic: bool = None,
+ underline: bool = None,
+ strikethrough: bool = None,
+ highlight_color: str = None,
+ ):
"""
Args:
name (str): The name for this property.
@@ -536,9 +784,7 @@ def available_tags(self) -> FrozenSet[str]:
@property
def as_dict(self) -> Dict:
- result = {
- self.name: self.value
- }
+ result = {self.name: self.value}
if self.font is not None:
result[self.name + "_font_family"] = self.font
@@ -561,15 +807,17 @@ def as_dict(self) -> Dict:
class Watermark(Property):
- def __init__(self,
- name: str,
- text: str,
- color: str = None,
- font: str = None,
- width: Union[int, str] = None,
- height: Union[int, str] = None,
- opacity: float = None,
- rotation: int = None):
+ def __init__(
+ self,
+ name: str,
+ text: str,
+ color: str = None,
+ font: str = None,
+ width: Union[int, str] = None,
+ height: Union[int, str] = None,
+ opacity: float = None,
+ rotation: int = None,
+ ):
"""
Args:
name (str): The name for this property.
@@ -595,9 +843,7 @@ def available_tags(self) -> FrozenSet[str]:
@property
def as_dict(self) -> Dict:
- result = {
- self.name: self.value
- }
+ result = {self.name: self.value}
if self.color is not None:
result[self.name + "_color"] = self.color
@@ -633,12 +879,10 @@ def available_tags(self) -> FrozenSet[str]:
@property
def as_dict(self) -> Dict:
- result = {
- self.name: self.code
- }
+ result = {self.name: self.code}
if self.data is not None:
- result[self.name + '_data'] = self.data
+ result[self.name + "_data"] = self.data
return result
@@ -646,15 +890,14 @@ def as_dict(self) -> Dict:
class COPChartDateOptions:
"""Date options for an COPChart (different from ChartDateOptions in charts.py)."""
- def __init__(self,
- format: str = None,
- unit: str = None,
- step: Union[int, str] = None):
+ def __init__(
+ self, format: str = None, unit: str = None, step: Union[int, str] = None
+ ):
"""
Args:
format (str, optional): The format to display the date on the chart's axis. Defaults to None.
unit (str, optional): The unit to be used for spacing the axis values. Defaults to None.
- step (Union[int, str], optional): How many of the above unit should be used for spacing the axis values (automatic if undefined).
+ step (Union[int, str], optional): How many of the above unit should be used for spacing the axis values (automatic if undefined).
This option is not supported in LibreOffice. Defaults to None.
"""
self.format: str = format
@@ -678,16 +921,21 @@ def as_dict(self) -> Dict:
class COPChart(Element):
"""The class for an COPChart. This is used for chart templating."""
- def __init__(self,
- name: str,
- x_data: Iterable[Union[str, int, float, Mapping]],
- y_datas: Union[Iterable[Iterable[Union[str, int, float, Mapping]]], Mapping[str, Iterable[Union[str, int, float, Mapping]]]],
- date: COPChartDateOptions = None,
- title: str = None,
- x_title: str = None,
- y_title: str = None,
- y2_title: str = None,
- x2_title: str = None):
+ def __init__(
+ self,
+ name: str,
+ x_data: Iterable[Union[str, int, float, Mapping]],
+ y_datas: Union[
+ Iterable[Iterable[Union[str, int, float, Mapping]]],
+ Mapping[str, Iterable[Union[str, int, float, Mapping]]],
+ ],
+ date: COPChartDateOptions = None,
+ title: str = None,
+ x_title: str = None,
+ y_title: str = None,
+ y2_title: str = None,
+ x2_title: str = None,
+ ):
"""
Args:
name (str): The name for this element.
@@ -711,16 +959,15 @@ def __init__(self,
self.y_datas: Dict[str, Iterable[Union[str, int, float]]] = None
"""If the argument 'y_datas' is of type Iterable[Iterable], then default names (e.g. series 1, series 2, ...) will be used."""
if isinstance(y_datas, Mapping):
- self.y_datas = {
- name: list(data) for name, data in y_datas.items()
- }
+ self.y_datas = {name: list(data) for name, data in y_datas.items()}
elif isinstance(y_datas, Iterable):
self.y_datas = {
f"series {i+1}": list(data) for i, data in enumerate(y_datas)
}
else:
raise TypeError(
- f'Expected Mapping or Iterable for y_data, got "{type(y_datas)}"')
+ f'Expected Mapping or Iterable for y_data, got "{type(y_datas)}"'
+ )
self.date: COPChartDateOptions = date
self.title: str = title
@@ -730,15 +977,17 @@ def __init__(self,
self.y2_title: str = y2_title
@classmethod
- def from_dataframe(cls,
- name: str,
- data: 'pandas.DataFrame',
- date: COPChartDateOptions = None,
- title: str = None,
- x_title: str = None,
- y_title: str = None,
- y2_title: str = None,
- x2_title: str = None) -> 'COPChart':
+ def from_dataframe(
+ cls,
+ name: str,
+ data: "pandas.DataFrame",
+ date: COPChartDateOptions = None,
+ title: str = None,
+ x_title: str = None,
+ y_title: str = None,
+ y2_title: str = None,
+ x2_title: str = None,
+ ) -> "COPChart":
"""Construct an COPChart object from a [Pandas dataframe](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html).
Args:
@@ -761,7 +1010,9 @@ def from_dataframe(cls,
for col_name, col_data in y_frame.iteritems():
y_datas[col_name] = col_data
- return cls(name, x_data, y_datas, date, title, x_title, y_title, y2_title, x2_title)
+ return cls(
+ name, x_data, y_datas, date, title, x_title, y_title, y2_title, x2_title
+ )
@property
def as_dict(self) -> Dict:
@@ -770,26 +1021,25 @@ def as_dict(self) -> Dict:
"data": self.x_data,
},
"yAxis": {
- "series": [{
- "name": name,
- "data": data
- } for name, data in self.y_datas.items()]
- }
+ "series": [
+ {"name": name, "data": data} for name, data in self.y_datas.items()
+ ]
+ },
}
if self.title is not None:
result["title"] = self.title
if self.date is not None:
- result['xAxis']['date'] = self.date.as_dict
+ result["xAxis"]["date"] = self.date.as_dict
if self.x_title is not None:
result["xAxis"]["title"] = self.x_title
if self.y_title is not None:
result["yAxis"]["title"] = self.y_title
if self.x2_title is not None:
- result['x2Axis'] = {}
+ result["x2Axis"] = {}
result["x2Axis"]["title"] = self.x2_title
if self.y2_title is not None:
- result['y2Axis'] = {}
+ result["y2Axis"] = {}
result["y2Axis"]["title"] = self.y2_title
return {self.name: result}
@@ -835,15 +1085,17 @@ def available_tags(self) -> FrozenSet[str]:
class TextBox(Element):
"""This tag will allow you to insert a text box starting in the cell containing the tag in Excel."""
- def __init__(self,
- name: str,
- value: str,
- font: str = None,
- font_color: str = None,
- font_size: Union[int, str] = None,
- transparency: Union[int, str] = None,
- width: Union[int, str] = None,
- height: Union[int, str] = None):
+ def __init__(
+ self,
+ name: str,
+ value: str,
+ font: str = None,
+ font_color: str = None,
+ font_size: Union[int, str] = None,
+ transparency: Union[int, str] = None,
+ width: Union[int, str] = None,
+ height: Union[int, str] = None,
+ ):
"""
Args:
name (str): The name for this element.
@@ -870,22 +1122,20 @@ def available_tags(self) -> FrozenSet[str]:
@property
def as_dict(self) -> Dict:
- result = {
- self.name: self.value
- }
+ result = {self.name: self.value}
if self.font is not None:
- result[self.name + '_font'] = self.font
+ result[self.name + "_font"] = self.font
if self.font_color is not None:
- result[self.name + '_font_color'] = self.font_color
+ result[self.name + "_font_color"] = self.font_color
if self.font_size is not None:
- result[self.name + '_font_size'] = self.font_size
+ result[self.name + "_font_size"] = self.font_size
if self.transparency is not None:
- result[self.name + '_transparency'] = self.transparency
+ result[self.name + "_transparency"] = self.transparency
if self.width is not None:
- result[self.name + '_width'] = self.width
+ result[self.name + "_width"] = self.width
if self.height is not None:
- result[self.name + '_height'] = self.height
+ result[self.name + "_height"] = self.height
return result
@@ -910,21 +1160,473 @@ def available_tags(self) -> FrozenSet[str]:
class Insert(Property):
- """Inside Word and PowerPoint documents, the tag {?insert fileToInsert} can be used to insert files like Word, Excel, Powerpoint and PDF documents."""
+ """Inside Word and PowerPoint and Excel documents, the tag {?insert fileToInsert} can be used to insert files like Word, Excel, Powerpoint and PDF documents.
+ Please use `ExcelInsert` element to insert in excel with more flexibility.
+ """
+
+ def __init__(self, name: str, value: str):
+ """
+ Args:
+ name (str): The name for the insert tag.
+ value (str): Base64 encoded document that needs to be inserted in output docx or pptx.
+ The document can be docx, pptx, xlsx, or pdf documents.
+ """
+ super().__init__(name, value)
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset({"{?insert " + self.name + "}"})
+
+class PdfInclude(Element):
+ """Inside Word and PowerPoint and Excel documents, the tag {?include pdf } can be used to include files like Word, Excel, Powerpoint and PDF documents.
+ Please use `ExcelInsert` element to insert in excel with more flexibility.
+ """
+ def __init__(self, name: str , value: str, filename: str, mime_type: str, file_content: str, file_source: str):
+ """
+ Args:
+ name (str): The tag name referenced in templates
+ filename (str): Name of the file to include
+ mime_type (str): MIME type of the content (e.g., 'image/png')
+ file_content (str): Base64 encoded content of the file
+ file_source (str): Source type ('base64', 'local', etc.)
+ """
+ super().__init__(name)
+ self.value = value
+ self.filename = filename
+ self.mime_type = mime_type
+ self.file_content = file_content
+ self.file_source = file_source
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset({"{?pdfinclude " + self.name + "}"})
+
+
+ @property
+ def as_dict(self) -> Dict:
+ result = {}
+ result [self.name] = {}
+
+ if self.filename is not None:
+ result [self.name]["name"] = self.filename
+ if self.mime_type is not None:
+ result [self.name]["mime_type"] = self.mime_type
+ if self.file_content is not None:
+ result [self.name]["file_content"] = self.file_content
+ if self.file_source is not None:
+ result [self.name]["file_source"] = self.file_source
+
+ return result
+
+
+class PptxShapeRemove(Property):
+ """Allows the removal of an entire shape / text-box if the associated tag evaluates to false. For example, if a template slide includes a text box with the tag {toShow?} and
+ the value of toShow is false or undefined, the entire shape will be removed from the slide.
+ """
+ def __init__(self, name: str, value: Union[bool, str]):
+ """
+ Args:
+ name (str): The name for the remove tag.
+ value (bool or string): False (to remove the shape / text-box) or string/True
+ The document should be, pptx.
+ """
+ super().__init__(name, value)
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset({"{ " + self.name + "?" + "}"})
+
+
+class HideSlide(Property):
+ """Allows hiding a slide.
+ LIMITATION: using _hide for slide hide is that it can only be used for hiding slides during generation(!slideGeneration)
+ """
+ def __init__(self, name: str, condition : str ):
+ """
+ Args:
+ name (str): Name/identifier for this hide condition
+ condition (str): The condition to determine when to hide the slide
+ The document should be, pptx.
+ """
+ super().__init__(name, condition)
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset({"{hide " + self.name + "}"})
+
+class HideSheets(Property):
+ """Allows hiding a sheet in Excel documents.
+ Can be used to hide sheets based on conditions during generation.
+ The hide tag {hide condition} is replaced by nothing but will hide the sheet if the condition evaluates to true.
+ The condition can use any Angular expressions that are supported.
+
+ """
+ def __init__(self, name: str, condition: str):
+ """
+ Args:
+ name (str): Name/identifier for this hide condition
+ condition (str): Angular expression that determines when to hide the sheet.
+ The document must be an Excel workbook (.xlsx)
+ """
+ super().__init__(name, condition)
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset({"{hide " + self.name + "}"})
+
+
+class ExcelInsert(Element):
+ """Inside Excel it is posiible to insert word, powerpoint, excel and pdf file using AOP tag {?insert fileToInsert}.
+ Options available are: you can provide dynamic icon and icon position.
"""
- Args:
- name (str): The name for the insert tag.
- value (str): Base64 encoded document that needs to be inserted in output docx or pptx.
- The documnet can be docx, pptx, xlsx, or pdf documents.
+
+ def __init__(self,
+ name: str,
+ value: str,
+ # isPreview: bool = None,
+ icon: str = None,
+ fromRow: int = None,
+ fromCol: Union[str, int] = None,
+ fromRowOff: str = None,
+ fromColOff: str = None,
+ toRow: int = None,
+ toCol: Union[str, int] = None,
+ toRowOff: str = None,
+ toColOff: str = None
+ ):
+ """It is possible to provide dynamic icon and position of icon.
+
+ Args:
+ name (str): Name of insert tag. Ex(fileToInsert)
+ value (str): File to insert of path to file. (Source can be FTP, SFTP, URL or base64encoded file.)
+ icon (str, optional): Icon that links the file to insert. Once clicked on it, opens the file inserted. If it is not provide default icon is used.
+ fromRow (int, optional): position for top of icon. Defaults to row of the tag.
+ fromCol (Union[str,int], optional): positon for left of icon. Defaults to column of the tag.
+ fromRowOff (str, optional): space after the value of from Row. Defaults to 0.
+ fromColOff (str, optional): space after the value of fromCol. Defaults to 0.
+ toRow (int, optional): position for bottom of icon. Defaults to row of the tag + 3.
+ toCol (Union[str,int], optional): position for right side of icon. Defaults to column of the tag.
+ toRowOff (str, optional): space after toRow value. Defaults to 20px.
+ toColOff (str, optional): space after toCol value. Defaults to 50px.
+ """
+ super().__init__(name)
+ self.value: str = value
+ # self.isPreview: bool = isPreview
+ self.icon: str = icon
+ self.fromRow: int = fromRow
+ self.fromCol: Union[str, int] = fromCol
+ self.fromRowOff: str = fromRowOff
+ self.fromColOff: str = fromColOff
+ self.toRow: int = toRow
+ self.toCol: Union[str, int] = toCol
+ self.toRowOff: str = toRowOff
+ self.toColOff: str = toColOff
+
+ @property
+ def as_dict(self) -> Dict:
+ result = {
+ self.name: self.value
+ }
+ # if self.isPreview is not None:
+ # result[self.name+'_isPreview'] = self.isPreview
+ if self.icon is not None:
+ result[self.name+'_icon'] = self.icon
+ if self.fromRow is not None:
+ result[self.name+'_fromRow'] = self.fromRow
+ if self.fromCol is not None:
+ result[self.name+'_fromCol'] = self.fromCol
+ if self.fromRowOff is not None:
+ result[self.name+'_fromRowOff'] = self.fromRowOff
+ if self.fromColOff is not None:
+ result[self.name+'_fromColOff'] = self.fromColOff
+ if self.toRow is not None:
+ result[self.name+'_toRow'] = self.toRow
+ if self.toCol is not None:
+ result[self.name+'_toCol'] = self.toCol
+ if self.toRowOff is not None:
+ result[self.name+'_toRowOff'] = self.toRowOff
+ if self.toColOff is not None:
+ result[self.name+'_toColOff'] = self.toColOff
+
+ return result
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset({"{?insert fileToInsert}"})
+
+
+class Embed(Property):
+ """Inside Word, it is possible to copy the content of one docx file to the template without rendering.
+
+ To do so, you can use AOP embed tag as {?embed fileToEmbed} where fileToEmbed contains the path of file or file itself.
+
+ The content of fileToEmbed replaces the tag
+
+ Only supported in Word and only supports docx file to embed.
"""
def __init__(self, name: str, value: str):
+ """It takes the tagName and its value as parameter.
+
+ Args:
+ name (str): Name of the tag (ex. fileToEmbed)
+ value (str): File to embed. Source can be FTP, SFTP, URL or base64 encoded file. (ex. base64encoded string)
+ """
super().__init__(name, value)
@property
def available_tags(self) -> FrozenSet[str]:
- return frozenset({"{?insert " + self.name + "}"})
+ return frozenset({"{?embed fileToEmbed}"})
+
+
+class SheetProtection(Element):
+ """Inside Excel documents, this tag can be used to make password protected sheets. This tag has the feature of password along with different other features.
+
+ Note: value is considered password, so try to use only one (either value or passowrd).
+ """
+
+ def __init__(self,
+ name: str,
+ value: str = None,
+ autoFilter: str = None,
+ deleteColumns: bool = None,
+ deleteRows: bool = None,
+ formatCells: bool = None,
+ formatColumns: bool = None,
+ formatRows: bool = None,
+ insertColumns: bool = None,
+ insertHyperlinks: bool = None,
+ insertRows: bool = None,
+ password: str = None,
+ pivotTables: bool = None,
+ selectLockedCells: bool = None,
+ selectUnlockedCells: bool = None,
+ sort: bool = None,
+ ):
+ """
+ Args:
+ name (str): The name for the sheet protection tag.
+ value (str): Value for the tag; this is used as password
+ autoFilter (str): lock auto filter in sheet.
+ deleteColumns (bool): lock delete columns in sheet.
+ deleteRows (bool): lock delete rows in sheet.
+ formatCells (bool): lock format cells.
+ formatColumns (bool): lock format columns.
+ formatRows (bool): lock format rows.
+ insertColumns (bool): lock insert columns.
+ insertHyperlinks (bool): lock insert hyperlinks.
+ insertRows (bool): lock insert rows.
+ password (str): password to lock with.
+ pivotTables (bool): lock pivot tables.
+ selectLockedCells (bool): lock select locked cells.
+ selectUnlockedCells (bool): lock select unlocked cells.
+ sort (bool): lock sort.
+ """
+ super().__init__(name)
+ self.value = value
+ self.autoFilter = autoFilter
+ self.deleteColumns = deleteColumns
+ self.deleteRows = deleteRows
+ self.formatCells = formatCells
+ self.formatColumns = formatColumns
+ self.formatRows = formatRows
+ self.insertColumns = insertColumns
+ self.insertHyperlinks = insertHyperlinks
+ self.insertRows = insertRows
+ self.password = password
+ self.pivotTables = pivotTables
+ self.selectLockedCells = selectLockedCells
+ self.selectUnlockedCells = selectUnlockedCells
+ self.sort = sort
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset({"{protect " + self.name + "}"})
+
+ @property
+ def as_dict(self) -> Dict:
+ result = {}
+ if self.value is not None:
+ result[self.name] = self.value
+ if self.autoFilter is not None:
+ result[self.name+'_allow_auto_filter'] = self.autoFilter
+ if self.deleteColumns is not None:
+ result[self.name+'_allow_delete_columns'] = self.deleteColumns
+ if self.deleteRows is not None:
+ result[self.name+'_allow_delete_rows'] = self.deleteRows
+ if self.formatCells is not None:
+ result[self.name+'_allow_format_cells'] = self.formatCells
+ if self.formatColumns is not None:
+ result[self.name+'_allow_format_columns'] = self.formatColumns
+ if self.formatRows is not None:
+ result[self.name+'_allow_format_rows'] = self.formatRows
+ if self.insertColumns is not None:
+ result[self.name+'_allow_insert_columns'] = self.insertColumns
+ if self.insertHyperlinks is not None:
+ result[self.name+'_allow_insert_hyperlinks'] = self.insertHyperlinks
+ if self.insertRows is not None:
+ result[self.name+'_allow_insert_rows'] = self.insertRows
+ if self.password is not None:
+ result[self.name+'_password'] = self.password
+ if self.pivotTables is not None:
+ result[self.name+'_allow_pivot_tables'] = self.pivotTables
+ if self.selectLockedCells is not None:
+ result[self.name+'_allow_select_locked_cells'] = self.selectLockedCells
+ if self.selectUnlockedCells is not None:
+ result[self.name+'_allow_select_unlocked_cells'] = self.selectUnlockedCells
+ if self.sort is not None:
+ result[self.name+'_allow_sort'] = self.sort
+ return result
+
+
+class ValidateCell(Element):
+ """
+ It is possible to insert cell validation in excel using validate tag as {validate validateTag} (validate keyword followed by tagName)
+ """
+
+ def __init__(self,
+ name: str,
+ ignoreBlank: bool = None,
+ allow: str = None,
+ value1: str = None,
+ value2: str = None,
+ inCellDropdown: bool = None,
+ data: str = None,
+ showInputMessage: bool = None,
+ inputTitle: str = None,
+ inputMessage: str = None,
+ showErrorAlert: bool = None,
+ errorStyle: str = None,
+ errorTitle: str = None,
+ errorMessage: str = None,
+ ):
+ """Available option while using validate cell are ( ignoreBlank, allow, value1, value2, inCellDropdown, data, showInputMessage, inputTitle, inputMessage, showErrorAlert, errorStyle, errorTitle,errorMessage )
+
+ Args:
+ name (string, optional): Name of the validate tag. For {validate tagName}, tagName is name for this element.
+ ignoreBlank (bool, optional): Set it to false for not allowing empty values in cell. The value is true by default.
+ allow (string, optional): Type of data used for validation. Available options are (anyValue, whole, decimal, list, date, time, textLength, custom). Please use camelCase to insert value for allow attribute.
+ value1 (string, optional): Value to compare with.
+ value2 (string, optional): Value to compare with.
+ Note:
+ These two options (_value1, _value2) can be used for any allow/type of validation that require values for comparison, in such case use "_value1" attribute as the first value to be passed and "_value2" attribute as the 2nd value.
+ Some allow type of validation require only one value to compare; in such case use "_value1" attribute.
+ For ex :
+ If allow type of validation is date and you have to check in between two dates.
+ Then you could use "_value1" attribute as start date and "_value2" attribute as end date.
+ If allow type of validation is whole and you have to check for value less than 100.
+ Then you could use "_value1" for that value and do not use "_value2".
+ While using time and date as allow type validation, please provide date/time with correct formatting.
+ for time: hours:minutes:seconds i.e hours , minutes, seconds separated by colon (:)
+ ex : 14:30:00 for 2:30 pm
+ for date: month/day/year i.e day, month , year separated by forward slash(/)
+ ex : 02/07/2023 for Feb 7 2023.
+ for list: you could use normal string with elements separated by comma(,).
+ ex : "first, second, third" for list of three elements.
+ inCellDropdown (bool, optional): Set it to false for not showing dropdown button while validation allow type is list. It is true by default for list allow type.
+ data (string, optional): Type of comparison to be done for the cell value. Available values are (lessThanOrEqual, notBetween, equal, notEqual, greaterThan, greaterThan, lessThan, greaterThanOrEqual, lessThanOrEqual). Default value is "between". Please use camelCase for the value as shown in examples.
+ showInputMessage (bool, optional): Set it to false to hide message shown when the cell to validate is being selected. The value for it is true by default.
+ inputTitle (string, optional): Title of message to be shown when cell to validate is selected.
+ inputMessage (string, optional): Message to be shown when cell to validate is selected.
+ showErrorAlert (bool, optional): Set it to false, if you want to hide error alert once cell validation fails. The value is true by default.
+ errorStyle (string, optional): Type of error style when cell validation fails. The value is stop by default. Available options are(stop,waring, Information).
+ errorTitle (string, optional): Title of error to be shown when cell validation fails.
+ errorMessage (string, optional): Message of error to be shown when cell validation fails.
+ """
+ super().__init__(name)
+ self.ignoreBlank = ignoreBlank
+ self.allow = allow
+ self.value1 = value1
+ self.value2 = value2
+ self.inCellDropdown = inCellDropdown
+ self.data = data
+ self.showInputMessage = showInputMessage
+ self.inputTitle = inputTitle
+ self.inputMessage = inputMessage
+ self.showErrorAlert = showErrorAlert
+ self.errorStyle = errorStyle
+ self.errorTitle = errorTitle
+ self.errorMessage = errorMessage
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset({"{validate " + self.name + "}"})
+
+ @property
+ def as_dict(self) -> Dict:
+ result = {}
+ if self.ignoreBlank is not None:
+ result[self.name + '_ignore_blank'] = self.ignoreBlank
+ if self.allow is not None:
+ result[self.name + '_allow'] = self.allow
+ if self.value1 is not None:
+ result[self.name + '_value1'] = self.value1
+ if self.value2 is not None:
+ result[self.name + '_value2'] = self.value2
+ if self.inCellDropdown is not None:
+ result[self.name + '_in_cell_dropdown'] = self.inCellDropdown
+ if self.data is not None:
+ result[self.name + '_data'] = self.data
+ if self.showInputMessage is not None:
+ result[self.name + '_show_input_message'] = self.showInputMessage
+ if self.inputTitle is not None:
+ result[self.name + '_input_title'] = self.inputTitle
+ if self.inputMessage is not None:
+ result[self.name + '_input_message'] = self.inputMessage
+ if self.showErrorAlert is not None:
+ result[self.name + '_show_error_alert'] = self.showErrorAlert
+ if self.errorStyle is not None:
+ result[self.name + '_error_style'] = self.errorStyle
+ if self.errorTitle is not None:
+ result[self.name + '_error_title'] = self.errorTitle
+ if self.errorMessage is not None:
+ result[self.name + '_error_message'] = self.errorMessage
+ return result
+
+
+class Link(Property):
+ """The class for the link/target tags.
+ This tags allows you to place a link to a target in the same document.
+ If the uid is not provided, a new uid will be generated uniquely for every link and target pair.
+ """
+
+ def __init__(
+ self,
+ name: str,
+ value: str,
+ uid_name: str = None,
+ uid_value: str = None,
+ ):
+ """Create a new link/target tag pair.
+ If the uid is not provided, a new uid will be generated uniquely for each link/target pair.
+
+ Args:
+ name (str): the name of the link/target tags.
+ value (str): the value of the link/target tags.
+ uid_name (str): the name of the uid of the link/target pair.
+ uid_value (str): the value of the uid of the link/target pair.
+ """
+ super().__init__(name, value)
+ self.uid_name = uid_name
+ self.uid_value = uid_value
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ if self.uid_name and self.uid_value:
+ return frozenset(
+ {
+ "{link" + self.name + ":" + self.uid_name + "}",
+ "{target" + self.name + ":" + self.uid_name + "}",
+ }
+ )
+ return frozenset({"{link" + self.name + "}", "{target" + self.name + "}"})
+
+ @property
+ def as_dict(self) -> Dict:
+ if self.uid_name and self.uid_value:
+ return {self.name: self.value, self.uid_name: self.uid_value}
+ return {self.name: self.value}
class ElementCollection(list, Element):
@@ -956,14 +1658,14 @@ def __repr__(self) -> str:
"""
return self.json
- def copy(self) -> 'ElementCollection':
+ def copy(self) -> "ElementCollection":
"""
Returns:
ElementCollection: A copy of this element collection.
"""
return self.__class__(self)
- def deepcopy(self) -> 'ElementCollection':
+ def deepcopy(self) -> "ElementCollection":
"""
Returns:
ElementCollection: A deep copy of this element collection.
@@ -982,7 +1684,7 @@ def add(self, element: Element):
"""
self.append(element)
- def add_all(self, obj: 'ElementCollection'):
+ def add_all(self, obj: "ElementCollection"):
"""Add all the elements in the given collection to this collection.
Args:
@@ -998,8 +1700,7 @@ def remove_element_by_name(self, element_name: str):
element_name (str): the name of the element that needs to be removed
"""
self.remove(
- next(element for element in self if element.name == element_name)
- )
+ next(element for element in self if element.name == element_name))
@property
def as_dict(self) -> Dict:
@@ -1024,7 +1725,9 @@ def available_tags(self) -> FrozenSet[str]:
return frozenset(result)
@classmethod
- def element_to_element_collection(cls, element: Element, name: str = "") -> 'ElementCollection':
+ def element_to_element_collection(
+ cls, element: Element, name: str = ""
+ ) -> "ElementCollection":
"""Generate an element collection from an element and a name.
Args:
@@ -1037,7 +1740,7 @@ def element_to_element_collection(cls, element: Element, name: str = "") -> 'Ele
return cls.from_mapping(element.as_dict, name)
@classmethod
- def from_mapping(cls, mapping: Mapping, name: str = "") -> 'ElementCollection':
+ def from_mapping(cls, mapping: Mapping, name: str = "") -> "ElementCollection":
"""Generate an element collection from a mapping and a name.
Args:
@@ -1053,7 +1756,7 @@ def from_mapping(cls, mapping: Mapping, name: str = "") -> 'ElementCollection':
return cls(name, result_set)
@classmethod
- def from_json(cls, json_str: str, name: str = "") -> 'ElementCollection':
+ def from_json(cls, json_str: str, name: str = "") -> "ElementCollection":
"""Generate an element collection from a JSON string.
Args:
diff --git a/cloudofficeprint/elements/form.py b/cloudofficeprint/elements/form.py
new file mode 100644
index 0000000..7578482
--- /dev/null
+++ b/cloudofficeprint/elements/form.py
@@ -0,0 +1,125 @@
+from typing import Dict, Union,Any, FrozenSet
+from .elements import Element
+
+class Textbox(Element):
+ """PDF form textbox element
+
+ Args:
+ name (str): the PDF field name
+ value (str, optional): text to put into the box
+ height (int|str, optional): box height
+ width (int|str, optional): box width
+ multiline (bool, optional): True for multiline input (serialized as 1/0)
+ """
+ def __init__(self,
+ name: str,
+ value: str = None,
+ height: Union[int, str] = None,
+ width: Union[int, str] = None,
+ multiline: bool = None):
+ super().__init__(name)
+ self.type = "text"
+ self.value = value
+ self.height = height
+ self.width = width
+ self.multiline = multiline
+ @property
+ def as_dict(self) -> Dict[str, Any]:
+ result = {"type": self.type, "name": self.name}
+ if self.value is not None:
+ result["value"] = self.value
+ if self.height is not None:
+ result["height"] = self.height
+ if self.width is not None:
+ result["width"] = self.width
+ if self.multiline is not None:
+ result["multiline"] = self.multiline
+ return result
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset([f"{{?form {self.name}}}"])
+
+
+class RadioButton(Element):
+ """PDF form radio button element
+
+ Args:
+ name (str): the PDF field name
+ value (str, optional): the field’s export values
+ text (str, optional): visible caption next to the button
+ selected (bool, optional): True if this button is chosen (serialized as 1/0)
+ height (int|str, optional): control height
+ width (int|str, optional): control width
+ """
+ def __init__(self,
+ name: str,
+ value: str = None,
+ text: str = None,
+ selected: bool = None,
+ height: Union[int, str] = None,
+ width: Union[int, str] = None):
+ super().__init__(name)
+ self.type = "radio"
+ self.value = value
+ self.text = text
+ self.selected = selected
+ self.height = height
+ self.width = width
+
+ @property
+ def as_dict(self) -> Dict[str, Any]:
+ result = {"type": self.type, "name": self.name}
+ if self.value is not None:
+ result["value"] = self.value
+ if self.text is not None:
+ result["text"] = self.text
+ if self.selected is not None:
+ result["selected"] = 1 if self.selected else 0
+ if self.height is not None:
+ result["height"] = self.height
+ if self.width is not None:
+ result["width"] = self.width
+ return result
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset([f"{{?form {self.name}}}"])
+
+class Checkbox(Element):
+ """PDF form checkbox element
+
+ Args:
+ name (str): the PDF field name
+ value (bool, optional): True if checked (serialized as 1/0)
+ text (str, optional): visible caption next to the box
+ height (int|str, optional): control height
+ width (int|str, optional): control width
+ """
+
+ def __init__(self, name: str, value: bool = None,
+ text: str = None,
+ height: Union[int, str] = None,
+ width: Union[int, str] = None):
+ super().__init__(name)
+ self.type = "checkbox"
+ self.value = value
+ self.text = text
+ self.height = height
+ self.width = width
+
+ @property
+ def as_dict(self) -> Dict[str, Any]:
+ result = {"type": self.type, "name": self.name}
+ if self.value is not None:
+ result["value"] = 1 if self.value else 0
+ if self.text is not None:
+ result["text"] = self.text
+ if self.height is not None:
+ result["height"] = self.height
+ if self.width is not None:
+ result["width"] = self.width
+ return result
+
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset([f"{{?form {self.name}}}"])
\ No newline at end of file
diff --git a/cloudofficeprint/elements/images.py b/cloudofficeprint/elements/images.py
index 83ebebb..b56585a 100644
--- a/cloudofficeprint/elements/images.py
+++ b/cloudofficeprint/elements/images.py
@@ -16,7 +16,9 @@ def __init__(self,
transparency: Union[int, str]=None,
url: str=None,
width: Union[int, str]=None,
- height: Union[int, str]=None):
+ height: Union[int, str] = None,
+ density: int = None
+ ):
"""
Args:
name (str): The name of the image element.
@@ -35,6 +37,7 @@ def __init__(self,
url (str): The URL to load when the image is clicked.
width (Union[int, str]): The width of the image (for non-proportional scaling).
height (Union[int, str]): The height of the image (for non-proportional scaling).
+ density (int):The density to use for svg to png conversion.
"""
super().__init__(name)
self.source: str = source
@@ -47,6 +50,10 @@ def __init__(self,
self.url: str = url
self.width: Union[int, str] = width
self.height: Union[int, str] = height
+ if density is not None and density > 1200:
+ self.density = 1200
+ else:
+ self.density = density
@property
def alt_text(self) -> str:
@@ -133,6 +140,8 @@ def _dict_suffixes(self) -> Dict:
result["_width"] = self.width
if self.height is not None:
result["_height"] = self.height
+ if self.density is not None:
+ result["_density"] = self.density
return result
@@ -158,7 +167,8 @@ def from_file(
transparency: Union[int, str]=None,
url: str=None,
width: Union[int, str]=None,
- height: Union[int, str]=None
+ height: Union[int, str]=None,
+ density: int = None
) -> 'Image':
"""Generate an Image object from a local file.
@@ -179,6 +189,7 @@ def from_file(
url (str): The URL to load when the image is clicked.
width (Union[int, str]): The width of the image (for non-proportional scaling).
height (Union[int, str]): The height of the image (for non-proportional scaling).
+ density (int): The density to use for svg to png conversion.
Returns:
Image: the generated Image object from a local file
@@ -195,6 +206,7 @@ def from_file(
url,
width,
height,
+ min(density, 1200) if density is not None else None
)
@staticmethod
@@ -209,7 +221,8 @@ def from_raw(
transparency: Union[int, str]=None,
url: str=None,
width: Union[int, str]=None,
- height: Union[int, str]=None
+ height: Union[int, str]=None,
+ density: int = None
) -> 'Image':
"""Generate an Image object from raw data.
@@ -230,7 +243,7 @@ def from_raw(
url (str): The URL to load when the image is clicked.
width (Union[int, str]): The width of the image (for non-proportional scaling).
height (Union[int, str]): The height of the image (for non-proportional scaling).
-
+ density (int): The density to use for svg to png conversion.
Returns:
Image: the generated Image object from raw data
"""
@@ -246,6 +259,7 @@ def from_raw(
url,
width,
height,
+ density,
)
@staticmethod
@@ -260,7 +274,8 @@ def from_base64(
transparency: Union[int, str]=None,
url: str=None,
width: Union[int, str]=None,
- height: Union[int, str]=None
+ height: Union[int, str]=None,
+ density: int = None
) -> 'Image':
"""Generate an Image object from a base64 string.
@@ -281,6 +296,8 @@ def from_base64(
url (str): The URL to load when the image is clicked.
width (Union[int, str]): The width of the image (for non-proportional scaling).
height (Union[int, str]): The height of the image (for non-proportional scaling).
+ density (int):The density to use for svg to png conversion.
+
Returns:
Image: the generated Image object from a base64 string
@@ -297,6 +314,7 @@ def from_base64(
url,
width,
height,
+ min(density, 1200) if density is not None else None
)
@staticmethod
@@ -311,7 +329,8 @@ def from_url(
transparency: Union[int, str]=None,
url: str=None,
width: Union[int, str]=None,
- height: Union[int, str]=None
+ height: Union[int, str]=None,
+ density: int = None
) -> 'Image':
"""Generate an Image object from a URL.
@@ -332,6 +351,7 @@ def from_url(
url (str): The URL to load when the image is clicked.
width (Union[int, str]): The width of the image (for non-proportional scaling).
height (Union[int, str]): The height of the image (for non-proportional scaling).
+ density (int):The density to use for svg to png conversion.
Returns:
Image: the generated Image object from a URL
@@ -348,4 +368,5 @@ def from_url(
url,
width,
height,
+ min(density, 1200) if density is not None else None
)
diff --git a/cloudofficeprint/elements/loops.py b/cloudofficeprint/elements/loops.py
index 7002ef4..2ccfe11 100644
--- a/cloudofficeprint/elements/loops.py
+++ b/cloudofficeprint/elements/loops.py
@@ -124,7 +124,7 @@ class ForEachInline(ForEach):
Note: this tag can be used to repeat only one row in Word. In Excel this works like a normal loop tag and
repeats the cells defined by the rectangular boundary of the starting and closing tag."""
- def __init__(self, name: str, content: Iterable[Element]):
+ def __init__(self, name: str, content: Iterable[Element], distribute: bool = False):
"""
Args:
name (str): The name for this element (Cloud Office Print tag).
@@ -135,7 +135,13 @@ def __init__(self, name: str, content: Iterable[Element]):
"{:" + name + "}",
"{/" + name + "}"
}
-
+ self._distribute = distribute
+ @property
+ def as_dict(self) -> Dict:
+ data = super().as_dict
+ if self._distribute:
+ data[f"{self.name}_distribute"] = True
+ return data
# These are the same, but they may not be forever
# and combining them into one class breaks consistency
@@ -157,3 +163,21 @@ def __init__(self, name: str, content: Iterable[Element]):
"{=" + name + "}",
"{/" + name + "}"
}
+
+class ForEachMergeCells(ForEach):
+ """Loop where table cells are vertically merged across rows during looping.
+ Only supported in Word templates with {##...} {/...} syntax."""
+
+ def __init__(self, name: str, content: Iterable[Element]):
+ """
+ Args:
+ name (str): The name for this element (Cloud Office Print tag with merge cells).
+ content (Iterable[Element]): An iterable containing the elements for this loop element.
+ """
+ super().__init__(name, content)
+ self._tags = {
+ "{##" + name + "}",
+ "{/" + name + "}"
+ }
+
+
diff --git a/cloudofficeprint/elements/pdf.py b/cloudofficeprint/elements/pdf.py
index 53673d5..e946c79 100644
--- a/cloudofficeprint/elements/pdf.py
+++ b/cloudofficeprint/elements/pdf.py
@@ -153,6 +153,68 @@ def _inner_dict(self) -> Dict:
result["image_max_width"] = self.max_width
return result
+
+class PDFComment(PDFInsertObject):
+ """A free‑text comment (annotation) on a PDF page."""
+ def __init__(self,
+ text: str,
+ x: int,
+ y: int,
+ page: Union[int, str] = "all",
+ # rotation: int = None,
+ bold: bool = None,
+ italic: bool = None,
+ font: str = None,
+ font_color: str = None,
+ font_size: int = None):
+ """
+ Args:
+ text (str): Text to insert.
+ x (int): X component of this object's position.
+ y (int): Y component of this object's position.
+ page (Union[int, str], optional): Page to include this object on. Either "all" or an integer. Defaults to "all".
+ rotation (int, optional): Text rotation in degrees. Defaults to None.
+ bold (bool, optional): Whether or not the text should be in bold. Defaults to None.
+ italic (bool, optional): Whether or not the text should be in italic. Defaults to None.
+ font (str, optional): The text font name. Defaults to None.
+ font_color (str, optional): The text font color, CSS notation. Defaults to None.
+ font_size (int, optional): The text font size. Defaults to None.
+ """
+ super().__init__(x, y, page)
+ self.text: str = text
+ # self.rotation: int = rotation
+ self.bold: bool = bold
+ self.italic: bool = italic
+ self.font: str = font
+ self.font_color: str = font_color
+ self.font_size: int = font_size
+
+ @staticmethod
+ def _identifier() -> str:
+ return "AOP_PDF_COMMENTS"
+
+ @property
+ def _inner_dict(self) -> Dict:
+ result = {
+ "text": self.text,
+ "x": self.x,
+ "y": self.y
+ }
+
+ # if self.rotation is not None:
+ # result["rotation"] = self.rotation
+ if self.bold is not None:
+ result["bold"] = self.bold
+ if self.italic is not None:
+ result["italic"] = self.italic
+ if self.font is not None:
+ result["font"] = self.font
+ if self.font_color is not None:
+ result["font_color"] = self.font_color
+ if self.font_size is not None:
+ result["font_size"] = self.font_size
+
+ return result
class PDFTexts(Element):
"""Group of PDF texts as an `Element`.
@@ -242,3 +304,30 @@ def as_dict(self) -> Dict:
@property
def available_tags(self) -> FrozenSet[str]:
return frozenset()
+
+class PDFComments(Element):
+ """Group multiple PDFComment instances into one Element."""
+ def __init__(self, comments: Iterable[PDFComment]):
+ """
+ Args:
+ comments (Iterable[PDFComment]): An iterable consisting of `PDFComment`-objects.
+ """
+ super().__init__(PDFComment._identifier())
+ # self.comments = comments
+ self.comments: Iterable[PDFComment] = comments
+
+
+ @property
+ def as_dict(self) -> Dict:
+ result = {}
+ for txt in self.comments:
+
+ if str(txt.page) in result:
+ result[str(txt.page)].append(txt._inner_dict)
+ else:
+ result[str(txt.page)] = [txt._inner_dict]
+ return {self.name: [result]}
+ @property
+ def available_tags(self) -> FrozenSet[str]:
+ return frozenset()
+
diff --git a/cloudofficeprint/printjob.py b/cloudofficeprint/printjob.py
index c5be0a4..1892845 100644
--- a/cloudofficeprint/printjob.py
+++ b/cloudofficeprint/printjob.py
@@ -2,19 +2,21 @@
Module containing the PrintJob class, which is also exposed at package level.
"""
-from cloudofficeprint.elements.rest_source import RESTSource
import requests
import asyncio
import json
+
+from typing import Union, List, Dict, Mapping, Optional
+from functools import partial
+from pprint import pprint
+
from .config import OutputConfig, Server
+from .elements import Element, RESTSource
from .exceptions import COPError
-from .response import Response
from .resource import Resource
-from .elements import Element, ElementCollection
-from typing import Union, List, Dict, Mapping
-from functools import partial
-import sys
-from pprint import pprint
+from .template import Template
+from .response import Response
+from .transformation import TransformationFunction
STATIC_OPTS = {
"tool": "python",
@@ -30,35 +32,44 @@ class PrintJob:
and the `PrintJob.execute` method to combine all these and send a request to the Cloud Office Print server.
"""
- def __init__(self,
- data: Union[Element, Mapping[str, Element], RESTSource],
- server: Server,
- template: Resource = None,
- output_config: OutputConfig = OutputConfig(),
- subtemplates: Dict[str, Resource] = {},
- prepend_files: List[Resource] = [],
- append_files: List[Resource] = [],
- cop_verbose: bool = False):
+ def __init__(
+ self,
+ data: Union[Element, Mapping[str, Element], RESTSource],
+ server: Server,
+ template: Union[Template, Resource] = None,
+ output_config: OutputConfig = OutputConfig(),
+ subtemplates: Dict[str, Resource] = {},
+ prepend_files: List[Resource] = [],
+ append_files: List[Resource] = [],
+ cop_verbose: bool = False,
+ compare_files: List[Resource] = [],
+ attachments : List[Resource] = [],
+ transformation_function: Optional[TransformationFunction] = None,
+ ):
"""
Args:
data (Union[Element, Mapping[str, Element], RESTSource]): This is either: An `Element` (e.g. an `ElementCollection`); A mapping, containing file names as keys and an `Element` as data. Multiple files will be produced from the different datas, the result is a zip file containing them. In the first case, no output file name is specified and the server will name it "file0".
server (Server): Server to be used for this print job.
- template (Resource): Template to use for this print job.
+ template (Union[Template, Resource]): Template to use for this print job.
output_config (OutputConfig, optional): Output configuration to be used for this print job. Defaults to `OutputConfig`().
subtemplates (Dict[str, Resource], optional): Subtemplates for this print job, accessible (in docx) through `{?include subtemplate_dict_key}`. Defaults to {}.
prepend_files (List[Resource], optional): Files to prepend to the output file. Defaults to [].
append_files (List[Resource], optional): Files to append to the output file. Defaults to [].
cop_verbose (bool, optional): Whether or not verbose mode should be activated. Defaults to False.
+ compare_files (List[Resource], optional): Files to compare with the output file. Defaults to [].
+ attachments (List[Resource], optional): Files to attach to the pdf file. Defaults to []. The file must be PDF.
"""
-
self.data: Union[Element, Mapping[str, Element], RESTSource] = data
self.server: Server = server
self.output_config: OutputConfig = output_config
- self.template: Resource = template
+ self.template: Union[Template, Resource] = template
self.subtemplates: Dict[str, Resource] = subtemplates
self.prepend_files: List[Resource] = prepend_files
self.append_files: List[Resource] = append_files
self.cop_verbose: bool = cop_verbose
+ self.attachments: List[Resource] = attachments
+ self.compare_files: List[Resource] = compare_files
+ self.transformation_function = transformation_function
def execute(self) -> Response:
"""Execute this print job.
@@ -67,7 +78,18 @@ def execute(self) -> Response:
Response: `Response`-object
"""
self.server._raise_if_unreachable()
- return self._handle_response(requests.post(self.server.url, proxies=self.server.config.proxies if self.server.config is not None else None, json=self.as_dict, headers={"Content-type": "application/json"}))
+ proxy = self.server.config.proxies if self.server.config else None
+ response = requests.post(
+ self.server.url,
+ json=self.as_dict,
+ proxies=proxy,
+ headers={"Content-type": "application/json"},
+ )
+ if type(self.template) is Template and self.template.should_hash:
+ template_hash = response.headers["Template-Hash"]
+ if template_hash:
+ self.template.update_hash(template_hash)
+ return self._handle_response(response)
async def execute_async(self) -> Response:
"""Async version of `PrintJob.execute`
@@ -76,16 +98,22 @@ async def execute_async(self) -> Response:
Response: `Response`-object
"""
self.server._raise_if_unreachable()
- return PrintJob._handle_response(
- await asyncio.get_event_loop().run_in_executor(
- None, partial(
- requests.post,
- self.server.url,
- proxies=self.server.config.proxies if self.server.config is not None else None,
- json=self.as_dict
- )
- )
+ proxy = self.server.config.proxies if self.server.config else None
+ response = await asyncio.get_event_loop().run_in_executor(
+ None,
+ partial(
+ requests.post,
+ self.server.url,
+ json=self.as_dict,
+ proxies=proxy,
+ headers={"Content-type": "application/json"},
+ ),
)
+ if type(self.template) is Template and self.template.should_hash:
+ template_hash = response.headers["Template-Hash"]
+ if template_hash:
+ self.template.update_hash(template_hash)
+ return PrintJob._handle_response(response)
@staticmethod
def execute_full_json(json_data: str, server: Server) -> Response:
@@ -99,7 +127,14 @@ def execute_full_json(json_data: str, server: Server) -> Response:
Response: `Response`-object
"""
server._raise_if_unreachable()
- return PrintJob._handle_response(requests.post(server.url, proxies=server.config.proxies if server.config is not None else None, data=json_data, headers={"Content-type": "application/json"}))
+ proxy = server.config.proxies if server.config else None
+ response = requests.post(
+ server.url,
+ data=json_data,
+ proxies=proxy,
+ headers={"Content-type": "application/json"},
+ )
+ return PrintJob._handle_response(response)
@staticmethod
async def execute_full_json_async(json_data: str, server: Server) -> Response:
@@ -113,17 +148,18 @@ async def execute_full_json_async(json_data: str, server: Server) -> Response:
Response: `Response`-object
"""
server._raise_if_unreachable()
- return PrintJob._handle_response(
- await asyncio.get_event_loop().run_in_executor(
- None, partial(
- requests.post,
- server.url,
- proxies=server.config.proxies if server.config is not None else None,
- data=json_data,
- headers={"Content-type": "application/json"}
- )
- )
+ proxy = server.config.proxies if server.config else None
+ response = await asyncio.get_event_loop().run_in_executor(
+ None,
+ partial(
+ requests.post,
+ server.url,
+ data=json_data,
+ proxies=proxy,
+ headers={"Content-type": "application/json"},
+ ),
)
+ return PrintJob._handle_response(response)
@staticmethod
def _handle_response(res: requests.Response) -> Response:
@@ -160,8 +196,8 @@ def as_dict(self) -> Dict:
Returns:
Dict: dict representation of this print job
"""
- result = dict(
- STATIC_OPTS) # Copy of STATIC_OPTS! Otherwise everything we add to 'result' will also be added to 'STATIC_OPTS'
+ # Copy of STATIC_OPTS! Otherwise everything we add to 'result' will also be added to 'STATIC_OPTS'
+ result = dict(STATIC_OPTS)
# server config goes in the upper level
if self.server.config:
result.update(self.server.config.as_dict)
@@ -176,43 +212,54 @@ def as_dict(self) -> Dict:
# If output_type is not specified, set this to the template filetype
# If no template found: default docx
- if 'output_type' not in self.output_config.as_dict.keys():
+ if "output_type" not in self.output_config.as_dict.keys():
if self.template:
- result['output']['output_type'] = result['template']['template_type']
+ result["output"]["output_type"] = result["template"]["template_type"]
else:
- result['output']['output_type'] = 'docx'
+ result["output"]["output_type"] = "docx"
if isinstance(self.data, Mapping):
- result["files"] = [{
- "filename": name,
- "data": data.as_dict
- } for name, data in self.data.items()]
+ result["files"] = [
+ {"filename": name, "data": data.as_dict}
+ for name, data in self.data.items()
+ ]
elif isinstance(self.data, RESTSource):
- result['files'] = [self.data.as_dict]
+ result["files"] = [self.data.as_dict]
else:
result["files"] = [{"data": self.data.as_dict}]
if len(self.prepend_files) > 0:
result["prepend_files"] = [
- res.secondary_file_dict for res in self.prepend_files
+ file.secondary_file_dict for file in self.prepend_files
]
if len(self.append_files) > 0:
result["append_files"] = [
- res.secondary_file_dict for res in self.append_files
+ file.secondary_file_dict for file in self.append_files
+ ]
+
+ if len(self.compare_files) > 0:
+ result["compare_files"] = [
+ file.secondary_file_dict for file in self.compare_files
+ ]
+
+ if len(self.attachments) > 0:
+ result["attachments"] = [
+ file.secondary_file_dict for file in self.attachments
]
if len(self.subtemplates) > 0:
- templates_list = []
- for name, res in self.subtemplates.items():
- to_add = res.secondary_file_dict
- to_add["name"] = name
- templates_list.append(to_add)
- result["templates"] = templates_list
-
- # If verbose mode is activated, print the result to the terminal
+ result["templates"] = [
+ {**file.secondary_file_dict, "name": name}
+ for name, file in self.subtemplates.items()
+ ]
+
+ if self.transformation_function is not None:
+ result["transformation_function"] = self.transformation_function.as_dict()
+
+
if self.cop_verbose:
- print('The JSON data that is sent to the Cloud Office Print server:\n')
+ print("The JSON data that is sent to the Cloud Office Print server:\n")
pprint(result)
return result
diff --git a/cloudofficeprint/resource.py b/cloudofficeprint/resource.py
index e39f60f..e57f86c 100644
--- a/cloudofficeprint/resource.py
+++ b/cloudofficeprint/resource.py
@@ -12,77 +12,67 @@
import json
from typing import Dict, Union
-from .own_utils import type_utils, file_utils
from abc import abstractmethod, ABC
+from .own_utils import type_utils, file_utils
+
class Resource(ABC):
"""The abstract base class for the resources."""
- def __init__(self, data: Union[str, bytes] = None, filetype: str = None):
- """
+ def __init__(
+ self,
+ data: Union[str, bytes] = None,
+ filetype: str = None,
+ ):
+ """Create a new Resource.
+
Args:
- data (Union[str, bytes], optional): the data for this resource. Defaults to None.
- filetype (str, optional): the file type of this resource. Defaults to None.
+ data (Union[str, bytes], optional): the data for this Resource. Defaults to None.
+ filetype (str, optional): the file type of this Resource. Defaults to None.
"""
- self._data: Union[str, bytes] = data
+ self.data: Union[str, bytes] = data
self.filetype: str = filetype
@property
def mimetype(self) -> str:
- """Resource type as a mime type.
-
- Returns:
- str: resource type as a mime type
"""
- return type_utils.extension_to_mimetype(self.filetype)
-
- @property
- def data(self) -> Union[str, bytes]:
- """The data contained in this Resource.
-
Returns:
- Union[str, bytes]: the data contained in this Resource
+ str: the mime type of the Resource
"""
- return self._data
+ return type_utils.extension_to_mimetype(self.filetype)
@property
def template_json(self) -> str:
- """Get the JSON representation when used as a template.
-
+ """
Returns:
- str: JSON representation of this resource as a template
+ str: the JSON representation of this Resource.
"""
return json.dumps(self.template_dict)
@property
@abstractmethod
def template_dict(self) -> Dict:
- """This Resource object as a dict object for use as a template.
- This dict and the template JSON representation (`Resource.template_json`) are isomorphic.
-
+ """
Returns:
- Dict: dict representation of this resource as a template
+ Dict: the dictionary representation of this Resource.
"""
pass
@property
def secondary_file_json(self) -> str:
- """The JSON representation for use as secondary file.
-
+ """
Returns:
- str: JSON representation of this resource as a secondary file
+ str: the JSON representation of this Resource.
"""
return json.dumps(self.secondary_file_dict)
@property
@abstractmethod
def secondary_file_dict(self) -> Dict:
- """This Resource object as a dict object for use as a secondary file (prepend, append, insert, as subtemplate).
- This dict and the "concat file" JSON representation (`Resource.secondary_file_json`) are isomorphic.
-
+ """
Returns:
- Dict: dict representation of this resource as a secondary file
+ Dict: the dictionarty representation of this resource as a secondary file (prepend, append, insert, as subtemplate).
"""
pass
@@ -90,91 +80,91 @@ def __str__(self) -> str:
"""Override the string representation of this class to return the template-style json.
Returns:
- str: JSON representation of this resource as a template
+ str: the JSON representation of this resource as a template.
"""
return self.template_json
@staticmethod
- def from_base64(base64string: str, filetype: str) -> 'Base64Resource':
- """Create a Base64Resource from a base64 string and a file type (extension).
+ def from_raw(raw_data: bytes, filetype: str) -> "RawResource":
+ """Create a RawResource from raw file data.
Args:
- base64string (str): base64 encoded string
- filetype (str): file type (extension)
+ raw_data (bytes): the raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object).
+ filetype (str): the file type (extension).
Returns:
- Base64Resource: the created Resource
+ RawResource: the created RawResource.
"""
- return Base64Resource(base64string, filetype)
+ return RawResource(raw_data, filetype)
@staticmethod
- def from_raw(raw_data: bytes, filetype: str) -> 'RawResource':
- """Create a RawResource from raw file data and a file type (extension).
+ def from_base64(base64string: str, filetype: str) -> "Base64Resource":
+ """Create a Base64Resource from a base64 string.
Args:
- raw_data (bytes): raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object)
- filetype (str): file type (extension)
+ base64string (str): the base64 encoded representation of a file.
+ filetype (str): the file type (extension).
Returns:
- RawResource: the created Resource
+ Base64Resource: the created Base64Resource.
"""
- return RawResource(raw_data, filetype)
+ return Base64Resource(base64string, filetype)
@staticmethod
- def from_local_file(local_path: str) -> 'Base64Resource':
+ def from_local_file(local_path: str) -> "Base64Resource":
"""Create a Base64Resource with the contents of a local file.
+ The filetype is determined by the extension of the file.
Throws IOError if it can't read the file.
- The filetype is determined by the extension of the file.
Args:
- local_path (str): path to local file
+ local_path (str): the path to local file.
Returns:
- Base64Resource: the created Resource
+ Base64Resource: the created Base64Resource.
"""
- base64string: str = file_utils.read_file_as_base64(local_path)
- return Base64Resource(base64string, type_utils.path_to_extension(local_path))
+ return Base64Resource(
+ file_utils.read_file_as_base64(local_path),
+ type_utils.path_to_extension(local_path),
+ )
@staticmethod
- def from_server_path(path: str) -> 'ServerPathResource':
+ def from_server_path(path: str) -> "ServerPathResource":
"""Create a ServerPathResource targeting a file on the server.
-
The filetype is determined by the extension of the file.
Args:
- path (str): location of target file on the server
+ path (str): the location of target file on the server.
Returns:
- ServerPathResource: the created Resource
+ ServerPathResource: the created ServerPathResource.
"""
return ServerPathResource(path)
@staticmethod
- def from_url(url: str, filetype: str) -> 'URLResource':
- """Create an Resource targeting the file at url with given filetype (extension).
+ def from_url(url: str, filetype: str) -> "URLResource":
+ """Create an URLResource targeting the file at a given URL.
Args:
- url (str): file url
- filetype (str): file type (extension)
+ url (str): the file URL.
+ filetype (str): the file type (extension).
Returns:
- URLResource: the created Resource
+ URLResource: the created URLResource.
"""
return URLResource(url, filetype)
@staticmethod
- def from_html(htmlstring: str, landscape: bool = False) -> 'HTMLResource':
+ def from_html(htmlstring: str, landscape: bool = False) -> "HTMLResource":
"""Create an HTMLResource with html data in plain text.
-
Landscape is not supported for prepend/append sources, only for template resources.
Args:
- htmlstring (str): html content
- landscape (bool, optional): whether to use the landscape option. Defaults to False.
+ htmlstring (str): the html content.
+ landscape (bool, optional): whether or not to use the landscape option. Defaults to False.
Returns:
- HTMLResource: the created Resource
+ HTMLResource: the created HTMLResource.
"""
return HTMLResource(htmlstring, landscape)
@@ -183,62 +173,83 @@ class RawResource(Resource):
"""A `Resource` containing raw binary data."""
def __init__(self, raw_data: bytes, filetype: str):
- """
+ """Create a new RawResource.
+
Args:
- raw_data (bytes): raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object)
- filetype (str): file type (extension)
+ raw_data (bytes): the raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object).
+ filetype (str): the file type (extension).
"""
super().__init__(raw_data, filetype)
@property
def base64(self) -> str:
- """Base64 representation of the raw data in `RawResource.data`.
-
+ """
Returns:
- str: base64 representation of the raw data in `RawResource.data`
+ str: the base64 representation of the raw data in `RawResource.data`.
"""
return file_utils.raw_to_base64(self.data)
@property
def template_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"template_type": self.filetype,
- "file": self.base64
+ "file": self.base64,
}
@property
def secondary_file_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"mime_type": self.mimetype,
"file_source": "base64",
- "file_content": self.base64
+ "file_content": self.base64,
}
class Base64Resource(Resource):
"""A `Resource` containing base64 data."""
- def __init__(self, base64string: str, filetype: str):
- """
+ def __init__(
+ self,
+ base64string: str,
+ filetype: str,
+ ):
+ """Create a new Base64Resource.
+
Args:
- base64string (str): base64 encoded file
- filetype (str): file type (extension)
+ base64string (str): the base64 encoded data.
+ filetype (str): the file type (extension).
"""
super().__init__(base64string, filetype)
@property
def template_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"template_type": self.filetype,
- "file": self.data
+ "file": self.data,
}
@property
def secondary_file_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"mime_type": self.mimetype,
"file_source": "base64",
- "file_content": self.data
+ "file_content": self.data,
}
@@ -246,25 +257,34 @@ class ServerPathResource(Resource):
"""A `Resource` targeting a file on the server."""
def __init__(self, server_path: str):
- """
+ """Create a new ServerPathResource.
+
Args:
- server_path (str): path on the server to target
+ server_path (str): the path on the server to target.
"""
super().__init__(server_path, type_utils.path_to_extension(server_path))
@property
def template_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"template_type": self.filetype,
- "filename": self.data
+ "filename": self.data,
}
@property
def secondary_file_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"mime_type": self.mimetype,
"file_source": "file",
- "filename": self.data
+ "filename": self.data,
}
@@ -272,37 +292,51 @@ class URLResource(Resource):
"""A `Resource` targeting a file at a URL."""
def __init__(self, url: str, filetype: str):
- """
+ """Create a new URLResource.
+
Args:
- url (str): URL location of the file
- filetype (str): file type (extension)
+ url (str): the URL location of the file.
+ filetype (str): the file type (extension).
"""
super().__init__(url, filetype)
@property
def template_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"template_type": self.filetype,
- "url": self.data
+ "url": self.data,
}
@property
def secondary_file_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"mime_type": self.mimetype,
"file_source": "file",
- "file_url": self.data
+ "file_url": self.data,
}
class HTMLResource(Resource):
"""A Resource containing HTML data in plain text."""
- def __init__(self, htmlstring: str, landscape: bool = False):
- """
+ def __init__(
+ self,
+ htmlstring: str,
+ landscape: bool = False,
+ ):
+ """Create a new HTMLResource.
+
Args:
- htmlstring (str): HTML input in plain text
- landscape (bool, optional): Whether the HTML should be rendered as landscape-oriented page. Defaults to False.
+ htmlstring (str): the HTML input in plain text.
+ landscape (bool, optional): whether the HTML should be rendered as landscape-oriented page. Defaults to False.
"""
super().__init__(htmlstring, "html")
self.landscape: bool = landscape
@@ -315,26 +349,30 @@ def orientation(self) -> str:
Orientation is not supported for prepend/append sources, only for template resources.
Returns:
- str: orientation
+ str: the orientation.
"""
return None if not self.landscape else "landscape"
@property
def template_dict(self) -> Dict:
- result = {
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
+ return {
"template_type": self.filetype,
- "html_template_content": self.data
+ "html_template_content": self.data,
+ "orientation": self.orientation,
}
- if self.orientation is not None:
- result["orientation"] = self.orientation
-
- return result
-
@property
def secondary_file_dict(self) -> Dict:
+ """
+ Returns:
+ str: the JSON representation of this resource.
+ """
return {
"mime_type": self.mimetype,
"file_source": "file",
- "file_content": self.data
+ "file_content": self.data,
}
diff --git a/cloudofficeprint/template.py b/cloudofficeprint/template.py
new file mode 100644
index 0000000..c7c3aaa
--- /dev/null
+++ b/cloudofficeprint/template.py
@@ -0,0 +1,281 @@
+import json
+from typing import Dict
+
+from .resource import Resource
+
+
+class Template:
+ """The Template class"""
+
+ def __init__(
+ self,
+ resource: Resource,
+ start_delimiter: str = None,
+ end_delimiter: str = None,
+ should_hash: bool = None,
+ template_hash: str = None,
+ ):
+ """Create a new Template.
+
+ Args:
+ resource (Resource): the resource of this template.
+ start_delimiter (str, optional): the starting delimiter used in the template.
+ end_delimiter (str, optional): the starting delimiter used in the template.
+ should_hash (bool, optional): whether the template should be hashed on the server.
+ template_hash (str, optional): the hash of the template.
+ """
+ self.resource = resource
+ self.start_delimiter = start_delimiter
+ self.end_delimiter = end_delimiter
+ self.should_hash = should_hash
+ self.template_hash = template_hash
+
+ def update_hash(self, template_hash: str):
+ """Update the Template to store a hash.
+ On the next request to the server, the file data will not be sent, only the hash of the template.
+
+ Args:
+ template_hash (str): the hash of the template.
+ """
+ self.template_hash = template_hash
+ self.should_hash = False
+
+ def reset_hash(self, should_hash: bool = True):
+ """Reset the stored hash of the template.
+
+ Args:
+ should_hash (bool, optional): whether the template should be hashed on the server. Defaults to True.
+ """
+ self.template_hash = None
+ self.should_hash = should_hash
+
+ @property
+ def mimetype(self) -> str:
+ """
+ Returns:
+ str: the mime type of the Resource
+ """
+ return self.resource.mimetype
+
+ @property
+ def template_json(self) -> str:
+ """
+ Returns:
+ str: the JSON representation of this Resource.
+ """
+ return json.dumps(self.template_dict)
+
+ @property
+ def template_dict(self) -> Dict:
+ """
+ Returns:
+ Dict: the dictionary representation of this Resource.
+ """
+ if self.template_hash and not self.should_hash:
+ dict = {
+ "template_type": self.resource.filetype,
+ "template_hash": self.template_hash,
+ }
+ if self.start_delimiter:
+ dict["start_delimiter"] = self.start_delimiter
+ if self.end_delimiter:
+ dict["end_delimiter"] = self.end_delimiter
+ return dict
+ dict = self.resource.template_dict
+ if self.start_delimiter:
+ dict["start_delimiter"] = self.start_delimiter
+ if self.end_delimiter:
+ dict["end_delimiter"] = self.end_delimiter
+ if self.should_hash:
+ dict["should_hash"] = self.should_hash
+ if self.template_hash:
+ dict["template_hash"] = self.template_hash
+ return dict
+
+ def __str__(self) -> str:
+ """Override the string representation of this class to return the template-style json.
+
+ Returns:
+ str: the JSON representation of this resource as a template.
+ """
+ return self.template_json
+
+ @staticmethod
+ def from_raw(
+ raw_data: bytes,
+ filetype: str,
+ start_delimiter: str = None,
+ end_delimiter: str = None,
+ should_hash: bool = None,
+ template_hash: str = None,
+ ) -> "Template":
+ """Create a Template with a RawResource from raw file data.
+
+ Args:
+ raw_data (bytes): the raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object).
+ filetype (str): the file type (extension).
+ start_delimiter (str, optional): the starting delimiter used in the template.
+ end_delimiter (str, optional): the starting delimiter used in the template.
+ should_hash (bool, optional): whether the template should be hashed on the server.
+ template_hash (str, optional): the hash of the template.
+
+ Returns:
+ Template: the created Template.
+ """
+ return Template(
+ Resource.from_raw(raw_data, filetype),
+ start_delimiter,
+ end_delimiter,
+ should_hash,
+ template_hash,
+ )
+
+ @staticmethod
+ def from_base64(
+ base64string: str,
+ filetype: str,
+ start_delimiter: str = None,
+ end_delimiter: str = None,
+ should_hash: bool = None,
+ template_hash: str = None,
+ ) -> "Template":
+ """Create a Template with a Base64Resource from a base64 string.
+
+ Args:
+ base64string (str): the base64 encoded representation of a file.
+ filetype (str): the file type (extension).
+ start_delimiter (str, optional): the starting delimiter used in the template.
+ end_delimiter (str, optional): the starting delimiter used in the template.
+ should_hash (bool, optional): whether the template should be hashed on the server.
+ template_hash (str, optional): the hash of the template.
+
+ Returns:
+ Template: the created Template.
+ """
+ return Template(
+ Resource.from_base64(base64string, filetype),
+ start_delimiter,
+ end_delimiter,
+ should_hash,
+ template_hash,
+ )
+
+ @staticmethod
+ def from_local_file(
+ local_path: str,
+ start_delimiter: str = None,
+ end_delimiter: str = None,
+ should_hash: bool = None,
+ template_hash: str = None,
+ ) -> "Template":
+ """Create a Template with a Base64Resource with the contents of a local file.
+ The filetype is determined by the extension of the file.
+
+ Throws IOError if it can't read the file.
+
+ Args:
+ local_path (str): the path to local file.
+ start_delimiter (str, optional): the starting delimiter used in the template.
+ end_delimiter (str, optional): the starting delimiter used in the template.
+ should_hash (bool, optional): whether the template should be hashed on the server.
+ template_hash (str, optional): the hash of the template.
+
+ Returns:
+ Template: the created Template.
+ """
+ return Template(
+ Resource.from_local_file(local_path),
+ start_delimiter,
+ end_delimiter,
+ should_hash,
+ template_hash,
+ )
+
+ @staticmethod
+ def from_server_path(
+ path: str,
+ start_delimiter: str = None,
+ end_delimiter: str = None,
+ should_hash: bool = None,
+ template_hash: str = None,
+ ) -> "Template":
+ """Create a Template with a ServerPathResource targeting a file on the server.
+ The filetype is determined by the extension of the file.
+
+ Args:
+ path (str): the location of target file on the server.
+ start_delimiter (str, optional): the starting delimiter used in the template.
+ end_delimiter (str, optional): the starting delimiter used in the template.
+ should_hash (bool, optional): whether the template should be hashed on the server.
+ template_hash (str, optional): the hash of the template.
+
+ Returns:
+ Template: the created Template.
+ """
+ return Template(
+ Resource.from_server_path(path),
+ start_delimiter,
+ end_delimiter,
+ should_hash,
+ template_hash,
+ )
+
+ @staticmethod
+ def from_url(
+ url: str,
+ filetype: str,
+ start_delimiter: str = None,
+ end_delimiter: str = None,
+ should_hash: bool = None,
+ template_hash: str = None,
+ ) -> "Template":
+ """Create a Template with a URLResource targeting the file at a given url.
+
+ Args:
+ url (str): the file URL.
+ filetype (str): the file type (extension).
+ start_delimiter (str, optional): the starting delimiter used in the template.
+ end_delimiter (str, optional): the starting delimiter used in the template.
+ should_hash (bool, optional): whether the template should be hashed on the server.
+ template_hash (str, optional): the hash of the template.
+
+ Returns:
+ Template: the created Template.
+ """
+ return Template(
+ Resource.from_url(url, filetype),
+ start_delimiter,
+ end_delimiter,
+ should_hash,
+ template_hash,
+ )
+
+ @staticmethod
+ def from_html(
+ htmlstring: str,
+ landscape: bool = False,
+ start_delimiter: str = None,
+ end_delimiter: str = None,
+ should_hash: bool = None,
+ template_hash: str = None,
+ ) -> "Template":
+ """Create a Template with a HTMLResource with html data in plain text.
+
+ Args:
+ htmlstring (str): the html content.
+ landscape (bool, optional): whether or not to use the landscape option. Defaults to False.
+ start_delimiter (str, optional): the starting delimiter used in the template.
+ end_delimiter (str, optional): the starting delimiter used in the template.
+ should_hash (bool, optional): whether the template should be hashed on the server.
+ template_hash (str, optional): the hash of the template.
+
+ Returns:
+ Template: the created Template.
+ """
+ return Template(
+ Resource.from_html(htmlstring, landscape),
+ start_delimiter,
+ end_delimiter,
+ should_hash,
+ template_hash,
+ )
diff --git a/cloudofficeprint/transformation.py b/cloudofficeprint/transformation.py
new file mode 100644
index 0000000..8c22542
--- /dev/null
+++ b/cloudofficeprint/transformation.py
@@ -0,0 +1,54 @@
+from typing import Optional
+
+class TransformationFunction:
+ """
+ Represents a transformation function for Cloud Office Print (AOP).
+ Args:
+ js_code (str, optional): Inline JavaScript code for transformation.
+ filename (str, optional): Filename from AOP's `assets/transformation_function/` directory.
+ """
+
+ def __init__(
+ self,
+ js_code: Optional[str] = None,
+ filename: Optional[str] = None
+ ):
+ self._js_code: Optional[str] = None
+ self._filename: Optional[str] = None
+
+ if js_code is not None:
+ self.js_code = js_code
+ if filename is not None:
+ self.filename = filename
+
+ @property
+ def js_code(self) -> Optional[str]:
+ return self._js_code
+
+ @js_code.setter
+ def js_code(self, code: str):
+ if self._filename is not None:
+ raise ValueError("Cannot set js_code when filename is already set")
+ if not isinstance(code, str) or not code.strip():
+ raise ValueError("js_code must be a non‑empty string")
+ self._js_code = code
+
+ @property
+ def filename(self) -> Optional[str]:
+ return self._filename
+
+ @filename.setter
+ def filename(self, name: str):
+ if self._js_code is not None:
+ raise ValueError("Cannot set filename when js_code is already set")
+ if not isinstance(name, str) or not name.lower().endswith(".js"):
+ raise ValueError("Filename must be a string ending with '.js'")
+ if "/" in name or "\\" in name:
+ raise ValueError("Filename must not include any path separators")
+ self._filename = name
+
+ def as_dict(self) -> Optional[str]:
+ """
+ Return the transformation_function
+ """
+ return self._js_code or self._filename
diff --git a/docs/cloudofficeprint.html b/docs/cloudofficeprint.html
index 9aacb90..61775ac 100644
--- a/docs/cloudofficeprint.html
+++ b/docs/cloudofficeprint.html
@@ -3,68 +3,40 @@
-
+
cloudofficeprint API documentation
-
-
-
-
-
-
-
-
-
+
cloudofficeprint
@@ -183,15 +155,19 @@
Usage
The examples below call this package cop.
-
importcloudofficeprintascop
-
+
+
importcloudofficeprintascop
+
+
Templates
Templates are represented by Resource. The simplest way to obtain a Resource is to load from a local path.
A Cloud Office Print server is configured as a config.Server. It takes a url and an optional config.ServerConfig which allows for various server configuration options. If you're using Cloud Office Print Cloud edition, you will need to use this to declare your API key.
PrintJob combines template, data, server and an optional output configuration (config.OutputConfig) and can execute itself on the Cloud Office Print server. An example using the variables declared above:
A print job can be executed asynchronously as well.
-
importasyncio
+
+
importasynciocoroutine=printjob.execute_async()# simply await your result when you need itresult=awaitcoroutine
-
+
+
Full JSON available
If you already have the JSON to be sent to the server (not just the data, but the entire JSON body including your API key and template), this package will wrap the request to the server for you (requests are made using requests).
In case the Cloud Office Print server returns an error, PrintJob will throw one as well.
You can catch it and get either its user-readable message or an encoded stack trace that can be passed to Cloud Office Print support.
For further information, such as where to find our examples, we refer to our README.md file on our Github page.
-
- View Source
-
"""
-This Python package provides a programmatic interface with a [Cloud Office Print](https://www.cloudofficeprint.com) server.
-
-## Usage
-The examples below call this package cop.
-```python
-import cloudofficeprint as cop
-```
-
-### Templates
-Templates are represented by `Resource`. The simplest way to obtain a `Resource` is to load from a local path.
-```python
-template = cop.Resource.from_local_file("./path/to/template.docx")
-```
-
-### Render elements
-Most render elements encapsulate the data for a single tag. An `elements.ElementCollection` is an element which represents a collection of elements.
-
-Combining a simple line chart and some text tags:
-```python
-line = cop.elements.LineChart(
- "linechart",
- cop.elements.LineSeries([1, 2, 3, 4], [1, 2, 3, 4], color="green"),
- cop.elements.XYSeries([1, 2, 3, 4], ["a", "b", "c", "d"])
-)
-
-text_tag = cop.elements.Property("tag-name", "Hello, world!")
-# or multiple at once using ElementCollection.from_mapping
-# and supplying the dictionary representation directly
-text_tags = cop.elements.ElementCollection.from_mapping({
- "another-tag": "Foo",
- "one-more-tag": "Bar"
-})
-
-combined_data = cop.elements.ElementCollection()
-combined_data.add(line)
-combined_data.add(text_tag)
-combined_data.add_all(text_tags)
-```
-
-### The server
-A Cloud Office Print server is configured as a `config.Server`. It takes a url and an optional `config.ServerConfig` which allows for various server configuration options. If you're using Cloud Office Print Cloud edition, you will need to use this to declare your API key.
-
-```python
-server = cop.config.Server(
- "http://server.url.com/",
- cop.config.ServerConfig(api_key = "YOUR_API_KEY")
-)
-```
-
-### Print job
-`PrintJob` combines template, data, server and an optional output configuration (`config.OutputConfig`) and can execute itself on the Cloud Office Print server. An example using the variables declared above:
-
-```python
-printjob = cop.PrintJob(combined_data, server, template)
-printjob.execute()
-```
-
-A print job can be executed asynchronously as well.
-
-```python
-import asyncio
-coroutine = printjob.execute_async()
-# simply await your result when you need it
-result = await coroutine
-```
-
-### Full JSON available
-If you already have the JSON to be sent to the server (not just the data, but the entire JSON body including your API key and template), this package will wrap the request to the server for you (requests are made using [requests](https://requests.readthedocs.io/en/master/)).
-```python
-json_data = open("./path/to/data.json", "r").read()
-cop.PrintJob.execute_full_json(
- json_data, server
- ).to_file("./test/from_full_json_output")
-```
-
-### Server errors
-In case the Cloud Office Print server returns an error, `PrintJob` will throw one as well.
-You can catch it and get either its user-readable message or an encoded stack trace that can be passed to Cloud Office Print support.
-```python
-try:
- # execute some previously constructed printjob
- printjob.execute()
-except cop.exceptions.COPError as err:
- print("Cloud Office Print error! " + err.user_message)
- print(err.encoded_message)
- ...
-```
-
-### Further information
-For further information, such as where to find our examples, we refer to our README.md file on our [Github page](https://github.com/United-Codes/cloudofficeprint-python/).
-"""
-
-from.importexceptions,config,elements,own_utils
-
-from.printjobimportPrintJob
-from.resourceimportResource
-from.responseimportResponse
-
-# specify what is imported on "from cloudofficeprint import *"
-# but that shouldn't really be used anyway
-__all__=[
- "exceptions",
- "config",
- "elements",
- "own_utils",
- "PrintJob",
- "Resource",
- "Response"
-]
-
"""Custom exceptions for cloudofficeprint."""
-
-fromtypingimportList
-
-
-classCOPError(Exception):
- """The error that is thrown when the Cloud Office Print server itself returns an error instead of a result.
-
- It contains a user message and an encoded message to be handed to Cloud Office Print support if they are contacted.
- """
-
- def__init__(self,full_message:str):
- """
- Args:
- full_message (str): the full error message received from the Cloud Office Print server
- """
- (self._user_message,
- self._contact_support_message,
- self._encoded_message)=self._split_message(full_message)
- super().__init__(self._user_message)
-
- @staticmethod
- def_split_message(message:str)->List[str]:
- """Split the Cloud Office Print server error message into different parts: user message, contact support message and encoded message.
-
- Args:
- message (str): Cloud Office Print server error message
-
- Returns:
- List[str]: a list with the split messages
- """
- separated=message.split("\n")
- # everything before the last 2 lines are considered user message
- user_message="\n".join(separated[:-2])
- # second to last line contains the support message
- contact_support_message=separated[-2]
- # last line contains the encoded message
- encoded_message=separated[-1]
- return[user_message,contact_support_message,encoded_message]
-
- @property
- defencoded_message(self)->str:
- """The encrypted and encoded part of the message, for Cloud Office Print support.
-
- Returns:
- str: the encrypted and encoded part of the message, for Cloud Office Print support
- """
- returnself._encoded_message
-
- @property
- defuser_message(self)->str:
- """The user-friendly part of the message.
-
- Returns:
- str: the user-friendly part of the message
- """
- returnself._user_message
-
- @property
- defcontact_support_message(self)->str:
- """The contact support message.
-
- Returns:
- str: the contact support message
- """
- returnself._contact_support_message
-
- @property
- deffull_message(self)->str:
- """The full error message as sent by the server.
-
- Returns:
- str: the full error message as sent by the server
- """
- returnself.user_message+"\n"+self.contact_support_message+"\n"+self.encoded_message
-
"""
-Module for output configurations.
-
-The classes under this module encapsulate various configuration options for a print job.
-They are to be used with `cloudofficeprint.printjob.PrintJob`.
-"""
-
-from.cloudimport*
-from.csvimport*
-from.outputimport*
-from.pdfimport*
-from.serverimport*
-
-
-
-
-
Module for output configurations.
-
-
The classes under this module encapsulate various configuration options for a print job.
-They are to be used with cloudofficeprint.printjob.PrintJob.
"""
-Elements are used to replace the various tags in a template with actual data.
-"""
-
-from.chartsimport*
-from.codesimport*
-from.elementsimport*
-from.imagesimport*
-from.loopsimport*
-from.pdfimport*
-from.rest_sourceimport*
-
-
-
-
-
Elements are used to replace the various tags in a template with actual data.
1"""
+ 2This Python package provides a programmatic interface with a [Cloud Office Print](https://www.cloudofficeprint.com) server.
+ 3
+ 4## Usage
+ 5The examples below call this package cop.
+ 6```python
+ 7import cloudofficeprint as cop
+ 8```
+ 9
+ 10### Templates
+ 11Templates are represented by `Resource`. The simplest way to obtain a `Resource` is to load from a local path.
+ 12```python
+ 13template = cop.Resource.from_local_file("./path/to/template.docx")
+ 14```
+ 15
+ 16### Render elements
+ 17Most render elements encapsulate the data for a single tag. An `elements.ElementCollection` is an element which represents a collection of elements.
+ 18
+ 19Combining a simple line chart and some text tags:
+ 20```python
+ 21line = cop.elements.LineChart(
+ 22 "linechart",
+ 23 cop.elements.LineSeries([1, 2, 3, 4], [1, 2, 3, 4], color="green"),
+ 24 cop.elements.XYSeries([1, 2, 3, 4], ["a", "b", "c", "d"])
+ 25)
+ 26
+ 27text_tag = cop.elements.Property("tag-name", "Hello, world!")
+ 28# or multiple at once using ElementCollection.from_mapping
+ 29# and supplying the dictionary representation directly
+ 30text_tags = cop.elements.ElementCollection.from_mapping({
+ 31 "another-tag": "Foo",
+ 32 "one-more-tag": "Bar"
+ 33})
+ 34
+ 35combined_data = cop.elements.ElementCollection()
+ 36combined_data.add(line)
+ 37combined_data.add(text_tag)
+ 38combined_data.add_all(text_tags)
+ 39```
+ 40
+ 41### The server
+ 42A Cloud Office Print server is configured as a `config.Server`. It takes a url and an optional `config.ServerConfig` which allows for various server configuration options. If you're using Cloud Office Print Cloud edition, you will need to use this to declare your API key.
+ 43
+ 44```python
+ 45server = cop.config.Server(
+ 46 "http://server.url.com/",
+ 47 cop.config.ServerConfig(api_key = "YOUR_API_KEY")
+ 48)
+ 49```
+ 50
+ 51### Print job
+ 52`PrintJob` combines template, data, server and an optional output configuration (`config.OutputConfig`) and can execute itself on the Cloud Office Print server. An example using the variables declared above:
+ 53
+ 54```python
+ 55printjob = cop.PrintJob(combined_data, server, template)
+ 56printjob.execute()
+ 57```
+ 58
+ 59A print job can be executed asynchronously as well.
+ 60
+ 61```python
+ 62import asyncio
+ 63coroutine = printjob.execute_async()
+ 64# simply await your result when you need it
+ 65result = await coroutine
+ 66```
+ 67
+ 68### Full JSON available
+ 69If you already have the JSON to be sent to the server (not just the data, but the entire JSON body including your API key and template), this package will wrap the request to the server for you (requests are made using [requests](https://requests.readthedocs.io/en/master/)).
+ 70```python
+ 71json_data = open("./path/to/data.json", "r").read()
+ 72cop.PrintJob.execute_full_json(
+ 73 json_data, server
+ 74 ).to_file("./test/from_full_json_output")
+ 75```
+ 76
+ 77### Server errors
+ 78In case the Cloud Office Print server returns an error, `PrintJob` will throw one as well.
+ 79You can catch it and get either its user-readable message or an encoded stack trace that can be passed to Cloud Office Print support.
+ 80```python
+ 81try:
+ 82 # execute some previously constructed printjob
+ 83 printjob.execute()
+ 84except cop.exceptions.COPError as err:
+ 85 print("Cloud Office Print error! " + err.user_message)
+ 86 print(err.encoded_message)
+ 87 ...
+ 88```
+ 89
+ 90### Further information
+ 91For further information, such as where to find our examples, we refer to our README.md file on our [Github page](https://github.com/United-Codes/cloudofficeprint-python/).
+ 92"""
+ 93
+ 94from.importexceptions,config,elements,own_utils
+ 95
+ 96from.printjobimportPrintJob
+ 97from.resourceimportResource
+ 98from.responseimportResponse
+ 99
+100# specify what is imported on "from cloudofficeprint import *"
+101# but that shouldn't really be used anyway
+102__all__=[
+103"exceptions",
+104"config",
+105"elements",
+106"own_utils",
+107"PrintJob",
+108"Resource",
+109"Response"
+110]
+
27classPrintJob:
+ 28"""A print job for a Cloud Office Print server.
+ 29
+ 30 This class contains all configuration options, resources, render elements ...
+ 31 and the `PrintJob.execute` method to combine all these and send a request to the Cloud Office Print server.
+ 32 """
+ 33
+ 34def__init__(self,
+ 35data:Union[Element,Mapping[str,Element],RESTSource],
+ 36server:Server,
+ 37template:Resource=None,
+ 38output_config:OutputConfig=OutputConfig(),
+ 39subtemplates:Dict[str,Resource]={},
+ 40prepend_files:List[Resource]=[],
+ 41append_files:List[Resource]=[],
+ 42cop_verbose:bool=False):
+ 43"""
+ 44 Args:
+ 45 data (Union[Element, Mapping[str, Element], RESTSource]): This is either: An `Element` (e.g. an `ElementCollection`); A mapping, containing file names as keys and an `Element` as data. Multiple files will be produced from the different datas, the result is a zip file containing them. In the first case, no output file name is specified and the server will name it "file0".
+ 46 server (Server): Server to be used for this print job.
+ 47 template (Resource): Template to use for this print job.
+ 48 output_config (OutputConfig, optional): Output configuration to be used for this print job. Defaults to `OutputConfig`().
+ 49 subtemplates (Dict[str, Resource], optional): Subtemplates for this print job, accessible (in docx) through `{?include subtemplate_dict_key}`. Defaults to {}.
+ 50 prepend_files (List[Resource], optional): Files to prepend to the output file. Defaults to [].
+ 51 append_files (List[Resource], optional): Files to append to the output file. Defaults to [].
+ 52 cop_verbose (bool, optional): Whether or not verbose mode should be activated. Defaults to False.
+ 53 """
+ 54
+ 55self.data:Union[Element,Mapping[str,Element],RESTSource]=data
+ 56self.server:Server=server
+ 57self.output_config:OutputConfig=output_config
+ 58self.template:Resource=template
+ 59self.subtemplates:Dict[str,Resource]=subtemplates
+ 60self.prepend_files:List[Resource]=prepend_files
+ 61self.append_files:List[Resource]=append_files
+ 62self.cop_verbose:bool=cop_verbose
+ 63
+ 64defexecute(self)->Response:
+ 65"""Execute this print job.
+ 66
+ 67 Returns:
+ 68 Response: `Response`-object
+ 69 """
+ 70self.server._raise_if_unreachable()
+ 71returnself._handle_response(requests.post(self.server.url,proxies=self.server.config.proxiesifself.server.configisnotNoneelseNone,json=self.as_dict,headers={"Content-type":"application/json"}))
+ 72
+ 73asyncdefexecute_async(self)->Response:
+ 74"""Async version of `PrintJob.execute`
+ 75
+ 76 Returns:
+ 77 Response: `Response`-object
+ 78 """
+ 79self.server._raise_if_unreachable()
+ 80returnPrintJob._handle_response(
+ 81awaitasyncio.get_event_loop().run_in_executor(
+ 82None,partial(
+ 83requests.post,
+ 84self.server.url,
+ 85proxies=self.server.config.proxiesifself.server.configisnotNoneelseNone,
+ 86json=self.as_dict
+ 87)
+ 88)
+ 89)
+ 90
+ 91@staticmethod
+ 92defexecute_full_json(json_data:str,server:Server)->Response:
+ 93"""If you already have the JSON to be sent to the server (not just the data, but the entire JSON body including your API key and template), this package will wrap the request to the server.
+ 94
+ 95 Args:
+ 96 json_data (str): full JSON data that needs to be sent to a Cloud Office Print server
+ 97 server (Server): `Server`-object
+ 98
+ 99 Returns:
+100 Response: `Response`-object
+101 """
+102server._raise_if_unreachable()
+103returnPrintJob._handle_response(requests.post(server.url,proxies=server.config.proxiesifserver.configisnotNoneelseNone,data=json_data,headers={"Content-type":"application/json"}))
+104
+105@staticmethod
+106asyncdefexecute_full_json_async(json_data:str,server:Server)->Response:
+107"""Async version of `Printjob.execute_full_json`
+108
+109 Args:
+110 json_data (str): full JSON data that needs to be sent to a Cloud Office Print server
+111 server (Server): `Server`-object
+112
+113 Returns:
+114 Response: `Response`-object
+115 """
+116server._raise_if_unreachable()
+117returnPrintJob._handle_response(
+118awaitasyncio.get_event_loop().run_in_executor(
+119None,partial(
+120requests.post,
+121server.url,
+122proxies=server.config.proxiesifserver.configisnotNoneelseNone,
+123data=json_data,
+124headers={"Content-type":"application/json"}
+125)
+126)
+127)
+128
+129@staticmethod
+130def_handle_response(res:requests.Response)->Response:
+131"""Converts the HTML response to a `Response`-object
+132
+133 Args:
+134 res (requests.Response): HTML response from the Cloud Office Print server
+135
+136 Raises:
+137 COPError: Error when the HTML status code is not 200
+138
+139 Returns:
+140 Response: `Response`-object of HTML response
+141 """
+142ifres.status_code!=200:
+143raiseCOPError(res.text)
+144else:
+145returnResponse(res)
+146
+147@property
+148defjson(self)->str:
+149"""JSON equivalent of the dict representation of this print job.
+150 This representation is isomorphic to the dict representation `Printjob.as_dict`.
+151
+152 Returns:
+153 str: JSON equivalent of the dict representation of this print job
+154 """
+155returnjson.dumps(self.as_dict)
+156
+157@property
+158defas_dict(self)->Dict:
+159"""Return the dict representation of this print job.
+160
+161 Returns:
+162 Dict: dict representation of this print job
+163 """
+164result=dict(
+165STATIC_OPTS)# Copy of STATIC_OPTS! Otherwise everything we add to 'result' will also be added to 'STATIC_OPTS'
+166# server config goes in the upper level
+167ifself.server.config:
+168result.update(self.server.config.as_dict)
+169
+170# output config goes in "output"
+171# and decides where its sub-configs go through its as_dict property
+172# (e.g. PDFConfigs are just appended at this "output" level)
+173result["output"]=self.output_config.as_dict
+174
+175ifself.template:
+176result["template"]=self.template.template_dict
+177
+178# If output_type is not specified, set this to the template filetype
+179# If no template found: default docx
+180if'output_type'notinself.output_config.as_dict.keys():
+181ifself.template:
+182result['output']['output_type']=result['template']['template_type']
+183else:
+184result['output']['output_type']='docx'
+185
+186ifisinstance(self.data,Mapping):
+187result["files"]=[{
+188"filename":name,
+189"data":data.as_dict
+190}forname,datainself.data.items()]
+191elifisinstance(self.data,RESTSource):
+192result['files']=[self.data.as_dict]
+193else:
+194result["files"]=[{"data":self.data.as_dict}]
+195
+196iflen(self.prepend_files)>0:
+197result["prepend_files"]=[
+198res.secondary_file_dictforresinself.prepend_files
+199]
+200
+201iflen(self.append_files)>0:
+202result["append_files"]=[
+203res.secondary_file_dictforresinself.append_files
+204]
+205
+206iflen(self.subtemplates)>0:
+207templates_list=[]
+208forname,resinself.subtemplates.items():
+209to_add=res.secondary_file_dict
+210to_add["name"]=name
+211templates_list.append(to_add)
+212result["templates"]=templates_list
+213
+214# If verbose mode is activated, print the result to the terminal
+215ifself.cop_verbose:
+216print('The JSON data that is sent to the Cloud Office Print server:\n')
+217pprint(result)
+218
+219returnresult
+
-
- View Source
-
classPrintJob:
- """A print job for a Cloud Office Print server.
-
- This class contains all configuration options, resources, render elements ...
- and the `PrintJob.execute` method to combine all these and send a request to the Cloud Office Print server.
- """
-
- def__init__(self,
- data:Union[Element,Mapping[str,Element],RESTSource],
- server:Server,
- template:Resource=None,
- output_config:OutputConfig=OutputConfig(),
- subtemplates:Dict[str,Resource]={},
- prepend_files:List[Resource]=[],
- append_files:List[Resource]=[],
- cop_verbose:bool=False):
- """
- Args:
- data (Union[Element, Mapping[str, Element], RESTSource]): This is either: An `Element` (e.g. an `ElementCollection`); A mapping, containing file names as keys and an `Element` as data. Multiple files will be produced from the different datas, the result is a zip file containing them. In the first case, no output file name is specified and the server will name it "file0".
- server (Server): Server to be used for this print job.
- template (Resource): Template to use for this print job.
- output_config (OutputConfig, optional): Output configuration to be used for this print job. Defaults to `OutputConfig`().
- subtemplates (Dict[str, Resource], optional): Subtemplates for this print job, accessible (in docx) through `{?include subtemplate_dict_key}`. Defaults to {}.
- prepend_files (List[Resource], optional): Files to prepend to the output file. Defaults to [].
- append_files (List[Resource], optional): Files to append to the output file. Defaults to [].
- cop_verbose (bool, optional): Whether or not verbose mode should be activated. Defaults to False.
- """
-
- self.data:Union[Element,Mapping[str,Element],RESTSource]=data
- self.server:Server=server
- self.output_config:OutputConfig=output_config
- self.template:Resource=template
- self.subtemplates:Dict[str,Resource]=subtemplates
- self.prepend_files:List[Resource]=prepend_files
- self.append_files:List[Resource]=append_files
- self.cop_verbose:bool=cop_verbose
-
- defexecute(self)->Response:
- """Execute this print job.
-
- Returns:
- Response: `Response`-object
- """
- self.server._raise_if_unreachable()
- returnself._handle_response(requests.post(self.server.url,proxies=self.server.config.proxiesifself.server.configisnotNoneelseNone,json=self.as_dict,headers={"Content-type":"application/json"}))
-
- asyncdefexecute_async(self)->Response:
- """Async version of `PrintJob.execute`
-
- Returns:
- Response: `Response`-object
- """
- self.server._raise_if_unreachable()
- returnPrintJob._handle_response(
- awaitasyncio.get_event_loop().run_in_executor(
- None,partial(
- requests.post,
- self.server.url,
- proxies=self.server.config.proxiesifself.server.configisnotNoneelseNone,
- json=self.as_dict
- )
- )
- )
-
- @staticmethod
- defexecute_full_json(json_data:str,server:Server)->Response:
- """If you already have the JSON to be sent to the server (not just the data, but the entire JSON body including your API key and template), this package will wrap the request to the server.
-
- Args:
- json_data (str): full JSON data that needs to be sent to a Cloud Office Print server
- server (Server): `Server`-object
-
- Returns:
- Response: `Response`-object
- """
- server._raise_if_unreachable()
- returnPrintJob._handle_response(requests.post(server.url,proxies=server.config.proxiesifserver.configisnotNoneelseNone,data=json_data,headers={"Content-type":"application/json"}))
-
- @staticmethod
- asyncdefexecute_full_json_async(json_data:str,server:Server)->Response:
- """Async version of `Printjob.execute_full_json`
-
- Args:
- json_data (str): full JSON data that needs to be sent to a Cloud Office Print server
- server (Server): `Server`-object
-
- Returns:
- Response: `Response`-object
- """
- server._raise_if_unreachable()
- returnPrintJob._handle_response(
- awaitasyncio.get_event_loop().run_in_executor(
- None,partial(
- requests.post,
- server.url,
- proxies=server.config.proxiesifserver.configisnotNoneelseNone,
- data=json_data,
- headers={"Content-type":"application/json"}
- )
- )
- )
-
- @staticmethod
- def_handle_response(res:requests.Response)->Response:
- """Converts the HTML response to a `Response`-object
-
- Args:
- res (requests.Response): HTML response from the Cloud Office Print server
-
- Raises:
- COPError: Error when the HTML status code is not 200
-
- Returns:
- Response: `Response`-object of HTML response
- """
- ifres.status_code!=200:
- raiseCOPError(res.text)
- else:
- returnResponse(res)
-
- @property
- defjson(self)->str:
- """JSON equivalent of the dict representation of this print job.
- This representation is isomorphic to the dict representation `Printjob.as_dict`.
-
- Returns:
- str: JSON equivalent of the dict representation of this print job
- """
- returnjson.dumps(self.as_dict)
-
- @property
- defas_dict(self)->Dict:
- """Return the dict representation of this print job.
-
- Returns:
- Dict: dict representation of this print job
- """
- result=dict(
- STATIC_OPTS)# Copy of STATIC_OPTS! Otherwise everything we add to 'result' will also be added to 'STATIC_OPTS'
- # server config goes in the upper level
- ifself.server.config:
- result.update(self.server.config.as_dict)
-
- # output config goes in "output"
- # and decides where its sub-configs go through its as_dict property
- # (e.g. PDFConfigs are just appended at this "output" level)
- result["output"]=self.output_config.as_dict
-
- ifself.template:
- result["template"]=self.template.template_dict
-
- # If output_type is not specified, set this to the template filetype
- # If no template found: default docx
- if'output_type'notinself.output_config.as_dict.keys():
- ifself.template:
- result['output']['output_type']=result['template']['template_type']
- else:
- result['output']['output_type']='docx'
-
- ifisinstance(self.data,Mapping):
- result["files"]=[{
- "filename":name,
- "data":data.as_dict
- }forname,datainself.data.items()]
- elifisinstance(self.data,RESTSource):
- result['files']=[self.data.as_dict]
- else:
- result["files"]=[{"data":self.data.as_dict}]
-
- iflen(self.prepend_files)>0:
- result["prepend_files"]=[
- res.secondary_file_dictforresinself.prepend_files
- ]
-
- iflen(self.append_files)>0:
- result["append_files"]=[
- res.secondary_file_dictforresinself.append_files
- ]
-
- iflen(self.subtemplates)>0:
- templates_list=[]
- forname,resinself.subtemplates.items():
- to_add=res.secondary_file_dict
- to_add["name"]=name
- templates_list.append(to_add)
- result["templates"]=templates_list
-
- # If verbose mode is activated, print the result to the terminal
- ifself.cop_verbose:
- print('The JSON data that is sent to the Cloud Office Print server:\n')
- pprint(result)
-
- returnresult
-
34def__init__(self,
+35data:Union[Element,Mapping[str,Element],RESTSource],
+36server:Server,
+37template:Resource=None,
+38output_config:OutputConfig=OutputConfig(),
+39subtemplates:Dict[str,Resource]={},
+40prepend_files:List[Resource]=[],
+41append_files:List[Resource]=[],
+42cop_verbose:bool=False):
+43"""
+44 Args:
+45 data (Union[Element, Mapping[str, Element], RESTSource]): This is either: An `Element` (e.g. an `ElementCollection`); A mapping, containing file names as keys and an `Element` as data. Multiple files will be produced from the different datas, the result is a zip file containing them. In the first case, no output file name is specified and the server will name it "file0".
+46 server (Server): Server to be used for this print job.
+47 template (Resource): Template to use for this print job.
+48 output_config (OutputConfig, optional): Output configuration to be used for this print job. Defaults to `OutputConfig`().
+49 subtemplates (Dict[str, Resource], optional): Subtemplates for this print job, accessible (in docx) through `{?include subtemplate_dict_key}`. Defaults to {}.
+50 prepend_files (List[Resource], optional): Files to prepend to the output file. Defaults to [].
+51 append_files (List[Resource], optional): Files to append to the output file. Defaults to [].
+52 cop_verbose (bool, optional): Whether or not verbose mode should be activated. Defaults to False.
+53 """
+54
+55self.data:Union[Element,Mapping[str,Element],RESTSource]=data
+56self.server:Server=server
+57self.output_config:OutputConfig=output_config
+58self.template:Resource=template
+59self.subtemplates:Dict[str,Resource]=subtemplates
+60self.prepend_files:List[Resource]=prepend_files
+61self.append_files:List[Resource]=append_files
+62self.cop_verbose:bool=cop_verbose
+
-
- View Source
-
def__init__(self,
- data:Union[Element,Mapping[str,Element],RESTSource],
- server:Server,
- template:Resource=None,
- output_config:OutputConfig=OutputConfig(),
- subtemplates:Dict[str,Resource]={},
- prepend_files:List[Resource]=[],
- append_files:List[Resource]=[],
- cop_verbose:bool=False):
- """
- Args:
- data (Union[Element, Mapping[str, Element], RESTSource]): This is either: An `Element` (e.g. an `ElementCollection`); A mapping, containing file names as keys and an `Element` as data. Multiple files will be produced from the different datas, the result is a zip file containing them. In the first case, no output file name is specified and the server will name it "file0".
- server (Server): Server to be used for this print job.
- template (Resource): Template to use for this print job.
- output_config (OutputConfig, optional): Output configuration to be used for this print job. Defaults to `OutputConfig`().
- subtemplates (Dict[str, Resource], optional): Subtemplates for this print job, accessible (in docx) through `{?include subtemplate_dict_key}`. Defaults to {}.
- prepend_files (List[Resource], optional): Files to prepend to the output file. Defaults to [].
- append_files (List[Resource], optional): Files to append to the output file. Defaults to [].
- cop_verbose (bool, optional): Whether or not verbose mode should be activated. Defaults to False.
- """
-
- self.data:Union[Element,Mapping[str,Element],RESTSource]=data
- self.server:Server=server
- self.output_config:OutputConfig=output_config
- self.template:Resource=template
- self.subtemplates:Dict[str,Resource]=subtemplates
- self.prepend_files:List[Resource]=prepend_files
- self.append_files:List[Resource]=append_files
- self.cop_verbose:bool=cop_verbose
-
-
-
Args:
data (Union[Element, Mapping[str, Element], RESTSource]): This is either: An Element (e.g. an ElementCollection); A mapping, containing file names as keys and an Element as data. Multiple files will be produced from the different datas, the result is a zip file containing them. In the first case, no output file name is specified and the server will name it "file0".
@@ -838,26 +650,26 @@
@staticmethod
- defexecute_full_json(json_data:str,server:Server)->Response:
- """If you already have the JSON to be sent to the server (not just the data, but the entire JSON body including your API key and template), this package will wrap the request to the server.
-
- Args:
- json_data (str): full JSON data that needs to be sent to a Cloud Office Print server
- server (Server): `Server`-object
-
- Returns:
- Response: `Response`-object
- """
- server._raise_if_unreachable()
- returnPrintJob._handle_response(requests.post(server.url,proxies=server.config.proxiesifserver.configisnotNoneelseNone,data=json_data,headers={"Content-type":"application/json"}))
-
+
+
+
91@staticmethod
+ 92defexecute_full_json(json_data:str,server:Server)->Response:
+ 93"""If you already have the JSON to be sent to the server (not just the data, but the entire JSON body including your API key and template), this package will wrap the request to the server.
+ 94
+ 95 Args:
+ 96 json_data (str): full JSON data that needs to be sent to a Cloud Office Print server
+ 97 server (Server): `Server`-object
+ 98
+ 99 Returns:
+100 Response: `Response`-object
+101 """
+102server._raise_if_unreachable()
+103returnPrintJob._handle_response(requests.post(server.url,proxies=server.config.proxiesifserver.configisnotNoneelseNone,data=json_data,headers={"Content-type":"application/json"}))
+
-
If you already have the JSON to be sent to the server (not just the data, but the entire JSON body including your API key and template), this package will wrap the request to the server.
105@staticmethod
+106asyncdefexecute_full_json_async(json_data:str,server:Server)->Response:
+107"""Async version of `Printjob.execute_full_json`
+108
+109 Args:
+110 json_data (str): full JSON data that needs to be sent to a Cloud Office Print server
+111 server (Server): `Server`-object
+112
+113 Returns:
+114 Response: `Response`-object
+115 """
+116server._raise_if_unreachable()
+117returnPrintJob._handle_response(
+118awaitasyncio.get_event_loop().run_in_executor(
+119None,partial(
+120requests.post,
+121server.url,
+122proxies=server.config.proxiesifserver.configisnotNoneelseNone,
+123data=json_data,
+124headers={"Content-type":"application/json"}
+125)
+126)
+127)
+
-
- View Source
-
@staticmethod
- asyncdefexecute_full_json_async(json_data:str,server:Server)->Response:
- """Async version of `Printjob.execute_full_json`
-
- Args:
- json_data (str): full JSON data that needs to be sent to a Cloud Office Print server
- server (Server): `Server`-object
-
- Returns:
- Response: `Response`-object
- """
- server._raise_if_unreachable()
- returnPrintJob._handle_response(
- awaitasyncio.get_event_loop().run_in_executor(
- None,partial(
- requests.post,
- server.url,
- proxies=server.config.proxiesifserver.configisnotNoneelseNone,
- data=json_data,
- headers={"Content-type":"application/json"}
- )
- )
- )
-
classResource(ABC):
- """The abstract base class for the resources."""
-
- def__init__(self,data:Union[str,bytes]=None,filetype:str=None):
- """
- Args:
- data (Union[str, bytes], optional): the data for this resource. Defaults to None.
- filetype (str, optional): the file type of this resource. Defaults to None.
- """
- self._data:Union[str,bytes]=data
- self.filetype:str=filetype
-
- @property
- defmimetype(self)->str:
- """Resource type as a mime type.
-
- Returns:
- str: resource type as a mime type
- """
- returntype_utils.extension_to_mimetype(self.filetype)
-
- @property
- defdata(self)->Union[str,bytes]:
- """The data contained in this Resource.
-
- Returns:
- Union[str, bytes]: the data contained in this Resource
- """
- returnself._data
-
- @property
- deftemplate_json(self)->str:
- """Get the JSON representation when used as a template.
-
- Returns:
- str: JSON representation of this resource as a template
- """
- returnjson.dumps(self.template_dict)
-
- @property
- @abstractmethod
- deftemplate_dict(self)->Dict:
- """This Resource object as a dict object for use as a template.
- This dict and the template JSON representation (`Resource.template_json`) are isomorphic.
-
- Returns:
- Dict: dict representation of this resource as a template
- """
- pass
-
- @property
- defsecondary_file_json(self)->str:
- """The JSON representation for use as secondary file.
-
- Returns:
- str: JSON representation of this resource as a secondary file
- """
- returnjson.dumps(self.secondary_file_dict)
-
- @property
- @abstractmethod
- defsecondary_file_dict(self)->Dict:
- """This Resource object as a dict object for use as a secondary file (prepend, append, insert, as subtemplate).
- This dict and the "concat file" JSON representation (`Resource.secondary_file_json`) are isomorphic.
-
- Returns:
- Dict: dict representation of this resource as a secondary file
- """
- pass
-
- def__str__(self)->str:
- """Override the string representation of this class to return the template-style json.
-
- Returns:
- str: JSON representation of this resource as a template
- """
- returnself.template_json
-
- @staticmethod
- deffrom_base64(base64string:str,filetype:str)->'Base64Resource':
- """Create a Base64Resource from a base64 string and a file type (extension).
-
- Args:
- base64string (str): base64 encoded string
- filetype (str): file type (extension)
-
- Returns:
- Base64Resource: the created Resource
- """
- returnBase64Resource(base64string,filetype)
-
- @staticmethod
- deffrom_raw(raw_data:bytes,filetype:str)->'RawResource':
- """Create a RawResource from raw file data and a file type (extension).
-
- Args:
- raw_data (bytes): raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object)
- filetype (str): file type (extension)
-
- Returns:
- RawResource: the created Resource
- """
- returnRawResource(raw_data,filetype)
-
- @staticmethod
- deffrom_local_file(local_path:str)->'Base64Resource':
- """Create a Base64Resource with the contents of a local file.
-
- Throws IOError if it can't read the file.
- The filetype is determined by the extension of the file.
-
- Args:
- local_path (str): path to local file
-
- Returns:
- Base64Resource: the created Resource
- """
- base64string:str=file_utils.read_file_as_base64(local_path)
- returnBase64Resource(base64string,type_utils.path_to_extension(local_path))
-
- @staticmethod
- deffrom_server_path(path:str)->'ServerPathResource':
- """Create a ServerPathResource targeting a file on the server.
-
- The filetype is determined by the extension of the file.
-
- Args:
- path (str): location of target file on the server
-
- Returns:
- ServerPathResource: the created Resource
- """
- returnServerPathResource(path)
-
- @staticmethod
- deffrom_url(url:str,filetype:str)->'URLResource':
- """Create an Resource targeting the file at url with given filetype (extension).
-
- Args:
- url (str): file url
- filetype (str): file type (extension)
-
- Returns:
- URLResource: the created Resource
- """
- returnURLResource(url,filetype)
+
+
+
+ class
+ Resource(abc.ABC):
- @staticmethod
- deffrom_html(htmlstring:str,landscape:bool=False)->'HTMLResource':
- """Create an HTMLResource with html data in plain text.
+
- Landscape is not supported for prepend/append sources, only for template resources.
-
- Args:
- htmlstring (str): html content
- landscape (bool, optional): whether to use the landscape option. Defaults to False.
-
- Returns:
- HTMLResource: the created Resource
- """
- returnHTMLResource(htmlstring,landscape)
-
+
+
+
20classResource(ABC):
+ 21"""The abstract base class for the resources."""
+ 22
+ 23def__init__(self,data:Union[str,bytes]=None,filetype:str=None):
+ 24"""
+ 25 Args:
+ 26 data (Union[str, bytes], optional): the data for this resource. Defaults to None.
+ 27 filetype (str, optional): the file type of this resource. Defaults to None.
+ 28 """
+ 29self._data:Union[str,bytes]=data
+ 30self.filetype:str=filetype
+ 31
+ 32@property
+ 33defmimetype(self)->str:
+ 34"""Resource type as a mime type.
+ 35
+ 36 Returns:
+ 37 str: resource type as a mime type
+ 38 """
+ 39returntype_utils.extension_to_mimetype(self.filetype)
+ 40
+ 41@property
+ 42defdata(self)->Union[str,bytes]:
+ 43"""The data contained in this Resource.
+ 44
+ 45 Returns:
+ 46 Union[str, bytes]: the data contained in this Resource
+ 47 """
+ 48returnself._data
+ 49
+ 50@property
+ 51deftemplate_json(self)->str:
+ 52"""Get the JSON representation when used as a template.
+ 53
+ 54 Returns:
+ 55 str: JSON representation of this resource as a template
+ 56 """
+ 57returnjson.dumps(self.template_dict)
+ 58
+ 59@property
+ 60@abstractmethod
+ 61deftemplate_dict(self)->Dict:
+ 62"""This Resource object as a dict object for use as a template.
+ 63 This dict and the template JSON representation (`Resource.template_json`) are isomorphic.
+ 64
+ 65 Returns:
+ 66 Dict: dict representation of this resource as a template
+ 67 """
+ 68pass
+ 69
+ 70@property
+ 71defsecondary_file_json(self)->str:
+ 72"""The JSON representation for use as secondary file.
+ 73
+ 74 Returns:
+ 75 str: JSON representation of this resource as a secondary file
+ 76 """
+ 77returnjson.dumps(self.secondary_file_dict)
+ 78
+ 79@property
+ 80@abstractmethod
+ 81defsecondary_file_dict(self)->Dict:
+ 82"""This Resource object as a dict object for use as a secondary file (prepend, append, insert, as subtemplate).
+ 83 This dict and the "concat file" JSON representation (`Resource.secondary_file_json`) are isomorphic.
+ 84
+ 85 Returns:
+ 86 Dict: dict representation of this resource as a secondary file
+ 87 """
+ 88pass
+ 89
+ 90def__str__(self)->str:
+ 91"""Override the string representation of this class to return the template-style json.
+ 92
+ 93 Returns:
+ 94 str: JSON representation of this resource as a template
+ 95 """
+ 96returnself.template_json
+ 97
+ 98@staticmethod
+ 99deffrom_base64(base64string:str,filetype:str)->'Base64Resource':
+100"""Create a Base64Resource from a base64 string and a file type (extension).
+101
+102 Args:
+103 base64string (str): base64 encoded string
+104 filetype (str): file type (extension)
+105
+106 Returns:
+107 Base64Resource: the created Resource
+108 """
+109returnBase64Resource(base64string,filetype)
+110
+111@staticmethod
+112deffrom_raw(raw_data:bytes,filetype:str)->'RawResource':
+113"""Create a RawResource from raw file data and a file type (extension).
+114
+115 Args:
+116 raw_data (bytes): raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object)
+117 filetype (str): file type (extension)
+118
+119 Returns:
+120 RawResource: the created Resource
+121 """
+122returnRawResource(raw_data,filetype)
+123
+124@staticmethod
+125deffrom_local_file(local_path:str)->'Base64Resource':
+126"""Create a Base64Resource with the contents of a local file.
+127
+128 Throws IOError if it can't read the file.
+129 The filetype is determined by the extension of the file.
+130
+131 Args:
+132 local_path (str): path to local file
+133
+134 Returns:
+135 Base64Resource: the created Resource
+136 """
+137base64string:str=file_utils.read_file_as_base64(local_path)
+138returnBase64Resource(base64string,type_utils.path_to_extension(local_path))
+139
+140@staticmethod
+141deffrom_server_path(path:str)->'ServerPathResource':
+142"""Create a ServerPathResource targeting a file on the server.
+143
+144 The filetype is determined by the extension of the file.
+145
+146 Args:
+147 path (str): location of target file on the server
+148
+149 Returns:
+150 ServerPathResource: the created Resource
+151 """
+152returnServerPathResource(path)
+153
+154@staticmethod
+155deffrom_url(url:str,filetype:str)->'URLResource':
+156"""Create an Resource targeting the file at url with given filetype (extension).
+157
+158 Args:
+159 url (str): file url
+160 filetype (str): file type (extension)
+161
+162 Returns:
+163 URLResource: the created Resource
+164 """
+165returnURLResource(url,filetype)
+166
+167@staticmethod
+168deffrom_html(htmlstring:str,landscape:bool=False)->'HTMLResource':
+169"""Create an HTMLResource with html data in plain text.
+170
+171 Landscape is not supported for prepend/append sources, only for template resources.
+172
+173 Args:
+174 htmlstring (str): html content
+175 landscape (bool, optional): whether to use the landscape option. Defaults to False.
+176
+177 Returns:
+178 HTMLResource: the created Resource
+179 """
+180returnHTMLResource(htmlstring,landscape)
+
def__init__(self,data:Union[str,bytes]=None,filetype:str=None):
- """
- Args:
- data (Union[str, bytes], optional): the data for this resource. Defaults to None.
- filetype (str, optional): the file type of this resource. Defaults to None.
- """
- self._data:Union[str,bytes]=data
- self.filetype:str=filetype
-
+
+
+
23def__init__(self,data:Union[str,bytes]=None,filetype:str=None):
+24"""
+25 Args:
+26 data (Union[str, bytes], optional): the data for this resource. Defaults to None.
+27 filetype (str, optional): the file type of this resource. Defaults to None.
+28 """
+29self._data:Union[str,bytes]=data
+30self.filetype:str=filetype
+
-
Args:
data (Union[str, bytes], optional): the data for this resource. Defaults to None.
@@ -1241,11 +1050,13 @@
This Resource object as a dict object for use as a secondary file (prepend, append, insert, as subtemplate).
This dict and the "concat file" JSON representation (Resource.secondary_file_json) are isomorphic.
@staticmethod
- deffrom_base64(base64string:str,filetype:str)->'Base64Resource':
- """Create a Base64Resource from a base64 string and a file type (extension).
+
- Args:
- base64string (str): base64 encoded string
- filetype (str): file type (extension)
-
- Returns:
- Base64Resource: the created Resource
- """
- returnBase64Resource(base64string,filetype)
-
+
+
+
98@staticmethod
+ 99deffrom_base64(base64string:str,filetype:str)->'Base64Resource':
+100"""Create a Base64Resource from a base64 string and a file type (extension).
+101
+102 Args:
+103 base64string (str): base64 encoded string
+104 filetype (str): file type (extension)
+105
+106 Returns:
+107 Base64Resource: the created Resource
+108 """
+109returnBase64Resource(base64string,filetype)
+
-
Create a Base64Resource from a base64 string and a file type (extension).
@staticmethod
- deffrom_raw(raw_data:bytes,filetype:str)->'RawResource':
- """Create a RawResource from raw file data and a file type (extension).
+
- Args:
- raw_data (bytes): raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object)
- filetype (str): file type (extension)
-
- Returns:
- RawResource: the created Resource
- """
- returnRawResource(raw_data,filetype)
-
+
+
+
111@staticmethod
+112deffrom_raw(raw_data:bytes,filetype:str)->'RawResource':
+113"""Create a RawResource from raw file data and a file type (extension).
+114
+115 Args:
+116 raw_data (bytes): raw data as a [bytes-like object](https://docs.python.org/3/glossary.html#term-bytes-like-object)
+117 filetype (str): file type (extension)
+118
+119 Returns:
+120 RawResource: the created Resource
+121 """
+122returnRawResource(raw_data,filetype)
+
-
Create a RawResource from raw file data and a file type (extension).
@staticmethod
- deffrom_local_file(local_path:str)->'Base64Resource':
- """Create a Base64Resource with the contents of a local file.
-
- Throws IOError if it can't read the file.
- The filetype is determined by the extension of the file.
-
- Args:
- local_path (str): path to local file
+
- Returns:
- Base64Resource: the created Resource
- """
- base64string:str=file_utils.read_file_as_base64(local_path)
- returnBase64Resource(base64string,type_utils.path_to_extension(local_path))
-
+
+
+
124@staticmethod
+125deffrom_local_file(local_path:str)->'Base64Resource':
+126"""Create a Base64Resource with the contents of a local file.
+127
+128 Throws IOError if it can't read the file.
+129 The filetype is determined by the extension of the file.
+130
+131 Args:
+132 local_path (str): path to local file
+133
+134 Returns:
+135 Base64Resource: the created Resource
+136 """
+137base64string:str=file_utils.read_file_as_base64(local_path)
+138returnBase64Resource(base64string,type_utils.path_to_extension(local_path))
+
-
Create a Base64Resource with the contents of a local file.
@staticmethod
- deffrom_server_path(path:str)->'ServerPathResource':
- """Create a ServerPathResource targeting a file on the server.
+ def
+ from_server_path(path:str) -> cloudofficeprint.resource.ServerPathResource:
- The filetype is determined by the extension of the file.
+
- Args:
- path (str): location of target file on the server
-
- Returns:
- ServerPathResource: the created Resource
- """
- returnServerPathResource(path)
-
+
+
+
140@staticmethod
+141deffrom_server_path(path:str)->'ServerPathResource':
+142"""Create a ServerPathResource targeting a file on the server.
+143
+144 The filetype is determined by the extension of the file.
+145
+146 Args:
+147 path (str): location of target file on the server
+148
+149 Returns:
+150 ServerPathResource: the created Resource
+151 """
+152returnServerPathResource(path)
+
-
Create a ServerPathResource targeting a file on the server.
@staticmethod
- deffrom_url(url:str,filetype:str)->'URLResource':
- """Create an Resource targeting the file at url with given filetype (extension).
+ def
+ from_url(url:str, filetype:str) -> cloudofficeprint.resource.URLResource:
- Args:
- url (str): file url
- filetype (str): file type (extension)
+
- Returns:
- URLResource: the created Resource
- """
- returnURLResource(url,filetype)
-
+
+
+
154@staticmethod
+155deffrom_url(url:str,filetype:str)->'URLResource':
+156"""Create an Resource targeting the file at url with given filetype (extension).
+157
+158 Args:
+159 url (str): file url
+160 filetype (str): file type (extension)
+161
+162 Returns:
+163 URLResource: the created Resource
+164 """
+165returnURLResource(url,filetype)
+
-
Create an Resource targeting the file at url with given filetype (extension).
@staticmethod
- deffrom_html(htmlstring:str,landscape:bool=False)->'HTMLResource':
- """Create an HTMLResource with html data in plain text.
+ def
+ from_html(htmlstring:str,landscape:bool=False) -> cloudofficeprint.resource.HTMLResource:
- Landscape is not supported for prepend/append sources, only for template resources.
+
- Args:
- htmlstring (str): html content
- landscape (bool, optional): whether to use the landscape option. Defaults to False.
-
- Returns:
- HTMLResource: the created Resource
- """
- returnHTMLResource(htmlstring,landscape)
-
+
+
+
167@staticmethod
+168deffrom_html(htmlstring:str,landscape:bool=False)->'HTMLResource':
+169"""Create an HTMLResource with html data in plain text.
+170
+171 Landscape is not supported for prepend/append sources, only for template resources.
+172
+173 Args:
+174 htmlstring (str): html content
+175 landscape (bool, optional): whether to use the landscape option. Defaults to False.
+176
+177 Returns:
+178 HTMLResource: the created Resource
+179 """
+180returnHTMLResource(htmlstring,landscape)
+
-
Create an HTMLResource with html data in plain text.
11classResponse():
+12"""The Response class serves as a container for and interface with the Cloud Office Print server's response to a printjob request.
+13
+14 The Cloud Office Print server can also throw an error, in which case you will be dealing with a cloudofficeprint.exceptions.COPError instead of this class.
+15 """
+16
+17def__init__(self,response:requests.Response):
+18"""You should never need to construct a Response manually.
+19
+20 Args:
+21 response (requests.Response): Response object from the requests package
+22 """
+23self._mimetype=response.headers["Content-Type"]
+24self._bytes=response.content
+25
+26@property
+27defmimetype(self)->str:
+28"""Mime type of this response.
+29
+30 Returns:
+31 str: mime type of this response
+32 """
+33returnself._mimetype
+34
+35@property
+36deffiletype(self)->str:
+37"""File type (extension) of this response. E.g. "docx".
+38
+39 Returns:
+40 str: file type of this response
+41 """
+42returntype_utils.mimetype_to_extension(self.mimetype)
+43
+44@property
+45defbinary(self)->bytes:
+46"""Binary representation of the output file.
+47
+48 Response.to_file can be used to output to a file,
+49 alternatively, use this property to do something else with the binary data.
+50
+51 Returns:
+52 bytes: response file as binary
+53 """
+54returnself._bytes
+55
+56defto_string(self)->str:
+57"""Return the string representation of this buffer.
+58 Useful if the server returns a JSON (e.g. for output_type 'count_tags').
+59
+60 Raises:
+61 err: raise error is bytes cannot be decoded in utf-8
+62
+63 Returns:
+64 str: string representation of this buffer
+65 """
+66try:
+67returnself._bytes.decode('utf-8')
+68exceptUnicodeDecodeErroraserr:
+69print("""The method 'to_string()' cannot be called on this object.
+70 The server response is probably not a string (e.g. JSON).
+71 To get the bytes of the response, use the property 'binary' instead.""")
+72raiseerr
+73
+74defto_file(self,path:str):
+75"""Write the response to a file at the given path without extension.
+76
+77 If the given file path does not contain an extension,
+78 the correct path is automatically added from the response data.
+79 That is how this method is intended to be used.
+80 You should only specify the extension in the path if you have some reason to specify the extension manually.
+81
+82 Args:
+83 path (str): path without extension
+84 """
+85
+86ifnotsplitext(path)[1]:
+87path+="."+self.filetype
+88
+89# open the file in binary ("b") and write ("w") mode
+90outfile=open(path,"wb")
+91outfile.write(self.binary)
+92outfile.close()
+
-
- View Source
-
classResponse():
- """The Response class serves as a container for and interface with the Cloud Office Print server's response to a printjob request.
-
- The Cloud Office Print server can also throw an error, in which case you will be dealing with a cloudofficeprint.exceptions.COPError instead of this class.
- """
-
- def__init__(self,response:requests.Response):
- """You should never need to construct a Response manually.
-
- Args:
- response (requests.Response): Response object from the requests package
- """
- self._mimetype=response.headers["Content-Type"]
- self._bytes=response.content
-
- @property
- defmimetype(self)->str:
- """Mime type of this response.
-
- Returns:
- str: mime type of this response
- """
- returnself._mimetype
-
- @property
- deffiletype(self)->str:
- """File type (extension) of this response. E.g. "docx".
-
- Returns:
- str: file type of this response
- """
- returntype_utils.mimetype_to_extension(self.mimetype)
-
- @property
- defbinary(self)->bytes:
- """Binary representation of the output file.
-
- Response.to_file can be used to output to a file,
- alternatively, use this property to do something else with the binary data.
-
- Returns:
- bytes: response file as binary
- """
- returnself._bytes
-
- defto_string(self)->str:
- """Return the string representation of this buffer.
- Useful if the server returns a JSON (e.g. for output_type 'count_tags').
-
- Raises:
- err: raise error is bytes cannot be decoded in utf-8
-
- Returns:
- str: string representation of this buffer
- """
- try:
- returnself._bytes.decode('utf-8')
- exceptUnicodeDecodeErroraserr:
- print("""The method 'to_string()' cannot be called on this object.
- The server response is probably not a string (e.g. JSON).
- To get the bytes of the response, use the property 'binary' instead.""")
- raiseerr
-
- defto_file(self,path:str):
- """Write the response to a file at the given path without extension.
-
- If the given file path does not contain an extension,
- the correct path is automatically added from the response data.
- That is how this method is intended to be used.
- You should only specify the extension in the path if you have some reason to specify the extension manually.
-
- Args:
- path (str): path without extension
- """
-
- ifnotsplitext(path)[1]:
- path+="."+self.filetype
-
- # open the file in binary ("b") and write ("w") mode
- outfile=open(path,"wb")
- outfile.write(self.binary)
- outfile.close()
-
-
-
The Response class serves as a container for and interface with the Cloud Office Print server's response to a printjob request.
def__init__(self,response:requests.Response):
- """You should never need to construct a Response manually.
-
- Args:
- response (requests.Response): Response object from the requests package
- """
- self._mimetype=response.headers["Content-Type"]
- self._bytes=response.content
-
+
+
+
17def__init__(self,response:requests.Response):
+18"""You should never need to construct a Response manually.
+19
+20 Args:
+21 response (requests.Response): Response object from the requests package
+22 """
+23self._mimetype=response.headers["Content-Type"]
+24self._bytes=response.content
+
-
You should never need to construct a Response manually.
56defto_string(self)->str:
+57"""Return the string representation of this buffer.
+58 Useful if the server returns a JSON (e.g. for output_type 'count_tags').
+59
+60 Raises:
+61 err: raise error is bytes cannot be decoded in utf-8
+62
+63 Returns:
+64 str: string representation of this buffer
+65 """
+66try:
+67returnself._bytes.decode('utf-8')
+68exceptUnicodeDecodeErroraserr:
+69print("""The method 'to_string()' cannot be called on this object.
+70 The server response is probably not a string (e.g. JSON).
+71 To get the bytes of the response, use the property 'binary' instead.""")
+72raiseerr
+
-
- View Source
-
defto_string(self)->str:
- """Return the string representation of this buffer.
- Useful if the server returns a JSON (e.g. for output_type 'count_tags').
-
- Raises:
- err: raise error is bytes cannot be decoded in utf-8
-
- Returns:
- str: string representation of this buffer
- """
- try:
- returnself._bytes.decode('utf-8')
- exceptUnicodeDecodeErroraserr:
- print("""The method 'to_string()' cannot be called on this object.
- The server response is probably not a string (e.g. JSON).
- To get the bytes of the response, use the property 'binary' instead.""")
- raiseerr
-
-
-
Return the string representation of this buffer.
Useful if the server returns a JSON (e.g. for output_type 'count_tags').
defto_file(self,path:str):
- """Write the response to a file at the given path without extension.
-
- If the given file path does not contain an extension,
- the correct path is automatically added from the response data.
- That is how this method is intended to be used.
- You should only specify the extension in the path if you have some reason to specify the extension manually.
-
- Args:
- path (str): path without extension
- """
+
- ifnotsplitext(path)[1]:
- path+="."+self.filetype
-
- # open the file in binary ("b") and write ("w") mode
- outfile=open(path,"wb")
- outfile.write(self.binary)
- outfile.close()
-
+
+
+
74defto_file(self,path:str):
+75"""Write the response to a file at the given path without extension.
+76
+77 If the given file path does not contain an extension,
+78 the correct path is automatically added from the response data.
+79 That is how this method is intended to be used.
+80 You should only specify the extension in the path if you have some reason to specify the extension manually.
+81
+82 Args:
+83 path (str): path without extension
+84 """
+85
+86ifnotsplitext(path)[1]:
+87path+="."+self.filetype
+88
+89# open the file in binary ("b") and write ("w") mode
+90outfile=open(path,"wb")
+91outfile.write(self.binary)
+92outfile.close()
+
-
Write the response to a file at the given path without extension.
The classes under this module encapsulate various configuration options for a print job.
-They are to be used with cloudofficeprint.printjob.PrintJob.
+They are to be used with cloudofficeprint.printjob.PrintJob.
-
- View Source
-
"""
-Module for output configurations.
+
-The classes under this module encapsulate various configuration options for a print job.
-They are to be used with `cloudofficeprint.printjob.PrintJob`.
-"""
+
-from.cloudimport*
-from.csvimport*
-from.outputimport*
-from.pdfimport*
-from.serverimport*
-
+
1"""
+ 2Module for output configurations.
+ 3
+ 4The classes under this module encapsulate various configuration options for a print job.
+ 5They are to be used with `cloudofficeprint.printjob.PrintJob`.
+ 6"""
+ 7
+ 8from.cloudimport*
+ 9from.csvimport*
+10from.outputimport*
+11from.pdfimport*
+12from.serverimport*
+13from.request_optionimport*
+
import json
-from typing import Dict, List
-from abc import ABC, abstractmethod
-
-__all__ = [
- "CloudAccessToken",
- "OAuthToken",
- "AWSToken",
- "FTPToken"
-]
-
-SERVICES = [
- "dropbox",
- "gdrive",
- "onedrive",
- "aws_s3",
- "sftp",
- "ftp"
-]
-
-
-class CloudAccessToken(ABC):
- """Abstract base class for classes used to specify cloud access information for outputting to a cloud service."""
-
- def __init__(self, service: str):
- """
- Args:
- service (str): name of the cloud service
-
- Raises:
- ValueError: raise error if the given name for the cloud service is not known
- """
- if not self.is_valid_service(service):
- raise ValueError(f'Unsupported cloud service "{service}".')
- self._service = service
-
- @property
- def service(self) -> str:
- """Returns which cloud service is being used.
-
- Returns:
- str: which cloud service is being used
- """
- return self._service
-
- @service.setter
- def service(self, value: str):
- """Setter for self._service
-
- Args:
- value (str): new value for self._service
-
- Raises:
- ValueError: raise error if the given name for the cloud service is not known
- """
- if not self.is_valid_service(value):
- raise ValueError(f'Unsupported cloud service "{value}".')
- self._service = value
-
- @property
- @abstractmethod
- def as_dict(self) -> Dict:
- """The cloud access token as a dict, for building the JSON.
-
- Returns:
- Dict: dict representation for this cloud access token
- """
- return {
- "output_location": self.service
- }
-
- @property
- def json(self) -> str:
- """The cloud access token as JSON.
-
- Returns:
- str: JSON representation for this cloud access token
- """
- return json.dumps(self.as_dict)
-
- @staticmethod
- def is_valid_service(value: str) -> bool:
- """Check if the given value is a valid service string.
-
- Args:
- value (str): the service to check
-
- Returns:
- bool: whether value is valid
- """
- return value in SERVICES
-
- @staticmethod
- def list_available_services() -> List[str]:
- """List all available services.
-
- Returns:
- List[str]: list of available service strings
- """
- return SERVICES
-
- @staticmethod
- def from_OAuth(service: str, token: str) -> 'OAuthToken':
- """Create a token from an OAuth string and service name.
-
- Args:
- service (str): cloud service
- token (str): OAuth access token
-
- Returns:
- OAuthToken: created token
- """
- return OAuthToken(service, token)
-
- @staticmethod
- def from_AWS(key_id: str, secret_key: str):
- """Create a token from Amazon S3 access key id and secret access key.
-
- Args:
- key_id (str): AWS access key ID
- secret_key (str): AWS secret access key
-
- Returns:
- AWSToken: created token
- """
- return AWSToken(key_id, secret_key)
-
- @staticmethod
- def from_FTP(host: str, port: int = None, user: str = None, password: str = None) -> 'FTPToken':
- """Create a token from FTP info
-
- When an argument is / defaults to None, no data about it is sent to the Cloud Office Print server.
- The Cloud Office Print server will then fill in default values.
-
- Args:
- host (str): host name or IP address
- port (int, optional): port to use. Defaults to None.
- user (str, optional): user name. Defaults to None.
- password (str, optional): password for user. Defaults to None.
-
- Returns:
- FTPToken: created token
- """
- return FTPToken(host, False, port, user, password)
-
- @staticmethod
- def from_SFTP(host: str, port: int = None, user: str = None, password: str = None) -> 'FTPToken':
- """Create a token from SFTP info
-
- When an argument is / defaults to None, no data about it is sent to the Cloud Office Print server.
- The Cloud Office Print server will then fill in default values.
-
- Args:
- host (str): host name or IP address
- port (int, optional): port to use. Defaults to None.
- user (str, optional): user name. Defaults to None.
- password (str, optional): password for user. Defaults to None.
-
- Returns:
- FTPToken: created token
- This is an FTPToken object, with sftp=True passed into the constructor.
- The only difference with FTP is CloudAccessToken.servicename.
- """
- return FTPToken(host, True, port, user, password)
-
-
-class OAuthToken(CloudAccessToken):
- """`CloudAccessToken` to be used for OAuth tokens"""
-
- def __init__(self, service: str, token: str):
- """
- Args:
- service (str): `CloudAccessToken.service`
- token (str): OAuth token
- """
- super().__init__(service)
- self.token: str = token
-
- @property
- def as_dict(self) -> Dict:
- result = super().as_dict
- result.update({
- "cloud_access_token": self.token
- })
- return result
-
-
-class AWSToken(CloudAccessToken):
- """`CloudAccessToken` to be used for AWS tokens"""
-
- def __init__(self, key_id: str, secret_key: str):
- """
- Args:
- key_id (str): AWS access key ID
- secret_key (str): AWS secret key
- """
- super().__init__("aws_s3")
- self.key_id: str = key_id
- self.secret_key: str = secret_key
-
- @property
- def as_dict(self) -> Dict:
- result = super().as_dict
- result.update({
- "cloud_access_token": {
- "access_key": self.key_id,
- "secret_access_key": self.secret_key
- }
- })
- return result
-
-
-class FTPToken(CloudAccessToken):
- """`CloudAccessToken` to be used for FTP/SFTP tokens"""
-
- def __init__(self, host: str, sftp: bool = False, port: int = None, user: str = None, password: str = None):
- """
- Args:
- host (str): Host name or IP address of the FTP/SFTP server.
- sftp (bool, optional): whether to use SFTP (else FTP). Defaults to False.
- port (int, optional): Port number of the FTP/SFTP server. Defaults to None.
- user (str, optional): User name for the FTP/SFTP server. Defaults to None.
- password (str, optional): Password for the user. Defaults to None.
- """
- super().__init__("sftp" if sftp else "ftp")
- self.host: str = host
- self.port: int = port
- self.user: str = user
- self.password: str = password
-
- @property
- def as_dict(self) -> Dict:
- cloud_access_token = {
- "host": self.host
- }
- if self.port is not None:
- cloud_access_token["port"] = self.port
- if self.user is not None:
- cloud_access_token["user"] = self.user
- if self.password is not None:
- cloud_access_token["password"] = self.password
-
- result = super().as_dict
- result.update({
- "cloud_access_token": cloud_access_token
- })
-
- return result
When an argument is / defaults to None, no data about it is sent to the Cloud Office Print server.
-The Cloud Office Print server will then fill in default values.
+
Create a token from Amazon S3 access key id and secret access key.
Create a token from an OAuth string and service name.
+
Create a token from FTP info
+
When an argument is / defaults to None, no data about it is sent to the Cloud Office Print server.
+The Cloud Office Print server will then fill in default values.
When an argument is / defaults to None, no data about it is sent to the Cloud Office Print server.
-The Cloud Office Print server will then fill in default values.
+
Create a token from an OAuth string and service name.
Check if the given value is a valid service string.
+
Create a token from SFTP info
+
When an argument is / defaults to None, no data about it is sent to the Cloud Office Print server.
+The Cloud Office Print server will then fill in default values.
The properties of this class define all possible PDF output options.
All of them are optional, which is why passing an instance of this class in an OutputConfig is also optional.
@@ -125,85 +136,10 @@
class CsvOptions:
- """Class of optional PDF options.
-
- The properties of this class define all possible PDF output options.
- All of them are optional, which is why passing an instance of this class in an OutputConfig is also optional.
- These options can be used when the template is xlsx and the output is csv.
- """
-
- def __init__(
- self,
- text_delimiter: str = None,
- field_separator: str = None,
- character_set: int = None,
- ):
- """
- Args:
- text_delimiter (str, optional): this option will specify the text delimiter. Can be " or ' (default "). Defaults to None.
- field_separator (str, optional): this option will specify the field separator. Default ,.
- Can be any ascii character or 'tab' for tab and 'space' for space. Defaults to None.
- character_set (str, optional): this option will determine the character set. Should be an integer.
- See: https://wiki.openoffice.org/wiki/Documentation/DevGuide/Spreadsheets/Filter_Options#Filter_Options_for_Lotus.2C_dBase_and_DIF_Filters
- for possible values. Default 0 or system encoding. Defaults to None.
- """
- self.text_delimiter: str = text_delimiter
- self.field_separator: str = field_separator
- self.character_set: int = character_set
-
- def __str__(self) -> str:
- """Get the string representation of these csv options.
-
- Returns:
- str: string representation of these csv options
- """
- return self.json
-
- @property
- def json(self) -> str:
- """The JSON representation of these csv options.
-
- The JSON representation is a direct JSON dump of the dict representation.
- The dict representation is accessed through the `as_dict` property.
-
- Returns:
- str: JSON representation of these csv options
- """
- return json.dumps(self.as_dict)
-
- @property
- def as_dict(self) -> Dict:
- """The dict representation of these csv options.
-
- Returns:
- Dict: the dict representation of these csv options
- """
- result = {}
-
- if self.text_delimiter is not None:
- result['output_text_delimiter'] = self.text_delimiter
- if self.field_separator is not None:
- result['output_field_separator'] = self.field_separator
- if self.character_set is not None:
- result['output_character_set'] = self.character_set
-
- return result
-
Instance variables
-
var as_dict :Â Dict
+
prop as_dict :Â Dict
-
The dict representation of these csv options.
-
Returns
-
-
Dict
-
the dict representation of these csv options
-
Expand source code
@@ -226,17 +162,15 @@
Returns
return result
-
-
var json :Â str
-
-
The JSON representation of these csv options.
-
The JSON representation is a direct JSON dump of the dict representation.
-The dict representation is accessed through the as_dict property.
+
The dict representation of these csv options.
Returns
-
str
-
JSON representation of these csv options
+
Dict
+
the dict representation of these csv options
+
+
prop json :Â str
+
Expand source code
@@ -253,6 +187,14 @@
Returns
"""
return json.dumps(self.as_dict)
+
The JSON representation of these csv options.
+
The JSON representation is a direct JSON dump of the dict representation.
+The dict representation is accessed through the as_dict property.
-
\ No newline at end of file
+
diff --git a/docs/cloudofficeprint/config/index.html b/docs/cloudofficeprint/config/index.html
index 0e317eb..7f68f0f 100644
--- a/docs/cloudofficeprint/config/index.html
+++ b/docs/cloudofficeprint/config/index.html
@@ -2,18 +2,32 @@
-
-
+
+
cloudofficeprint.config API documentation
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
@@ -25,23 +39,6 @@
Module cloudofficeprint.config
Module for output configurations.
The classes under this module encapsulate various configuration options for a print job.
They are to be used with PrintJob.
-
-
-Expand source code
-
-
"""
-Module for output configurations.
-
-The classes under this module encapsulate various configuration options for a print job.
-They are to be used with `cloudofficeprint.printjob.PrintJob`.
-"""
-
-from .cloud import *
-from .csv import *
-from .output import *
-from .pdf import *
-from .server import *