BeyondCV 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ BASE_TEMPLATE = {
2
+ "name": "Name",
3
+ "title": "Title",
4
+ "profile_summary": "summary",
5
+ "education": [
6
+ {"institute": "institute name", "degree": "degree name", "year": "year"}
7
+ ],
8
+ "experience": [
9
+ {
10
+ "organisation": "organisation name",
11
+ "job_title": "job title",
12
+ "job_period": "period",
13
+ "description": ["Experience bullet 1", "Experience bullet 2", "etc..."]
14
+ }
15
+ ],
16
+ "projects": [
17
+ {
18
+ "project_name": "Proj Name",
19
+ "period": "period",
20
+ "description": ["Bullet 1", "Bullet 2", "etc..."]
21
+ }
22
+ ],
23
+ "skill_groups": [
24
+ {"group_name": "Soft Skills", "items": ["Communication", "Problem Solving", "etc..."]},
25
+ {"group_name": "Programming Languages", "items": ["Python", "C", "C++"]},
26
+ {"group_name": "Software Tools", "items": ["CMake", "git", "Docker"]},
27
+ {"group_name": "Other Group", "items": ["item", "etc..."]},
28
+ ],
29
+ "languages": [
30
+ {"language": "Arabic", "proficiency": "Native"}
31
+ ],
32
+ "certifications": ["certification 1", "certification n.."]
33
+ }
34
+
35
+ EXTRA_MODULES = {
36
+ "salary": [
37
+ {
38
+ "key": "salary_expectation",
39
+ "description": "Candidate's stated salary expectation",
40
+ "type": "string"
41
+ }
42
+ ],
43
+ "military_status": [
44
+ {
45
+ "key": "military_status",
46
+ "description": "Candidtate's military status",
47
+ "type": "string"
48
+ }
49
+ ],
50
+ "social": [
51
+ {
52
+ "key": "linkedin",
53
+ "description": "LinkedIn profile URL",
54
+ "type": "string"
55
+ },
56
+ {
57
+ "key": "github",
58
+ "description": "GitHub profile URL",
59
+ "type": "string"
60
+ },
61
+ {
62
+ "key": "email",
63
+ "description": "Email",
64
+ "type": "string"
65
+ }
66
+ ]
67
+ }
68
+
69
+
70
+ def build_extra_fields_text(modules: list[str]) -> str:
71
+ """Builds the extra instruction block to append to the prompt."""
72
+ if not modules:
73
+ return ""
74
+
75
+ fields = [f for m in modules for f in EXTRA_MODULES.get(m, [])]
76
+ if not fields:
77
+ return ""
78
+
79
+ lines = ["Also extract the following additional fields:"]
80
+ for f in fields:
81
+ lines.append(f"- \"{f['key']}\": {f['description']} (type: {f['type']})")
82
+ return "\n".join(lines)
@@ -0,0 +1,87 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ from BeyondCV.LLM.utils import safe_parse_json, load_prompt
7
+ from BeyondCV.config import bcv_config as cfg
8
+
9
+ _always_use_cache: bool = bool(cfg.use_cache) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
10
+
11
+ class LLMInvoker(ABC):
12
+ def __init__(self, path_to_pdf: str | Path, modules: list[str] | None = None) -> None:
13
+ self.pdf_path: str | Path = path_to_pdf
14
+ self.file_name: str = Path(path_to_pdf).stem
15
+
16
+ archive_path = self.get_default_archive_path()
17
+ if archive_path.exists():
18
+ if _always_use_cache:
19
+ use_cache = "yes"
20
+ else:
21
+ use_cache = input(
22
+ f"Archived profile found for '{self.file_name}' at {archive_path}.\nUse cached version? [Y/n]: "
23
+ ).strip().lower()
24
+ if use_cache in ("", "y", "yes"):
25
+ with open(archive_path, "r") as f:
26
+ self.result_json = json.load(f)
27
+ self.result_archive: str | Path = str(archive_path) # pyright: ignore[reportRedeclaration]
28
+ print(f"Loaded profile from archive: {archive_path}")
29
+ return
30
+
31
+
32
+ print(f"Extracting data from '{path_to_pdf}'")
33
+
34
+ prompt: str = load_prompt(path_to_pdf, modules=modules)
35
+ self.result_json: Any = safe_parse_json(self.invoke(prompt))
36
+
37
+ print("Data retrieved.")
38
+
39
+ self.result_archive: str | Path = self.archive_json()
40
+
41
+ def get_archive_location(self) -> str | Path:
42
+ return self.result_archive
43
+
44
+ def get_result_json(self) -> Any:
45
+ return self.result_json
46
+
47
+
48
+ @abstractmethod
49
+ def invoke(self, prompt: str) -> str:
50
+ """
51
+ Prompts the LLM with the PDF and returns the json object as a string.
52
+ This function handles everything LLM related; from getting the prompt to prompting thr LLM and receiving the output.
53
+
54
+ Args:
55
+ prompt: The prompt to send to the LLM.
56
+
57
+ Returns:
58
+ Returns the LLM response text. Text sanitisation happens in the __init__ function.
59
+ """
60
+ pass
61
+
62
+ def get_default_archive_path(self) -> Path:
63
+ return Path.home() / ".beyondcv" / "archive" / f"{self.file_name}.json"
64
+
65
+ def archive_json(self) -> str | Path:
66
+ """
67
+ The JSON file is saved to an archive location and the location is returned.
68
+ This method can be overriden by the user to save to any location of their choice.
69
+
70
+ This method is automatically called by the __init__ function.
71
+ """
72
+ if not self.result_json:
73
+ raise ValueError("No JSON object available.")
74
+
75
+ json_archive: Path = self.get_default_archive_path()
76
+ json_archive.parent.mkdir(parents=True, exist_ok=True)
77
+
78
+ with open(json_archive, "w") as f:
79
+ json.dump(self.result_json, f, indent=2, ensure_ascii=False)
80
+
81
+ print(f"Archived profile JSON in {json_archive}")
82
+ return json_archive
83
+
84
+
85
+ __all__ = [
86
+ "LLMInvoker"
87
+ ]
File without changes
@@ -0,0 +1,12 @@
1
+ You are a CV parsing expert. Extract the following information from this CV and return it as a valid JSON object.
2
+ Below is a template. Fill in the template with information from the CV attached. Do not make up any information. Only write what is written in the CV. Do not assume any information.
3
+ If information cannot be found, leave it empty as such for example:
4
+ "projects": "",
5
+ If, for example, only one experience exists then only include one experience section "experience": [{{}}] (<- GOOD) no extras; "experience": [{{}}, {{}}] (<- BAD)
6
+
7
+ {json_template}
8
+ {extra_fields}
9
+
10
+ Return ONLY the JSON object, no additional text or markdown formatting.
11
+ CV Text:
12
+ {extracted_text}
BeyondCV/LLM/utils.py ADDED
@@ -0,0 +1,51 @@
1
+ import json
2
+ import pypdf
3
+ from pathlib import Path
4
+ from typing import Any
5
+ from BeyondCV.LLM.CVFields import BASE_TEMPLATE, build_extra_fields_text
6
+
7
+
8
+ def extract_text_from_pdf(pdf_path: str | Path) -> str:
9
+ """
10
+ Extract text content from a PDF file.
11
+
12
+ Args:
13
+ pdf_path: Path to the PDF file
14
+
15
+ Returns:
16
+ Extracted text as a string
17
+ """
18
+ with open(pdf_path, 'rb') as file:
19
+ pdf_reader = pypdf.PdfReader(file)
20
+ text = ""
21
+ for page in pdf_reader.pages:
22
+ text += page.extract_text()
23
+ return text
24
+
25
+
26
+ def load_prompt(path_to_pdf: str | Path, modules: list[str] | None = None) -> str:
27
+ if not modules: modules = []
28
+
29
+ prompt_path = Path(__file__).parent / "prompt.txt"
30
+
31
+ with open(prompt_path, "r") as p:
32
+ prompt_template = p.read()
33
+
34
+ return prompt_template.format(
35
+ json_template=json.dumps(BASE_TEMPLATE, indent=2),
36
+ extra_fields=build_extra_fields_text(modules),
37
+ extracted_text=extract_text_from_pdf(path_to_pdf),
38
+ )
39
+
40
+
41
+ def safe_parse_json(response_text: str) -> Any:
42
+ """Parse JSON from LLM response, handling markdown fences if present."""
43
+ if not response_text:
44
+ raise ValueError("Response is empty")
45
+ try:
46
+ return json.loads(response_text)
47
+ except json.JSONDecodeError:
48
+ import re
49
+ cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", response_text.strip())
50
+ return json.loads(cleaned)
51
+
@@ -0,0 +1,136 @@
1
+ __all__ = [
2
+ "CellConfig",
3
+ "ParagraphConfig",
4
+ "Paragraph",
5
+ "Cell",
6
+ "Row",
7
+ "Table"
8
+ ]
9
+
10
+ from colour import Color
11
+ from BeyondCV.config import bcv_config as cfg
12
+ from BeyondCV.utils import PaperDimensions, get_paper_dimensions, get_page_dimensions
13
+
14
+ _default_alignment: dict[str, str] = {
15
+ "vertical": "center", # Can be "top", "center", "bottom"
16
+ "horizontal": "left", # Can be "left", "center", "right"
17
+ }
18
+
19
+ # Paper imensions are that of the whole paper
20
+ _paper_dimensions: PaperDimensions = get_paper_dimensions(str(cfg.paper_size).lower()) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
21
+ # Page dimensions are that of the space within the margins
22
+ _page_dimensions: PaperDimensions = get_page_dimensions(
23
+ _paper_dimensions,
24
+ float(cfg.margin_left_cm), float(cfg.margin_right_cm), # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
25
+ float(cfg.margin_top_cm), float(cfg.margin_bottom_cm) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
26
+ )
27
+
28
+
29
+ class CellConfig:
30
+ def __init__(
31
+ self,
32
+ width_cm: float = 0.0, # If width is 0.0, the with of the cell is set to 1/total row width
33
+ color: Color | None = None,
34
+ content_alignment: dict[str, str] = _default_alignment,
35
+ show_borders: bool = False
36
+ ):
37
+ self.width_cm: float = width_cm
38
+ self.color: Color | None = color
39
+ self.content_alignment: dict[str, str] = content_alignment
40
+ self.show_borders: bool = show_borders
41
+
42
+
43
+ class ParagraphConfig:
44
+ def __init__(
45
+ self,
46
+ font_name: str = cfg.default_font, # pyright: ignore[reportUnknownMemberType]
47
+ font_size_pt: float = 10.0,
48
+ bold: bool = False,
49
+ italic: bool = False,
50
+ underline: bool = False,
51
+ bullet: bool = False
52
+ ):
53
+ self.font_name: str = font_name
54
+ self.font_size_pt: float = font_size_pt
55
+ self.bold: bool = bold
56
+ self.italic: bool = italic
57
+ self.underline: bool = underline
58
+ self.bullet: bool = bullet
59
+
60
+
61
+ class Paragraph:
62
+ def __init__(
63
+ self,
64
+ text: str,
65
+ config: ParagraphConfig | None = None
66
+ ):
67
+ self.text: str = text
68
+ self.config: ParagraphConfig = config if config else ParagraphConfig()
69
+
70
+
71
+ class Cell:
72
+ def __init__(
73
+ self,
74
+ content: list[Paragraph] | Paragraph,
75
+ config: CellConfig | None = None
76
+ ):
77
+ self.paragraphs: list[Paragraph] = [content] if isinstance(content, Paragraph) else content
78
+ self.config: CellConfig = config if config else CellConfig()
79
+
80
+
81
+ class Row:
82
+ def __init__(
83
+ self,
84
+ cells: list[Cell] | Cell,
85
+ min_height_cm: float = 0.45,
86
+ row_width_cm: float = 0.0 # If this value is 0, the row is as wide as the page margins
87
+ ):
88
+ self.cells: list[Cell] = [cells] if isinstance(cells, Cell) else cells
89
+ self.row_width_cm: float = row_width_cm if row_width_cm > 0.0 else _page_dimensions.width
90
+ self.min_height_cm: float = min_height_cm
91
+
92
+ for cell in self.cells:
93
+ if cell.config.width_cm <= 0.0:
94
+ cell.config.width_cm = self.row_width_cm * (1/len(self.cells))
95
+
96
+ def add_cell(self, cell: Cell):
97
+ self.cells.append(cell)
98
+
99
+
100
+ class Column:
101
+ def __init__(
102
+ self,
103
+ cells: list[Cell],
104
+ min_height_cm: float = 0.45,
105
+ width_cm: float = 0.0, # Again, if this value is zero, width is calculated at runtime depending on the number of columns in the table
106
+ ):
107
+ self.cells: list[Cell] = cells
108
+ self.min_height_cm: float = min_height_cm
109
+ self.width_cm: float = width_cm
110
+
111
+
112
+ class Table:
113
+ def __init__(
114
+ self,
115
+ content: list[Row] | list[Column]
116
+ ):
117
+ self.content: list[Row] | list[Column] = content
118
+
119
+ if Table.are_columns(self.content) and len(self.content) > 0:
120
+ for col in self.content:
121
+ col.width_cm = 1/len(self.content) * _page_dimensions.width # pyright: ignore[reportAttributeAccessIssue]
122
+
123
+ @staticmethod
124
+ def are_columns(items: list[Row] | list[Column]):
125
+ for i in items:
126
+ if not isinstance(i, Column):
127
+ return False
128
+ return True
129
+
130
+ @staticmethod
131
+ def are_rows(items: list[Row] | list[Column]):
132
+ for i in items:
133
+ if not isinstance(i, Row):
134
+ return False
135
+ return True
136
+
@@ -0,0 +1,10 @@
1
+ from BeyondCV.TableBuilder.Table import *
2
+
3
+ __all__ = [
4
+ "CellConfig",
5
+ "ParagraphConfig",
6
+ "Paragraph",
7
+ "Cell",
8
+ "Row",
9
+ "Table"
10
+ ]
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = [
4
+ "CVTemplate",
5
+ "Section",
6
+ "RepeatingSection",
7
+ ]
8
+
9
+ import copy
10
+ import re
11
+ from typing import Any
12
+
13
+ from BeyondCV.TableBuilder.Table import Cell, Paragraph, ParagraphConfig, Row, Table
14
+
15
+
16
+ class SectionBase:
17
+ def _resolve_row(self, row: Row, data: dict[str, Any]) -> Row:
18
+ resolved_cells: list[Cell] = []
19
+ for cell in row.cells:
20
+ resolved_paragraphs: list[Paragraph] = []
21
+ for p in cell.paragraphs:
22
+ resolved = SectionBase._resolve_placeholders(p.text, data)
23
+ if isinstance(resolved, list):
24
+ resolved_paragraphs.extend(
25
+ Paragraph(item, copy.deepcopy(p.config)) for item in resolved
26
+ )
27
+ else:
28
+ resolved_paragraphs.append(Paragraph(resolved, copy.deepcopy(p.config)))
29
+ resolved_cells.append(Cell(resolved_paragraphs, copy.deepcopy(cell.config)))
30
+ return Row(resolved_cells, row.min_height_cm, row.row_width_cm)
31
+
32
+
33
+ @staticmethod
34
+ def _resolve_placeholders(text: str, data: dict[str, Any]) -> str | list[str]:
35
+ """
36
+ Replace {field_name} placeholders in a text string with values from the data dict.
37
+
38
+ When a field holds a list value (e.g. description = ["bullet1", "bullet2"]),
39
+ the entire function returns a list of strings instead, so the caller can
40
+ create one Paragraph per list item.
41
+
42
+ If the template text contains a suffix immediately after the list placeholder
43
+ (e.g. "{items},"), the suffix is appended to every item except the last.
44
+
45
+ Args:
46
+ text: A string that may contain {field_name} placeholders.
47
+ data: The data dict to resolve placeholders against.
48
+
49
+ Returns:
50
+ The resolved string, or a list of strings if the field value was a list.
51
+ """
52
+ _PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
53
+
54
+ def _replace(match: re.Match[str]) -> str:
55
+ key: str = match.group(1)
56
+ value: str | list[str] | None = data.get(key)
57
+ if value is None:
58
+ return match.group(0)
59
+ if isinstance(value, list):
60
+ return "<__LIST_PLACEHOLDER__>"
61
+ return str(value)
62
+
63
+ resolved = _PLACEHOLDER_RE.sub(_replace, text)
64
+ if "<__LIST_PLACEHOLDER__>" in resolved:
65
+ keys = [m.group(1) for m in _PLACEHOLDER_RE.finditer(text)]
66
+ for k in keys:
67
+ v: Any | None = data.get(k)
68
+ if isinstance(v, list):
69
+ # Extract any suffix that follows the {key} placeholder in the template.
70
+ suffix_match = re.search(r"\{" + re.escape(k) + r"\}(.*)$", text)
71
+ suffix = suffix_match.group(1) if suffix_match else ""
72
+ items = [str(item) for item in v] # pyright: ignore[reportUnknownArgumentType, reportUnknownVariableType]
73
+ if suffix:
74
+ return [item + suffix for item in items[:-1]] + [items[-1]]
75
+ return items
76
+ return resolved.replace("<__LIST_PLACEHOLDER__>", "")
77
+ return resolved
78
+
79
+
80
+ class SectionTitle:
81
+ def __init__(self, title: str, text_config: ParagraphConfig | None = None):
82
+ if text_config == None:
83
+ text_config = ParagraphConfig(font_size_pt=15, bold=True)
84
+
85
+ self.table: Table = Table([
86
+ Row(Cell([Paragraph(title, config=text_config)]))
87
+ ])
88
+
89
+
90
+ class Section(SectionBase):
91
+ def __init__(
92
+ self,
93
+ *rows: Row,
94
+ title: SectionTitle | None = None
95
+ ):
96
+ self.rows: list[Row] = list(rows)
97
+ self.title: SectionTitle | None = title
98
+
99
+ def build(self, data: dict[str, Any]) -> list[Table]:
100
+ resolved_rows = [self._resolve_row(row, data) for row in self.rows]
101
+ return [Table(resolved_rows)] if not self.title else [self.title.table, Table(resolved_rows)]
102
+
103
+
104
+ class RepeatingSection(SectionBase):
105
+ def __init__(
106
+ self,
107
+ source_key: str,
108
+ item: Table,
109
+ header: list[Row] | Row | None = None,
110
+ title: SectionTitle | None = None
111
+ ):
112
+ self.source_key: str = source_key
113
+ self.item: Table = item
114
+ self.header: list[Row] = [header] if isinstance(header, Row) else header or []
115
+ self.title: SectionTitle | None = title
116
+
117
+ def build(self, data: dict[str, Any]) -> list[Table]:
118
+ items: list[Any] | str = data.get(self.source_key, [])
119
+ if not isinstance(items, list):
120
+ items = [items]
121
+
122
+ result: list[Table] = [] if not self.title else [self.title.table]
123
+ if not self.header:
124
+ return [*result, *self._build_separate(items)]
125
+ else:
126
+ return [*result, *self._build_with_header(items, data)]
127
+
128
+ def _build_separate(self, items: list[Any]) -> list[Table]:
129
+ tables: list[Table] = []
130
+ for item_data in items:
131
+ if not isinstance(item_data, dict):
132
+ tables.append(copy.deepcopy(self.item))
133
+ continue
134
+
135
+ resolved_rows: list[Row] = []
136
+ for row in self.item.content:
137
+ if isinstance(row, Row):
138
+ resolved_rows.append(self._resolve_row(row, item_data)) # pyright: ignore[reportUnknownArgumentType]
139
+ tables.append(Table(resolved_rows))
140
+
141
+ return tables
142
+
143
+ def _build_with_header(self, items: list[Any], data: dict[str, Any]) -> list[Table]:
144
+ resolved_rows: list[Row] = []
145
+ for row in self.header:
146
+ resolved_rows.append(self._resolve_row(row, data))
147
+
148
+ for item_data in items:
149
+ if not isinstance(item_data, dict):
150
+ for row in self.item.content:
151
+ if isinstance(row, Row):
152
+ resolved_rows.append(copy.deepcopy(row))
153
+ continue
154
+
155
+ for row in self.item.content:
156
+ if isinstance(row, Row):
157
+ resolved_rows.append(self._resolve_row(row, item_data)) # pyright: ignore[reportUnknownArgumentType]
158
+
159
+ return [Table(resolved_rows)]
160
+
161
+
162
+ class CVTemplate:
163
+ def __init__(self, sections: list[Section | RepeatingSection]):
164
+ self.sections: list[Section | RepeatingSection] = sections
165
+
166
+ def build(self, data: dict[str, Any]) -> list[Table]:
167
+ tables: list[Table] = []
168
+ for section in self.sections:
169
+ tables.extend(section.build(data))
170
+ return tables
@@ -0,0 +1,7 @@
1
+ from BeyondCV.Template.Template import CVTemplate, Section, RepeatingSection
2
+
3
+ __all__ = [
4
+ "CVTemplate",
5
+ "Section",
6
+ "RepeatingSection",
7
+ ]
@@ -0,0 +1,39 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+ from pathlib import Path
4
+ from BeyondCV.Template import CVTemplate
5
+
6
+
7
+ class DocTranslator(ABC):
8
+ def __init__(self, document_name: str, template: CVTemplate):
9
+ """
10
+ Intialises the translator, making sure the save location exists.
11
+ Args:
12
+ document_name: Name of the output document. This MUST contain the file extension so that the file is correcly saved.
13
+ template: The CV template created that will be translated.
14
+ """
15
+ if not Path(document_name).suffix:
16
+ raise ValueError(f"document_name must include a file extension (e.g. 'my_cv.docx'), got: '{document_name}'")
17
+
18
+ doc_location: Path = Path.home() / ".beyondcv" / "outfiles" / f"{document_name}"
19
+ doc_location.parent.mkdir(parents=True, exist_ok=True)
20
+ self._doc_location: Path = doc_location
21
+ self._template: CVTemplate = template
22
+
23
+ @abstractmethod
24
+ def build(self, data: dict[str, Any]) -> str:
25
+ """
26
+ A method uniquely implemented for each filetype. This method should build the document and return the path to this newly built document.
27
+
28
+ Args:
29
+ data: The data about the profile as a json object.
30
+
31
+ Returns:
32
+ The location of the newly created document.
33
+ """
34
+ pass
35
+
36
+
37
+ __all__ = [
38
+ "DocTranslator",
39
+ ]
@@ -0,0 +1,214 @@
1
+ from typing import Any, override
2
+
3
+ from docx import Document
4
+ from docx.document import Document as DocType
5
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
6
+ from docx.oxml import OxmlElement # pyright: ignore[reportUnknownVariableType]
7
+ from docx.oxml.ns import qn
8
+ from docx.oxml.table import CT_TcPr
9
+ from docx.shared import Cm, Pt
10
+ from docx.table import _Cell as DocxCell, Table as DocxTable # pyright: ignore[reportPrivateUsage]
11
+ from docx.text.paragraph import Paragraph as DocxParagraph
12
+
13
+ from colour import Color
14
+
15
+ from BeyondCV.Translator.DocTranslator import DocTranslator
16
+ from BeyondCV.TableBuilder.Table import Table as CVTable, Row, Cell, Paragraph, Column
17
+ from BeyondCV.config import bcv_config as cfg
18
+
19
+
20
+ class DocxTranslator(DocTranslator):
21
+ _HALIGN_MAP: dict[str, WD_ALIGN_PARAGRAPH] = {
22
+ "left": WD_ALIGN_PARAGRAPH.LEFT,
23
+ "center": WD_ALIGN_PARAGRAPH.CENTER,
24
+ "right": WD_ALIGN_PARAGRAPH.RIGHT,
25
+ }
26
+
27
+ @override
28
+ def build(self, data: dict[str, Any]) -> str:
29
+ tables = self._template.build(data)
30
+ doc: DocType = Document()
31
+
32
+ for section in doc.sections:
33
+ section.top_margin = Cm(float(cfg.margin_top_cm)) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
34
+ section.bottom_margin = Cm(float(cfg.margin_bottom_cm)) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
35
+ section.left_margin = Cm(float(cfg.margin_left_cm)) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
36
+ section.right_margin = Cm(float(cfg.margin_right_cm)) # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
37
+
38
+ for i, table_model in enumerate(tables):
39
+ if i > 0:
40
+ _ = doc.add_paragraph()
41
+ self._add_table(doc, table_model)
42
+
43
+ doc.save(str(self._doc_location))
44
+ return str(self._doc_location)
45
+
46
+ # ------------------------------------------------------------------ #
47
+ # Table rendering
48
+ # ------------------------------------------------------------------ #
49
+
50
+ def _add_table(self, doc: DocType, table_model: CVTable):
51
+ if CVTable.are_columns(table_model.content):
52
+ self._add_column_table(doc, table_model)
53
+ else:
54
+ self._add_row_table(doc, table_model)
55
+
56
+ def _add_row_table(self, doc: DocType, table_model: CVTable):
57
+ rows = [r for r in table_model.content if isinstance(r, Row)]
58
+ if not rows:
59
+ return
60
+
61
+ max_cols = max(len(r.cells) for r in rows)
62
+ table = doc.add_table(rows=len(rows), cols=max_cols)
63
+ self._remove_table_borders(table)
64
+
65
+ for row_idx, row_model in enumerate(rows):
66
+ table.rows[row_idx].height = Cm(row_model.min_height_cm)
67
+
68
+ for col_idx, cell_model in enumerate(row_model.cells):
69
+ cell = table.cell(row_idx, col_idx)
70
+ self._fill_cell(cell, cell_model)
71
+
72
+ if len(row_model.cells) < max_cols:
73
+ start = table.cell(row_idx, len(row_model.cells) - 1)
74
+ end = table.cell(row_idx, max_cols - 1)
75
+ _ = start.merge(end)
76
+
77
+ def _add_column_table(self, doc: DocType, table_model: CVTable):
78
+ columns = [c for c in table_model.content if isinstance(c, Column)]
79
+ if not columns:
80
+ return
81
+
82
+ col_count = len(columns)
83
+ row_count = max(len(c.cells) for c in columns)
84
+ if row_count == 0:
85
+ return
86
+
87
+ table = doc.add_table(rows=row_count, cols=col_count)
88
+ self._remove_table_borders(table)
89
+
90
+ for col_idx, col_model in enumerate(columns):
91
+ for row_idx, cell_model in enumerate(col_model.cells):
92
+ cell = table.cell(row_idx, col_idx)
93
+ self._fill_cell(cell, cell_model)
94
+
95
+ # ------------------------------------------------------------------ #
96
+ # Cell / Paragraph formatting
97
+ # ------------------------------------------------------------------ #
98
+
99
+ def _fill_cell(self, cell: DocxCell, cell_model: Cell):
100
+ config = cell_model.config
101
+
102
+ if config.width_cm > 0.0:
103
+ cell.width = Cm(config.width_cm)
104
+
105
+ self._set_cell_vertical_alignment(cell, config.content_alignment.get("vertical", "center"))
106
+
107
+ self._remove_cell_shading(cell)
108
+ if config.color is not None:
109
+ self._set_cell_shading(cell, config.color)
110
+
111
+ self._remove_cell_borders(cell)
112
+ if config.show_borders:
113
+ self._set_cell_borders(cell)
114
+
115
+ for para_idx, para_model in enumerate(cell_model.paragraphs):
116
+ p = cell.paragraphs[0] if para_idx == 0 else cell.add_paragraph()
117
+ _ = p.clear()
118
+ self._format_paragraph(p, para_model, config.content_alignment)
119
+
120
+ def _format_paragraph(self, p: DocxParagraph, para_model: Paragraph, alignment: dict[str, str]):
121
+ p.paragraph_format.space_before = Pt(0)
122
+ p.paragraph_format.space_after = Pt(0)
123
+
124
+ halign = alignment.get("horizontal", "left")
125
+ p.alignment = self._HALIGN_MAP.get(halign, WD_ALIGN_PARAGRAPH.LEFT)
126
+
127
+ if para_model.config.bullet:
128
+ p.style = "List Bullet"
129
+
130
+ run = p.add_run(para_model.text)
131
+ run.font.name = para_model.config.font_name
132
+ run.font.size = Pt(para_model.config.font_size_pt)
133
+ run.bold = para_model.config.bold
134
+ run.italic = para_model.config.italic
135
+ run.underline = para_model.config.underline
136
+
137
+ # ------------------------------------------------------------------ #
138
+ # XML helpers — vertical alignment
139
+ # ------------------------------------------------------------------ #
140
+
141
+ @staticmethod
142
+ def _set_cell_vertical_alignment(cell: DocxCell, valign: str):
143
+ tc = cell._tc # pyright: ignore[reportPrivateUsage]
144
+ tcPr: CT_TcPr = tc.get_or_add_tcPr()
145
+ existing = tcPr.find(qn("w:vAlign"))
146
+ if existing is not None:
147
+ tcPr.remove(existing)
148
+ elem = OxmlElement("w:vAlign")
149
+ elem.set(qn("w:val"), valign)
150
+ tcPr.append(elem)
151
+
152
+ # ------------------------------------------------------------------ #
153
+ # XML helpers — shading / background colour
154
+ # ------------------------------------------------------------------ #
155
+
156
+ @staticmethod
157
+ def _remove_cell_shading(cell: DocxCell):
158
+ tc = cell._tc # pyright: ignore[reportPrivateUsage]
159
+ tcPr = tc.get_or_add_tcPr()
160
+ for shd in tcPr.findall(qn("w:shd")):
161
+ tcPr.remove(shd)
162
+
163
+ @staticmethod
164
+ def _set_cell_shading(cell: DocxCell, color: Color):
165
+ hex_color = color.hex_l[1:]
166
+ tc = cell._tc # pyright: ignore[reportPrivateUsage]
167
+ tcPr = tc.get_or_add_tcPr()
168
+ shd = OxmlElement("w:shd")
169
+ shd.set(qn("w:fill"), hex_color)
170
+ shd.set(qn("w:val"), "clear")
171
+ tcPr.append(shd)
172
+
173
+ # ------------------------------------------------------------------ #
174
+ # XML helpers — borders
175
+ # ------------------------------------------------------------------ #
176
+
177
+ @staticmethod
178
+ def _remove_cell_borders(cell: DocxCell):
179
+ tc = cell._tc # pyright: ignore[reportPrivateUsage]
180
+ tcPr = tc.get_or_add_tcPr()
181
+ existing = tcPr.find(qn("w:tcBorders"))
182
+ if existing is not None:
183
+ tcPr.remove(existing)
184
+
185
+ @staticmethod
186
+ def _set_cell_borders(cell: DocxCell):
187
+ tc = cell._tc # pyright: ignore[reportPrivateUsage]
188
+ tcPr = tc.get_or_add_tcPr()
189
+ borders = OxmlElement("w:tcBorders")
190
+ for side in ("top", "left", "bottom", "right"):
191
+ border = OxmlElement(f"w:{side}")
192
+ border.set(qn("w:val"), "single")
193
+ border.set(qn("w:sz"), "4")
194
+ border.set(qn("w:space"), "0")
195
+ border.set(qn("w:color"), "000000")
196
+ borders.append(border)
197
+ tcPr.append(borders)
198
+
199
+ @staticmethod
200
+ def _remove_table_borders(table: DocxTable):
201
+ tbl = table._tbl # pyright: ignore[reportPrivateUsage]
202
+ tblPr = tbl.find(qn("w:tblPr"))
203
+ if tblPr is None:
204
+ tblPr = OxmlElement("w:tblPr")
205
+ tbl.insert(0, tblPr)
206
+ borders = OxmlElement("w:tblBorders")
207
+ for side in ("top", "left", "bottom", "right", "insideH", "insideV"):
208
+ border = OxmlElement(f"w:{side}")
209
+ border.set(qn("w:val"), "none")
210
+ border.set(qn("w:sz"), "0")
211
+ border.set(qn("w:space"), "0")
212
+ border.set(qn("w:color"), "auto")
213
+ borders.append(border)
214
+ tblPr.append(borders)
@@ -0,0 +1,7 @@
1
+ from typing import override, Any
2
+ from BeyondCV.Translator.DocTranslator import DocTranslator
3
+
4
+ class TeXTranslator(DocTranslator):
5
+ @override
6
+ def build(self, data: dict[str, Any]) -> str:
7
+ pass
@@ -0,0 +1,7 @@
1
+ from BeyondCV.Translator.DocxTranslator import DocxTranslator
2
+ from BeyondCV.Translator.TeXTranslator import TeXTranslator
3
+
4
+ __all__ = [
5
+ "DocxTranslator",
6
+ "TeXTranslator"
7
+ ]
BeyondCV/__init__.py ADDED
File without changes
BeyondCV/config.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ In this file, the CV template proprties can be configured.
3
+ """
4
+
5
+ __all__ = [
6
+ "bcv_config"
7
+ ]
8
+
9
+ import yaml
10
+ from pathlib import Path
11
+ from addict import Dict
12
+ from BeyondCV.utils import merge_dicts_recursively
13
+
14
+
15
+ def _load_yaml(file_path: str | Path) -> dict[str, str]:
16
+ try:
17
+ with open(file_path, "r") as file:
18
+ return yaml.safe_load(file) or {}
19
+ except FileNotFoundError:
20
+ return {}
21
+
22
+
23
+ default_config_dir = Path(__file__).resolve().parent / "default_config.yaml"
24
+ current_config_dir = "current_config.yaml"
25
+
26
+ # ORDER MATTERS! Ensure current_config exists AFTER default_config
27
+ bcv_config: Dict = Dict(merge_dicts_recursively(
28
+ _load_yaml(default_config_dir),
29
+ _load_yaml(current_config_dir)
30
+ ))
@@ -0,0 +1,7 @@
1
+ paper_size: "A4" # Can be "A4" or "letter"
2
+ margin_top_cm: 2.54
3
+ margin_bottom_cm: 2.54
4
+ margin_right_cm: 2.54
5
+ margin_left_cm: 2.54
6
+ default_font: "Aptos"
7
+ use_cache: false
BeyondCV/utils.py ADDED
@@ -0,0 +1,53 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = [
4
+ "PaperDimensions",
5
+ "merge_dicts_recursively",
6
+ "get_page_dimensions",
7
+ "get_paper_dimensions",
8
+ ]
9
+
10
+
11
+ import itertools as it
12
+ from typing import Any, NamedTuple
13
+
14
+ class PaperDimensions(NamedTuple):
15
+ width: float
16
+ height: float
17
+
18
+
19
+ # Taken from https://github.com/3b1b/manim/blob/master/manimlib/utils/dict_ops.py
20
+ def merge_dicts_recursively(*dicts: dict[Any, Any]) -> dict[Any, Any]:
21
+ """
22
+ Creates a dict whose keyset is the union of all the
23
+ input dictionaries. The value for each key is based
24
+ on the first dict in the list with that key.
25
+
26
+ dicts later in the list have higher priority
27
+
28
+ When values are dictionaries, it is applied recursively
29
+ """
30
+ result: dict[Any, Any] = dict()
31
+ all_items = it.chain(*[d.items() for d in dicts])
32
+ for key, value in all_items:
33
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
34
+ result[key] = merge_dicts_recursively(result[key], value) # pyright: ignore[reportUnknownArgumentType]
35
+ else:
36
+ result[key] = value
37
+ return result
38
+
39
+
40
+ def get_paper_dimensions(paper_size: str = "a4") -> PaperDimensions:
41
+ if paper_size == "letter":
42
+ return PaperDimensions(21.6, 27.9)
43
+ else:
44
+ return PaperDimensions(21.0, 29.7)
45
+
46
+
47
+ def get_page_dimensions(paper_dims: PaperDimensions, margin_left_cm: float, margin_right_cm: float, margin_top_cm: float, margin_bottom_cm: float):
48
+ return PaperDimensions(
49
+ paper_dims.width - margin_left_cm - margin_right_cm,
50
+ paper_dims.height - margin_top_cm - margin_bottom_cm
51
+ )
52
+
53
+
@@ -0,0 +1,51 @@
1
+ Metadata-Version: 2.4
2
+ Name: BeyondCV
3
+ Version: 0.1.0
4
+ Summary: A CV builder.
5
+ Author: Abdallah Soliman
6
+ Author-email: Abdallah Soliman <abdallahsoliman03@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ License-File: LICENSE
9
+ Requires-Dist: addict>=2.4.0
10
+ Requires-Dist: colour>=0.1.5
11
+ Requires-Dist: debugpy>=1.8.20
12
+ Requires-Dist: dotenv>=0.9.9
13
+ Requires-Dist: openai>=2.36.0
14
+ Requires-Dist: pypdf>=6.11.0
15
+ Requires-Dist: python-docx>=1.2.0
16
+ Requires-Dist: pyyaml>=6.0.3
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+
20
+ # BeyondCV
21
+ No more pressing CTRL+C, CTRL+V to put your CV into a new template.
22
+
23
+ <!--toc:start-->
24
+ - [Introduction](#introduction)
25
+ - [Architecture](#architecture)
26
+ - [License](#license)
27
+ - [Disclaimer](#disclaimer)
28
+ <!--toc:end-->
29
+
30
+ ## Introduction
31
+ This is a tool used to quickly move an existing CV/resume from one template to another.
32
+ Drop in the old CV, configure the output template and get the new CV.
33
+
34
+ This tool is best used to mass convert CVs, for inidvidual CVs, this may not be the best option as the
35
+ CV template may be less configurable than one made manually.
36
+
37
+ This project has two different parts to it:
38
+ 1. BeyondCV: the library used to create a CV builder, providing a framework to follow.
39
+ 2. BuildImpl: an implementation using BeyondCV to create a full CV conversion pipeline.
40
+
41
+ ## How To Use
42
+ See the [BuildImpl](/BuildImpl/) file for a sample implementation.
43
+
44
+ ## Architecture
45
+ See the [Architecture](/Architecture.md) document for more info.
46
+
47
+ ## License
48
+ See [LICENSE](/LICENSE) document.
49
+
50
+ ## Disclaimer
51
+ This code was partly generated by AI, so there may be some bugs.
@@ -0,0 +1,21 @@
1
+ BeyondCV/LLM/CVFields.py,sha256=9bHZ18ZGYamUZrddibm4emva_bK0y0KWhGU3cBnEbsg,2459
2
+ BeyondCV/LLM/LLMInvoker.py,sha256=MdqOMFAwCoecOzLNTJILu1RAiLuStmWAhQ2giy9KzxQ,3158
3
+ BeyondCV/LLM/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ BeyondCV/LLM/prompt.txt,sha256=k9Lh6fsO7Ku9Iv871CVW0rlg0ZWcpLYg_ZsRvYiKopw,688
5
+ BeyondCV/LLM/utils.py,sha256=0XydHhlkupIw9el8bK6pFliOk6zKnSn6IwvecMWPPKI,1498
6
+ BeyondCV/TableBuilder/Table.py,sha256=4iaWhEGbEVnmnvYQp_30r4g55clnd0Yd7MFJSl07PYc,4600
7
+ BeyondCV/TableBuilder/__init__.py,sha256=rORvLB5tp3jrOmYpzY_JB67YxDHHFZU2_8IRR7IH0Gc,160
8
+ BeyondCV/Template/Template.py,sha256=FQa4ziXGHlcd2s77rNA991HiD4ULix502wj2H9GWyME,6682
9
+ BeyondCV/Template/__init__.py,sha256=GkzINbj7aYVFVlRPorMLs-1K5QPtY_TDupVpcYI49Iw,154
10
+ BeyondCV/Translator/DocTranslator.py,sha256=m7sIA-43J64gaI5LRPEQMcq9_AXxSZxx2o3wtSBzgag,1423
11
+ BeyondCV/Translator/DocxTranslator.py,sha256=JFIEv7lsgdIIt9XUOuMSu5g-BamPzAi26Qs9uKn_w1E,8785
12
+ BeyondCV/Translator/TeXTranslator.py,sha256=t7cCEGyCEBYyBhWK8CRP-tsAkERcWpzBWHN2YkA46fQ,214
13
+ BeyondCV/Translator/__init__.py,sha256=g4oNIJ9CUJl9HloQwyiAM_5p9OAlx_iKi3JOpM_qx9k,186
14
+ BeyondCV/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ BeyondCV/config.py,sha256=b2WrLpa3-VmNlDlFtV3YXKTStPL7992Q0xK1zaPWft8,760
16
+ BeyondCV/default_config.yaml,sha256=qcvgE71U_vCISouFGzyavmPg3xqXqJtT1-jLMUJxSZ8,178
17
+ BeyondCV/utils.py,sha256=8SdCz-cJaz3Ow7XUcW1O2U_Tw5uajzWBRG_DMNpDogI,1684
18
+ beyondcv-0.1.0.dist-info/licenses/LICENSE,sha256=NhwhbwmIgo8AFh5O1g9ctPGkLN6hDdf_Dqv2gYAybhY,11053
19
+ beyondcv-0.1.0.dist-info/WHEEL,sha256=XkDrRXQq-qVsrKMtsDUOHeLkiG7UK4Ds0JuG05OqKU4,81
20
+ beyondcv-0.1.0.dist-info/METADATA,sha256=S19zqD3sAtBhy7PI_bHPZNqBf-tbODfnp_fOgQc48qQ,1627
21
+ beyondcv-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.13
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,202 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
202
+