From 0e0754fa10db44722846cab04509694d8f9a0a57 Mon Sep 17 00:00:00 2001 From: Ondrej Vlach Date: Wed, 29 Nov 2023 14:04:36 +0100 Subject: [PATCH] wip --- Cargo.lock | 370 +++++++++++++++++++++++++++++++++ Cargo.toml | 9 +- src/lib.rs | 1 + src/main.rs | 9 +- src/routes/pdf.rs | 57 +++-- src/tools/mod.rs | 2 + src/tools/pdf.rs | 25 +++ src/tools/tera.rs | 16 ++ templates/two_column.html.tera | 200 ++++++++++++++++++ 9 files changed, 659 insertions(+), 30 deletions(-) create mode 100644 src/tools/pdf.rs create mode 100644 src/tools/tera.rs create mode 100644 templates/two_column.html.tera diff --git a/Cargo.lock b/Cargo.lock index e742898..8cabfd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -270,6 +280,28 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "chrono-tz" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e23185c0e21df6ed832a12e2bda87c7d1def6842881fb634a8511ced741b0d76" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -321,6 +353,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -421,6 +477,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deunicode" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1abaf4d861455be59f64fd2b55606cb151fce304ede7165f410243ce96bde6" + [[package]] name = "devise" version = "0.4.1" @@ -699,6 +761,30 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + [[package]] name = "h2" version = "0.3.22" @@ -802,6 +888,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "hyper" version = "0.14.27" @@ -877,6 +972,22 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.3", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -951,6 +1062,12 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + [[package]] name = "linux-raw-sys" version = "0.4.11" @@ -1009,6 +1126,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1061,6 +1187,7 @@ version = "0.1.0" dependencies = [ "gethostname", "opentelemetry", + "opentelemetry-appender-tracing", "opentelemetry-otlp", "opentelemetry-semantic-conventions", "opentelemetry-stdout", @@ -1068,6 +1195,7 @@ dependencies = [ "rocket", "tracing", "tracing-appender", + "tracing-core", "tracing-log", "tracing-opentelemetry", "tracing-subscriber", @@ -1135,6 +1263,20 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12c4bd073648dae8ac45cfc81588d74b3dc5f334119ac08567ddcbfe16f2d809" +dependencies = [ + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "opentelemetry-otlp" version = "0.14.0" @@ -1254,8 +1396,11 @@ dependencies = [ "rocket", "serde", "serde_yaml", + "tempfile", + "tera", "tokio", "tracing", + "urlencoding", "yansi 0.5.1", ] @@ -1282,6 +1427,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "pear" version = "0.2.7" @@ -1311,6 +1465,89 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "pest_meta" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.3" @@ -1661,6 +1898,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1747,6 +1993,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1765,6 +2022,12 @@ dependencies = [ "libc", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -1774,6 +2037,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + [[package]] name = "smallvec" version = "1.11.2" @@ -1882,6 +2155,28 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -2258,6 +2553,12 @@ dependencies = [ "serde", ] +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "uncased" version = "0.9.9" @@ -2268,6 +2569,56 @@ dependencies = [ "version_check", ] +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2368,6 +2719,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2482,6 +2843,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 25d4210..5142092 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,10 +17,9 @@ ovlach_data = { git = "ssh://git@gitlab.nanobyte.cz/ondrej/ov-site-api-data.git" async-trait = "0.1.74" async-mutex = "1.4.0" tokio = { version = "1.34.0", features = ["macros"] } - - - tracing = { version = "0.1.40", features = ["attributes", "std"] } yansi = "0.5.1" - -nanobyte_opentelemetry = { path = "../nanobyte_opentelemetry" } \ No newline at end of file +tera = "1.19.1" +nanobyte_opentelemetry = { path = "../nanobyte_opentelemetry" } +tempfile = "3.8.1" +urlencoding = "2.1.3" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 514dc2b..9603a88 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,5 +12,6 @@ pub fn rocket_builder() -> Rocket { .attach(Chromium::ignite()) .mount("/", routes![ routes::pdf::render_pdf_cv, + routes::pdf::render_html_cv ]) } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8a5c08b..7a30c9f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,18 @@ -use nanobyte_opentelemetry::{default_filter_layer, LogLevel}; +use std::{panic, fs::File, io::Write}; + +use log::error; +use nanobyte_opentelemetry::{default_filter_layer, LogLevel, install_panic_handler}; use ovlach_pdf::rocket_builder; use tracing::Level; + #[rocket::main] async fn main() { + install_panic_handler(); let _opentelemetry = nanobyte_opentelemetry::init_telemetry( env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"), - Some(default_filter_layer(LogLevel::Normal)) + Some(default_filter_layer(LogLevel::DebugWithoutRs)) ); let _ = rocket_builder().launch().await; } \ No newline at end of file diff --git a/src/routes/pdf.rs b/src/routes/pdf.rs index edeb080..c155939 100644 --- a/src/routes/pdf.rs +++ b/src/routes/pdf.rs @@ -1,17 +1,20 @@ -use core::panic; -use std::{fs, io::Write}; - use headless_chrome::Browser; use headless_chrome::types::PrintToPdfOptions; use nanobyte_opentelemetry::rocket::TracingSpan; -use rocket::{get, fs::NamedFile}; -use tracing::{info_span, info}; -use crate::chromium::rocket::BrowserHolder; +use rocket::{get, response::stream::ByteStream, fairing::Fairing, fs::NamedFile, futures::TryFutureExt}; +use tempfile::NamedTempFile; +use tera::Context; +use tracing::{info_span, error, debug}; +use crate::{chromium::rocket::BrowserHolder, tools::{tera::NanoTera, pdf::PdfStream}}; +use urlencoding::encode; -fn generate_pdf(browser: Browser) { +// TODO: request-id +fn generate_pdf(browser: Browser, file: &NamedTempFile) -> Vec { let tab = browser.new_tab().unwrap(); + let path = format!("file://{}", file.path().to_str().unwrap()); info_span!("open_pdf").in_scope(|| { - tab.navigate_to("file:///home/6a6996c0-1609-48b6-8ca6-affbef1b4d1d/Devel/Nanobyte/ovlach/ovlach_pdf/template.html").unwrap().wait_until_navigated().unwrap(); + debug!("Render pdf from {}", &path); + tab.navigate_to(&path).unwrap().wait_until_navigated().unwrap(); }); let options = PrintToPdfOptions{ @@ -31,25 +34,33 @@ fn generate_pdf(browser: Browser) { tab.print_to_pdf(Some(options)).unwrap() }); - info_span!("write_temporary_file").in_scope(|| { - let mut file = fs::OpenOptions::new() - // .create(true) // To create a new file - .write(true) - // either use the ? operator or unwrap since it returns a Result - .open("/tmp/foo.pdf").unwrap(); - file.write_all(&bytes).unwrap(); - }); + return bytes; } -#[get("/cv//pdf")] -pub async fn render_pdf_cv(username: &str, browser: BrowserHolder, tracing: TracingSpan) -> Option { +#[tracing::instrument] +fn render_template(template_name: &str, file: &NamedTempFile, tera: NanoTera) { + // TODO: handle errors + tera.0.render_to("two_column.html.tera", &Context::new(), file); +} + +#[get("/cv//output.pdf")] +pub async fn render_pdf_cv(username: &str, browser: BrowserHolder, tera: NanoTera, tracing: TracingSpan) -> PdfStream { let entered_span = tracing.0.enter(); + let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap(); + render_template("two_column", &file, tera); let span = info_span!("render_pdf", username = username); - info!("generate PDF"); - span.in_scope(||{ - generate_pdf(browser.browser); + let pdf = span.in_scope(||{ + generate_pdf(browser.browser, &file) }); - info!("done generating"); drop(entered_span); - Some(NamedFile::open("/tmp/foo.pdf").await.expect("failed to open foo.pdf")) + PdfStream::new(pdf) +} + +/// Route only for debuging +#[get("/cv//output.html")] +pub async fn render_html_cv(username: &str, tera: NanoTera, tracing: TracingSpan) -> NamedFile { + let entered_span = tracing.0.enter(); + let file = tempfile::Builder::new().suffix(".html").tempfile().unwrap(); + render_template("two_column", &file, tera); + NamedFile::open(file.path()).await.unwrap() } \ No newline at end of file diff --git a/src/tools/mod.rs b/src/tools/mod.rs index e69de29..d53bde8 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -0,0 +1,2 @@ +pub mod tera; +pub mod pdf; \ No newline at end of file diff --git a/src/tools/pdf.rs b/src/tools/pdf.rs new file mode 100644 index 0000000..1e5ab93 --- /dev/null +++ b/src/tools/pdf.rs @@ -0,0 +1,25 @@ +use rocket::{response::{Responder, self}, Request, Response, http::{Header, ContentType}}; +use std::io::Cursor; + + + +pub struct PdfStream { + data: Vec, +} + +impl PdfStream { + pub fn new(data: Vec) -> PdfStream { + PdfStream { data } + } +} + + +#[rocket::async_trait] +impl<'r> Responder<'r, 'static> for PdfStream { + fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> { + Response::build() + .header(ContentType::PDF) + .sized_body(self.data.len(), Cursor::new(self.data)) + .ok() + } +} diff --git a/src/tools/tera.rs b/src/tools/tera.rs new file mode 100644 index 0000000..e47c69c --- /dev/null +++ b/src/tools/tera.rs @@ -0,0 +1,16 @@ +use rocket::{request::{FromRequest, Outcome}, Request}; +use tera::Tera; + +#[derive(Debug)] +pub struct NanoTera(pub Tera); + + +// Allows a route to access the span +#[rocket::async_trait] +impl<'r> FromRequest<'r> for NanoTera { + type Error = (); + + async fn from_request(request: &'r Request<'_>) -> Outcome { + rocket::outcome::Outcome::Success(NanoTera(Tera::new("templates/*").unwrap())) + } +} \ No newline at end of file diff --git a/templates/two_column.html.tera b/templates/two_column.html.tera new file mode 100644 index 0000000..6c5e659 --- /dev/null +++ b/templates/two_column.html.tera @@ -0,0 +1,200 @@ + + + + + + + + + + +
+
+
+

Ondřej Vlach

+
Software engineer
+
+
+
+

Experience

+
+
+
+
+
Title
+
Company
+
PHP HTML Aholamora Snake
+

+ baklklbkalkblaklkb lakbl aklkbl akblka lkblakbla +

+
+
+
Location
+
Dates
+
+
+
+
+
+
+

Experience

+
+
+
+
+
Title
+
Company
+
PHP HTML Aholamora Snake
+

+ baklklbkalkblaklkb lakbl aklkbl akblka lkblakbla +

+
+
+
Location
+
Dates
+
+
+
+
+
+
+

Experience

+
+
+
+
+
Title
+
Company
+
PHP HTML Aholamora Snake
+

+ baklklbkalkblaklkb lakbl aklkbl akblka lkblakbla +

+
+
+
Location
+
Dates
+
+
+
+
+
+
+ Photo2342342314213 423142342314213423 + +
+ L + +
+ +
+
+ + \ No newline at end of file